Verkossa olevien rajapintojen käyttö
Learning objectives
- Kertaat käsitettä rajapinta.
- Tutustut verkossa olevaa rajapintaa käyttävien mobiilisovellusten toteuttamiseen.
- Osaat hakea tietoa verkossa olevasta rajapinnasta ja osaat lähettää tietoa verkossa olevaan rajapintaan.
- Tunnet käsitteen kapselointi ja osaat luoda rajapinnan toiminnan kapseloivan luokan.
Tarkastelimme edellisessä osassa ohjelmointiympäristön käyttöönottoa ja opimme tekemä än mobiilisovelluksesta pyynnön verkossa olevaan palveluun. Ohjelmamme näytti seuraavanlaiselta.
import 'package:flutter/material.dart';
import 'package:http/http.dart';
main() async {
final url = Uri.parse('https://fitech-api.deno.dev/sum?one=43&two=-1');
final response = await get(url);
final teksti = Text(response.body);
final sovellusrunko = Scaffold(body: teksti);
final sovellus = MaterialApp(home: sovellusrunko);
runApp(sovellus);
}
Käsitettä rajapinta (API, application programming interface) käsitellään kurssin Internet- ja selainohjelmointi osassa Tiedon hakeminen avoimesta rajapinnasta. Tässä osassa käsittelemme rajapintojen käyttöä mobiilisovelluksissa.
Tämän osan esimerkeissä ja tehtävissä käytetään http-kirjastoa. Kirjasto http
tulee lisätä paikallisessa ohjelmointiympäristössä käsiteltävän sovelluksen pubspec.yaml
tiedoston dependencies
kohtaan. Kirjasto on korostettuna alla olevassa pubspec.yaml
tiedostoa kuvaavassa tekstissä.
name: sovellukseni
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: 1.0.1
http: 0.12.2
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
flutter:
uses-material-design: true
Rajapinnat ja datan muoto
Rajapintoja käsiteltäessä rajapinnasta haettavan datan ja rajapintaan lähetettävän datan muoto on rajapinnan toteuttajan määrittelemä. Esimerkiksi koirien kuvia tarjoava Dog API palauttaa JSON-muotoisen dokumentin, jossa on kentät (avain-arvo -parit) message
ja status
. Tällainen dokumentti näyttää esimerkiksi seuraavalta.
{
"message": "https://images.dog.ceo/breeds/shiba/shiba-9.jpg",
"status": "success"
}
Osoitteessa https://simple-joke-api.deno.dev/random oleva satunnaisesti valitun vitsin palauttava rajapinta palauttaa JSON-muotoisen dokumentin, jossa on kentät setup
ja punchline
. Tällainen dokumentti näyttää esimerkiksi seuraavalta.
{
"setup":"What do you call a belt made out of watches?",
"punchline":"A waist of time."
}
Rajapinnan palauttama data voi tarjota myös kenttiä, jotka sisältävät monimutkaisempaa dataa. Esimerkiksi osoitteessa https://api.frankfurter.app/latest?from=EUR&to=USD oleva rajapinta tarjoaa viimeisimmät valuuttakurssit eurosta kohdevaluuttaan (tässä USD
). Rajapinta palauttaa JSON-dokumentin, joka sisältää kentät amount
, base
, date
, ja rates
. Kenttä rates
sisältää avain-arvo -pareja, jossa avain on valuutta ja arvo on kyseisen valuutan arvo. Koska edellä olevassa pyynnössä määritellään kohdevaluutta, sisältää rates
vain yhden avain-arvo -parin. Tämä dokumentti näyttää esimerkiksi seuraavalta.
{
"amount":1.0,
"base":"EUR",
"date":"1999-12-30",
"rates":{
"USD":1.0046
}
}
Jokainen edellä nähdyistä JSON-dokumenteista voidaan kuvata myös luokkia käyttäen. Luokkia käsiteltiin muunmuassa Data ja tieto-kurssin osassa Rakenteellisen tiedon esittäminen luokkien avulla.
Esimerkiksi koiran kuvan osoitetta kuvaava dokumentti voidaan esittää seuraavalla luokalla.
class Koirakuva {
String message;
String status;
Koirakuva(this.message, this.status);
}
Vastaavasti vitsi ja valuuttakurssit voidaan kuvata seuraavien luokkien avulla.
class Vitsi {
int id;
String type;
String setup;
String punchline;
Vitsi(this.id, this.type, this.setup, this.punchline);
}
class Valuuttakurssit {
double amount;
String base;
String date;
Map rates;
Valuuttakurssit(this.amount, this.base, this.date, this.rates);
}
Rajapinnasta saatavan datan käsittely
Edellä kuvatut rajapinnat palauttavat aina merkkijonon. Vastaavasti, niille lähetettävä tieto on aina merkkijonomuotoista. Tämä liittyy siihen, että verkossa käytetyn HTTP-protokollan viestit ovat merkkijonomuotoisia. Voit lukea lisää HTTP-protokollasta kurssin Internet- ja selainohjelmointi osasta Internetin perusosat tai kurssin Web software development osasta HTTP Protocol.
Koska rajapinnat palauttavat merkkijonon, muunnetaan rajapinnasta saatava tieto ennen käsittelyä. Ensin merkkijonomuotoinen JSON-dokumentti muunnetaan sanakirjaksi, jonka jälkeen sanakirjasta tehdään rajapinnan palauttamaa sisältöä vastaava olio.
Muunnos merkkijonosta sanakirjaksi tapahtuu pakkauksesta dart:convert
löytyvällä funktiolla jsonDecode
. Olion luomista varten luomme käsiteltävään luokkaan konstruktorin, jolle annetaan sanakirja parametrina.
Alla olevassa esimerkissä näytetään miten merkkijonomuotoinen JSON-dokumentti muunnetaan olioksi. Ensin merkkijono muunnetaan sanakirjaksi, jonka jälkeen sanakirja annetaan luokan nimetylle konstruktorille (tässä sanakirjasta
). Lopulta luodun olion oliomuuttujien message
ja status
arvot tulostetaan.
Kirjastoa http
käytetään tiedon hakemiseen annetusta osoitteesta. Tietoa haettaessa merkkijonomuotoinen osoite muunnetaan verkko-osoitetta kuvaavaksi osoitteeksi Uri
-luokan tarjoamalla funktiolla parse
. Tämän jälkeen osoitteeseen tehdään pyyntö get
-funktiolla. Pyynnön vastauksena saadaan Response-olio, jolla on merkkijonomuotoinen oliomuuttuja body
. Tämä muuttuja sisältää pyynnön vastauksen.
Alla olevassa esimerkissä näytetään sovellus, joka käynnistyessään hakee osoitteesta https://dog.ceo/api/breeds/image/random
merkkijonon ja muuntaa sen Koirakuva
-olioksi. Lopulta ohjelmassa näytetään Koirakuva
-olion message
-muuttuja (eli kuvan osoite) merkkijonona.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
main() async {
final url = Uri.parse('https://dog.ceo/api/breeds/image/random');
final response = await get(url);
final sanakirja = jsonDecode(response.body);
final kuva = Koirakuva.sanakirjasta(sanakirja);
final teksti = Text(kuva.message);
final sovellusrunko = Scaffold(body: teksti);
final sovellus = MaterialApp(home: sovellusrunko);
runApp(sovellus);
}
class Koirakuva {
String message;
String status;
Koirakuva(this.message, this.status);
Koirakuva.sanakirjasta(sanakirja) {
this.message = sanakirja['message'];
this.status = sanakirja['status'];
}
}
Rajapinnat ja kuvaava nimeäminen
Edellä olevat esimerkit eivät kovin hyvin kuvaa rajapinnasta konkreettisesti saatavaa tietoa. Esimerkiksi, luokan Koirakuva
muuttuja message
voidaan ymmärtää viestinä, vaikka rajapinnassa se todellisuudessa sisältää kuvan.
Voimme halutessamme määritellä luokat niin, että ne ovat ohjelmoijan kannalta ymmärrettävämmät. Kaikkia kenttiä ei myöskään tarvitse sisällyttää luokkiin, ellei niitä erikseen käytetä. Esimerkiksi luokan Koirakuva
voi määritellä huomattavasti kuvaavammin.
class Kuva {
String osoite;
Kuva(this.osoite);
Kuva.sanakirjasta(sanakirja) {
this.osoite = sanakirja['message'];
}
}
Tapahtumat ja haku rajapinnasta
Edellisissä esimerkeissä rajapintahaut on toteutettu siten, että haku tehdään sovelluksen käynnistyessä. Tarkastellaan seuraavaksi hakujen tekemistä silloin kun sovellus on jo käynnissä. Opimme osassa Tapahtumat ja sovelluksen tila käsittelemään tapahtumia ja sovelluksen tilaa. Hyödynnämme tässä oppimaamme -- luomme sovelluksen, joka tarjoaa toiminnallisuuden uusien kuvien hakemiseen nappia painettaessa.
Käytämme StatefulWidget
luokasta perittyä luokkaa Kuvahaku
tilallisen komponentin kuvastamiseen. Komponentin konkreettinen sisältö sekä tilan päivityslogiikka luodaan State
-luokan perivään luokkaan KuvahakuState
. Oleellinen toiminnallisuus liittyy kuvan hakemiseen, joka toteutetaan hae
-metodiin. Alla hae
-metodi on jätetty tyhjäksi.
Sovelluksessa näytetään oletuksena osoitteesta https://images.dog.ceo/breeds/shiba/shiba-17.jpg haettu kuva.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
main() {
final sovellus = MaterialApp(home: Scaffold(body: Kuvahaku()));
runApp(sovellus);
}
class Kuvahaku extends StatefulWidget {
KuvahakuState createState() => KuvahakuState();
}
class KuvahakuState extends State {
Koirakuva koirakuva = Koirakuva('https://images.dog.ceo/breeds/shiba/shiba-17.jpg', '');
hae() async {
// uuden kuvan hakeminen
}
Widget build(BuildContext context) {
final nappi = ElevatedButton(child: Text('Hae'), onPressed: hae);
return Column(children: [nappi, Image.network(koirakuva.message)]);
}
}
class Koirakuva {
String message;
String status;
Koirakuva(this.message, this.status);
Koirakuva.sanakirjasta(sanakirja) {
this.message = sanakirja['message'];
this.status = sanakirja['status'];
}
}
Toteutetaan seuraavaksi toiminnallisuus uuden kuvan hakemiseen. Uusi kuva haetaan kun käyttäjä painaa sovelluksessa olevaa nappia.
Metodi hae
on toistaiseksi tyhjä. Kun luomme asynkronista toiminnallisuutta, jossa joudumme odottamaan sovelluslogiikan suoritusta, käytämme avainsanaa async
funktion määrittelyssä ja avainsanaa await
suorituksen odottamiseen. Kuvan hakeminen sisältää merkkijonomuotoisen osoitteen muuntamisen osoitteeksi, tiedon hakemisen osoitteesta, sekä tiedon käsittelyn. Oleellisin osa tästä toiminnallisuudesta näyttää seuraavalta.
var url = Uri.parse('https://dog.ceo/api/breeds/image/random');
var response = await get(url);
var sanakirja = jsonDecode(response.body);
Sovelluksen tilaa päivitetään setState
-kutsun jälkeen. Tässä tila liittyy oleellisesti koiran kuvan tiedot sisältävään koirakuva
-muuttujaan, joka on luokan KuvahakuState
ainoa oliomuttuja. Mikäli haluamme päivittää sovelluksen tilaa, tulee meidän päivittää koirakuva
-muuttujan arvoa setState
-funktiokutsun yhteydessä.
Rajapintahaun ja sovelluksen tilan päivittämisen yhdistäminen näyttää kokonaisuudessaan seuraavalta.
hae() async {
// ensin haetaan kuva
var url = Uri.parse('https://dog.ceo/api/breeds/image/random');
var response = await get(url);
var sanakirja = jsonDecode(response.body);
// sitten päivitetään tila
setState(() {
koirakuva = Koirakuva.sanakirjasta(sanakirja);
});
}
Kokonaisuudessaan satunnaisten koirakuvien hakemiseen käytetty sovellus näyttää seuraavanlaiselta.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
main() {
final sovellus = MaterialApp(home: Scaffold(body: Kuvahaku()));
runApp(sovellus);
}
class Kuvahaku extends StatefulWidget {
KuvahakuState createState() => KuvahakuState();
}
class KuvahakuState extends State {
Koirakuva koirakuva = Koirakuva('https://images.dog.ceo/breeds/shiba/shiba-17.jpg', '');
hae() async {
// ensin haetaan kuva
var url = Uri.parse('https://dog.ceo/api/breeds/image/random');
var response = await get(url);
var sanakirja = jsonDecode(response.body);
// sitten päivitetään tila
setState(() {
koirakuva = Koirakuva.sanakirjasta(sanakirja);
});
}
Widget build(BuildContext context) {
final nappi = ElevatedButton(child: Text('Hae'), onPressed: hae);
return Column(children: [nappi, Image.network(koirakuva.message)]);
}
}
class Koirakuva {
String message;
String status;
Koirakuva(this.message, this.status);
Koirakuva.sanakirjasta(sanakirja) {
this.message = sanakirja['message'];
this.status = sanakirja['status'];
}
}
Tarkastellaan vielä toista esimerkkiä. Osoitteessa https://api.open-meteo.com/v1/forecast?current_weather=true&latitude=leveysaste&longitude=korkeusaste
, missä leveysaste
ja korkeusaste
korvataan sopivilla arvoilla, on rajapinta joka tarjoaa säätietoja. Esimerkiksi osoitteessa https://api.open-meteo.com/v1/forecast?current_weather=true&latitude=60.1699&longitude=24.9384 on (kutakuinkin) Helsingin ajankohtainen säätieto. Data on esimerkiksi seuraavaa muotoa:
{
"latitude":60.16998,
"longitude":24.94519,
"generationtime_ms":1.5480518341064453,
"utc_offset_seconds":0,
"timezone":"GMT",
"timezone_abbreviation":"GMT",
"elevation":8.0,
"current_weather":
{
"temperature":-3.8,
"windspeed":25.6,
"winddirection":134.0,
"weathercode":73,
"time":"2023-01-09T13:00"
}
}
Käytämme tässä rajapinnan tarjoamaa tietoa säästä. Oleelliset muuttujat rajapinnan palauttamassa JSON-muotoisessa dokumentissa on kannaltamme current_temperature
arvon alla oleva temperature
, joka sisältää lämpötilan.
Luodaan ensin säätä kuvaava luokka Saa
. Luokalla on muuttuja temperature
, konstruktori ja nimetty konstruktori sanakirjasta
, sekä luokan merkkijonomuotoisen kuvauksen palauttava metodi toString
.
class Saa {
String temperature;
Saa(this.temperature);
Saa.sanakirjasta(sanakirja) {
this.temperature = sanakirja['current_temperature']['temperature'];
}
toString() {
return '${temperature}';
}
}
Luodaan seuraavaksi sovellus, joka mahdollistaa sään hakemisen syötetystä kaupungista. Sovelluksessa on tilallinen komponentti SaaPalvelu
, joka perii luokan StatefulWidget
. Konkreettinen tila ja tilan päivityslogiikka toteutetaan luokkaan SaaPalveluState
, joka perii luokan State
. Kun sovellus käynnistyy, sovelluksessa näytetään tekstikentt ä sekä merkkijono ?
, joka kuvastaa ettei säätä ole vielä haettu.
Alla on sovelluksen runko, josta puuttuu hae
-funktioon toteutettava konkreettinen haku- ja päivitystoiminnallisuus.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
main() {
final sovellus = MaterialApp(home: Scaffold(body: SaaPalvelu()));
runApp(sovellus);
}
class SaaPalvelu extends StatefulWidget {
SaaPalveluState createState() => SaaPalveluState();
}
class SaaPalveluState extends State {
final api = 'https://api.open-meteo.com/v1/forecast?current_weather=true';
final textEditingController = TextEditingController();
Saa saa = Saa('?');
hae() async {
// sään hakeminen ja päivittäminen
}
Widget build(BuildContext context) {
final tekstikentta = TextField(
controller: textEditingController,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(Icons.send),
onPressed: hae
),
)
);
return Column(children: [tekstikentta, Text(saa.toString())]);
}
}
class Saa {
String temperature;
Saa(this.temperature);
Saa.sanakirjasta(sanakirja) {
this.temperature = sanakirja['current_temperature']['temperature'];
}
toString() {
return '${temperature}';
}
}
Haku rajapinnasta toteutetaan lähes samalla tavalla kuin aiemmin. Tällä kertaa haettavaan osoitteeseen liitetään tekstikentän arvo, joka saadaan textEditingController
-olion muuttujasta text
. Oletetaan, että tekstikenttään kirjoitetaan leveys- ja korkeusaste pilkulla eroteltuna, ja että rajapinnan osoite on muuttujassa api
.
var haettava = textEditingController.text;
var osat = haettava.split(",");
var leveysaste = osat[0];
var korkeusaste = osat[1];
var url = Uri.parse('${api}&latitude=${leveysaste}&longitude=${korkeusaste}');
var response = await get(url);
var sanakirja = jsonDecode(response.body);
Sovelluksen tilan päivitys tapahtuu setState
-kutsun jälkeen. Säätietoja näyttävässä sovelluksessa oleellinen tila on saa
-muuttujan arvo. Mikäli haluamme päivittää sovelluksen tilaa, tulee meidän päivittää saa
-muuttujan arvoa setState
-funktiokutsun yhteydessä.
Rajapintahaun ja sovelluksen tilan päivittämisen yhdistäminen näyttää kokonaisuudessaan seuraavalta.
hae() async {
// ensin haetaan tieto rajapinnasta
var haettava = textEditingController.text;
var osat = haettava.split(",");
var leveysaste = osat[0];
var korkeusaste = osat[1];
var url = Uri.parse('${api}&latitude=${leveysaste}&longitude=${korkeusaste}');
var response = await get(url);
var sanakirja = jsonDecode(response.body);
// ja sitten päivitetään sovelluksen tila
setState(() {
saa = Saa.sanakirjasta(sanakirja);
});
}
Kokonaisuudessaan sovellus näyttää seuraavalta.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
main() {
final sovellus = MaterialApp(home: Scaffold(body: SaaPalvelu()));
runApp(sovellus);
}
class SaaPalvelu extends StatefulWidget {
SaaPalveluState createState() => SaaPalveluState();
}
class SaaPalveluState extends State {
final api = 'https://api.open-meteo.com/v1/forecast?current_weather=true';
final textEditingController = TextEditingController();
Saa saa = Saa('?');
hae() async {
var haettava = textEditingController.text;
var osat = haettava.split(",");
var leveysaste = osat[0];
var korkeusaste = osat[1];
var url = Uri.parse('${api}&latitude=${leveysaste}&longitude=${korkeusaste}');
var response = await get(url);
var sanakirja = jsonDecode(response.body);
// sitten päivitetään
setState(() {
saa = Saa.sanakirjasta(sanakirja);
});
}
Widget build(BuildContext context) {
final tekstikentta = TextField(
controller: textEditingController,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(Icons.send),
onPressed: hae
),
)
);
return Column(children: [tekstikentta, Text(saa.toString())]);
}
}
class Saa {
String temperature;
Saa(this.temperature);
Saa.sanakirjasta(sanakirja) {
this.temperature = sanakirja['current_temperature']['temperature'];
}
toString() {
return '${temperature}';
}
}
Tiedon lähettäminen rajapintaan
Olemme aiemmin tarkastelleet muutamaa rajapintaa, jossa tietoa lähetetään rajapintaan osana osoitetta tai pyynnön parametreja. Esimerkiksi edellisessä esimerkissä käsitellyssä sääpalvelussa haettava kaupunki asetettuun osaksi osoitetta. Rajapintaan voi lähettää tietoa myös pyynnön rungossa.
Tämä tapahtuu http-kirjaston post
-funktiolla. Funktiolle post
annetaan parametrina osoite, mahdolliset otsaketiedot, sekä pyynnön runko. Otsaketiedoilla kerrotaan pyynnön sisällöstä (Content-Type
) ja pyynnön runko sisältää konkreettisesti lähetettävän datan. Alla olevassa esimerkissä kuvataan tiedon lähettämiseen liittyvät oleellisimmat osat.
var osoite = 'https://osoite.com';
var otsakeTiedot = {'Content-Type': 'application/json; charset=UTF-8'};
var sisaltoSanakirjana = {'avain': 'arvo'};
await post(
Uri.parse(osoite),
headers: otsakeTiedot,
body: jsonEncode(sisaltoSanakirjana)
);
Yllä riveillä 1-3 määritellään rajapinnan osoite, rajapintaan lähetettävät otsaketiedot, sekä pyynnön sisältö. Rivistä 5 lähtien tehdään konkreettinen pyyntö. Kuten get
-funktiota käytettäessä, myös post
-funktiota käytettäessä merkkijonomuotoinen osoite tulee muuntaa osoitteeksi Uri.parse
-funktiolla. Otsaketiedot annetaan parametrin headers
arvoksi ja pyynnön runko annetaan parametrin body
arvoksi. Funktio jsonEncode
muuntaa sille annetun sanakirjan merkkijonomuotoiseksi JSON-dokumentiksi.
Tarkastellaan tätä vielä seuraavan esimerkin kautta. Osoitteessa https://fitech-api.deno.dev/sum-api
on rajapinta, jolle voi lähettää JSON-muotoista dataa merkkijonomuodossa. Lähetettävän dokumentin tulee sisältää kentät one
ja two
, jotka sisältävät numerot. Rajapinta palauttaa JSON-muotoisen dokumentin, jossa oleva kentät sum
sisältää lähetettyjen kentät arvojen summa.
Esimerkiksi, mikäli lähetettävä dokumentti sisältää arvot 1
ja 2
muuttujille one
ja two
.
{
"one": 1,
"two": 2
}
On vastauksessa muuttujalla sum
arvo 3
.
{
"sum": 3
}
Konkreettinen tiedon lähettämiseen ja hakemiseen käytettävä logikkaa on tällöin seuraavanlainen. Alla otsakkeet on asetettu suoraan post
-kutsun kentän headers
arvoksi.
var lahetettava = {
'one': 1,
'two': 2
};
var response = await post(
Uri.parse('https://fitech-api.deno.dev/sum-api'),
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode(lahetettava)
);
var sanakirja = jsonDecode(response.body);
Alla oleva esimerkki kuvastaa ohjelmaa, joka sisältää kaksi tekstikenttää sekä napin. Kun käyttäjä syöttää tekstikenttiin numerot ja painaa nappia, sovellus tekee pyynnön rajapintaan, ja näyttää rajapinnasta saadun arvon käyttäjälle.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart';
main() {
final sovellus = MaterialApp(home: Scaffold(body: SummaLaskin()));
runApp(sovellus);
}
class SummaLaskin extends StatefulWidget {
SummaLaskinState createState() => SummaLaskinState();
}
class SummaLaskinState extends State {
final TextEditingController ekaController = TextEditingController();
final TextEditingController tokaController = TextEditingController();
var summa = '';
laske() async {
var lahetettava = {
'one': ekaController.text,
'two': tokaController.text
};
var response = await post(
Uri.parse('https://fitech-api.deno.dev/sum-api'),
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode(lahetettava)
);
var sanakirja = jsonDecode(response.body);
// sitten päivitetään
setState(() {
summa = '${sanakirja['sum']}';
});
}
Widget build(BuildContext context) {
final ekaKentta = TextField(
controller: ekaController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly]
);
final tokaKentta = TextField(
controller: tokaController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly]
);
final nappi = ElevatedButton(child: Text('Laske'), onPressed: laske);
return Column(children: [ekaKentta, tokaKentta, nappi, Text(summa)]);
}
}
Rajapinnan kapselointi
Tähän asti nähdyissä esimerkeissä ja tehtävissä rajapintaa on käsitelty suoraan siten, että rajapinnan käsittelyyn tarkoitettu logiikka on ollut samassa tiedostossa muun sovelluslogiikan kanssa. Sovelluksen koon kasvaessa sovellus on kuitenkin mielekästä pilkkoa pienempiin osiin.
Kapseloinnilla (encapsulation) tarkoitetaan samaan aihealueeseen liittyvien toiminnallisuuksien eriyttämistä omaksi kokonaisuudekseen kuten esimerkiksi luokaksi tai erilliseksi funktioita sisältäväksi tiedostoksi. Kapseloinnin yhteydessä konkreettinen toiminnallisuus piilotetaan luokkaa tai funktioita käyttävältä ohjelmoijalta siten, että ohjelmoija voi käyttää toiminnallisuutta, mutta ohjelmoijan ei tarvitse jatkuvasti nähdä miten toiminnallisuus on toteutettu.
Esimerkiksi summan laskemiseen käytetyn osoitteessa https://fitech-api.deno.dev/sum-api
olevan rajapinnan voi kapseloida erilliseen luokkaan SummaApi
, joka tarjoaa metodin laske
. Metodi laske
saa kaksi parametria, jotka ovat rajapinnalle lähetettävät arvot, ja palauttaa rajapinnasta saadun arvon. Luokka näyttää seuraavalta.
import 'dart:convert';
import 'package:http/http.dart';
class SummaApi {
laske(eka, toka) async {
var data = {
'one': eka,
'two': toka
};
var response = await post(
Uri.parse('https://fitech-api.deno.dev/sum-api'),
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode(data)
);
var sanakirja = jsonDecode(response.body);
return sanakirja['sum'];
}
}
Mikäli yllä kuvattu luokka on määriteltynä tiedostossa summa_api.dart
, tulee tiedoston sisältö tuoda ohjelman käyttöön komennolla import 'summa_api.dart'
. Tämän jälkeen luokkaa SummaApi
voidaan käyttää sovelluksessa aivan kuten mitä tahansa muita luokkia. Nyt rajapintaa käyttävän luokan SummaLaskinState
metodi laske
on merkittävästi selkeämpi.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'summa_api.dart';
main() {
final sovellus = MaterialApp(home: Scaffold(body: SummaLaskin()));
runApp(sovellus);
}
class SummaLaskin extends StatefulWidget {
SummaLaskinState createState() => SummaLaskinState();
}
class SummaLaskinState extends State {
final TextEditingController ekaController = TextEditingController();
final TextEditingController tokaController = TextEditingController();
var summa = '';
laske() async { var eka = ekaController.text; var toka = tokaController.text; var tulos = await SummaApi().laske(eka, toka); setState(() { summa = '$tulos'; }); }
Widget build(BuildContext context) {
final ekaKentta = TextField(
controller: ekaController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly]
);
final tokaKentta = TextField(
controller: tokaController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly]
);
final nappi = ElevatedButton(child: Text('Laske'), onPressed: laske);
return Column(children: [ekaKentta, tokaKentta, nappi, Text(summa)]);
}
}
Hi! Please help us improve the course!
Please consider the following statements and questions regarding this part of the course. We use your answers for improving the course.
I can see how the assignments and materials fit in with what I am supposed to learn.
I find most of what I learned so far interesting.
I am certain that I can learn the taught skills and knowledge.
I find that I would benefit from having explicit deadlines.
I feel overwhelmed by the amount of work.
I try out the examples outlined in the materials.
I feel that the assignments are too difficult.
I feel that I've been systematic and organized in my studying.
How many hours (estimated with a precision of half an hour) did you spend reading the material and completing the assignments for this part? (use a dot as the decimal separator, e.g 8.5)
How would you improve the material or assignments?