Jazyk Dart je nutné znát, když chceme ve Flutteru psát. Proto se mrkneme nejen na to, jak se ve Flutteru píše, ale taky si ho představíme z hlediska návrhu. Odpovíme si i na základní otázku: Proč to funguje multiplatformě? Nakonec zavoláme nativní službu konkrétní platformy.
Vyvíjíme mobilní aplikaci ve Flutteru (2/3)
Minule jsme si řekli něco o frameworku Flutter. Nakonfigurovali jsme si prostředí, abychom mohli začít vytvářet Flutter aplikace, popsali si, jak vypadá a co obsahuje projekt Flutter aplikace. Nakonec jsme si ukázali, jak vytvořit platformě závislý dialog.
Dnes si představíme Flutter techničtěji. Ukážeme si jazyk Dart tak, abychom věděli, jak se v tom Flutteru vlastně píše. Rozebereme Flutter z hlediska návrhu a vysvětlíme, proč to vlastně funguje multiplatformě. Nakonec si ukážeme, jak zavolat nativní službu konkrétní platformy. Tak pojďme na to.
Jazykové okénko
Dart je objektový programovací jazyk, který je syntaxí velmi podobný dnešním moderním jazykům, jako je Kotlin, C# nebo Java.
Proměnné
Dart je silně typový jazyk. To znamená, že každá proměnná má svůj datový typ, který je určen při vytvoření proměnné a nelze ho během chodu programu měnit. Při zakládání nové proměnné není nutné explicitně určit typ, ale můžeme pomocí klíčového slova var
využít dedukci typu. I když se jedná o silně typový jazyk, umožňuje použitím klíčového slova dynamic
, vytvoření proměnné bez “datového typu”.
// explicitní datový typ String firstName = 'Eman'; // dedukce datového typu var surname = 'Novák'; // proměnný datový typ dynamic company = 'eMan.cz'; //firstName = 10; // chyba při kompilaci //surname = 10; // chyba při kompilaci company = 10;
Třídy a objekty
Všechno v Dartu je objekt, každý objekt je instancí nějaké třídy a všechny třídy dědí od třídy Object
. Třídy se skládají z třídních proměnných a metod (statické metody), objekty pak z instančních proměnných a metod.
Jedním z konceptů objektově orientovaného programování je zapouzdření tříd. Například v Javě lze definovat několik úrovní viditelnosti, které jsou většinou řešené pomocí klíčových slov jazyka, jako private
, protected
, či public
. V Dartu existuje jen public a private viditelnost, která není definovaná speciálními klíčovými slovy, ale pro private viditelnost stačí při deklaraci třídy, proměnné apod., použít v názvu prvku prefix _
. Viditelnost se ovšem nevztahuje na úroveň tříd, ale na úroveň jednotlivých částí programu – knihoven (library
).
class _MyPrivateClass { String _myPrivateVariable = '...' // privátní proměnná v privátní třídě String myPublicVariable = '...' // veřejná proměnná v privátní třídě } class MyPublicClass { String _myPrivateVariable = '...' // privátní proměnná ve veřejné třídě String myPublicVariable = '...' // veřejná proměnná ve veřejné třídě }
Poznámka: Defaultně je každý vlastní dart soubor knihovnou. To znamená, že jednotlivé privátní komponenty nejsou mezi soubory viditelné.
Zajímavým konceptem jazyka jsou tzv. Named constructors. Jedná se o klasický konstruktor objektu, kterému lze ale definovat jméno. U vhodně pojmenovaného konstruktoru pak jeho název vystihuje způsob vzniku objektu. Například v Javě by šel podobný konstrukt vytvořit pomocí statické metody, která by prováděla vytvoření a inicializaci samotného objektu.
class Person { String firstName; String surname; Person.fromJson(Map data) { firstName = data['first_name']; surname = data['surname']; } } main() { var person = new Person.fromJson({'first_name': 'Eman', 'surname': 'von Prag'}); print(person.firstName); // Eman print(person.surname); // von Prag }
Funkce
Kromě metod podporuje Dart i klasické funkce, a jelikož všechno je objekt, jsou i ony objekt. Hlavní funkcí je funkce main()
, která je vstupní funkcí programu, takže každý program bude začínat právě touto funkcí. Definice funkcí do sebe můžeme vnořovat a vytvářet tzv. Nested function. Každá vnořená funkce získává kontext funkce, do které byla vnořena – vidí její lokální proměnné.
bool topLevel = true; void main() { var insideMain = true; void nestedFunction() { var insideNestedFunction = true; void nestednestedFunction() { var insideNestedNestedFunction = true; assert(topLevel); assert(insideMain); assert(insideNestedFunction); assert(insideNestedNestedFunction); } //assert(insideNestedNestedFunction) chyba překladu - neznámá proměnná } }
Null-aware operátory
Podobně jako Kotlin i Dart nabízí několik operátorů, které zjednodušují zápis Null Pointer safety kódu.
- If null operátor –
expr1 ?? expr2
– pokud není výrazexpr1
roven null, vyhodnotí se celý výraz jakoexpr1
, jinakexpr2
- Null-aware assignment –
variable ??= expr
– pokud je hodnotavariable
rovna null, přiřadí se do proměnné výrazexpr
- Null-aware access –
variable?.method
– pokud není hodnotavariable
rovna null, provede se zavolání metodymethod
a vrátí se výsledek volání, jinak se vrátí null
Kaskáda
Slouží k zavolání více metod na jednom objektu. Například v Kotlinu se docílí stejného chování pomocí apply
.
class Person { String firstName; String surname; String company; } main() { var person = new Person() ..firstName = 'Bob' ..surname = 'Clever' ..company = 'eMan.cz'; }
Všechno je widget
“Všechno je widget”, to je věta, se kterou jste se už určitě setkali, pokud jste četli nějaký článek o Flutter. Ano, je tomu opravdu tak, všechno je widget. Všechno, i samotná aplikace, je widget. Díky tomu lze ke každému prvku UI přistupovat jednotně. Rozlišujeme dva druhy widgetů:
- Stateless widget – Prvky, které reprezentují stateless widget, jsou neměnné, tzn. nemění svůj vnitřní stav. Stav prvku je nastaven při jeho konstrukci a nelze ho během života objektu měnit (všechny atributy třídy jsou final).
- Stateful widget – Prvky, které mohou a také mění svůj vnitřní stav. Respektive stateful widgety se skládají ze dvou částí:
- části, která reprezentuje samotný widget a zůstává celou dobu také neměnná
- části, která reprezentuje stav widgetu, může se měnit a slouží k perzistenci stavu widgetu.
Každý widget je velice štíhlý, protože se stará pouze o tu činnost, pro kterou byl vytvořen, o žádnou jinou. Například widget Text
, který umí zobrazit text, se stará pouze o samotné zobrazení a například vůbec neřeší svoji pozici vůči ostatním wigetům nebo schopnost sbírání událostí (například kliků). Pokud bychom ho chtěli o nějakou další funkcionalitu rozšířit, jednoduše ho obalíme widgetem, který danou funkcionalitu podporuje. V konečném výsledku pak vývoj takové Flutter aplikace vede ke skládání jednotlivých widgetů za docílením požadované funkcionality, no prostě takové lego. A lego má přece každý rád 😉
Technologie Flutteru
Flutter nevyužívá žádné nativní prvky cílové platformy. Všechny prvky napsali vývojáři Flutteru znovu, lépe a tak, aby vyhovovaly zvyklostem cílové platformy. Většina zdrojového kódu aplikace je překládána přímo do strojového kódu výsledného procesoru (na Androidu pomocí Android NDK, na iOS pak prostřednictvím LLVM). Díky tomu máme jen jeden zdrojový kód pro obě platformy. A taky by pak, alespoň na Androidu, měla být výsledná aplikace mnohem rychlejší.
Jednotlivé vrstvy frameworku jsou zobrazeny na schématu níže. Závislost jednotlivých vrstev je shora dolů – každá výše položená vrstva závisí na vrstvách níže. Zelená část zobrazuje hlavní komponenty frameworku Flutter. Jak vidíte, Material (Android) komponenta a Cupertino (iOS) komponenta se nacházejí na stejné úrovni, což značně komplikuje vývoj platformě závislé aplikace, jak jsme se přesvědčili v předchozím článku. V některých materiálech přímo na stránkách Flutteru můžete najít schéma, kde Material komponenta závisí na Cupertino komponentě. To by zmíněný problém řešilo, nicméně aktuální zdrojové kódy Flutteru spíše nasvědčují tomu, že se obě komponenty nacházejí na stejné úrovni.
V modré části se pak nacházejí komponenty jádra Flutteru, které se starají o samotný běh aplikace. Komponenta Skia se stará o rendrování 2D grafiky, komponenta Dart o AOT kompilaci částí kódu, které nemohly být zkompilovány přímo do strojového kódu a komponenta Text o rendrování textu.
Nativní rozhraní
Flutter umožňuje zavolat určitou službu nativního rozhraní platformy prostřednictvím tzv. Method channel. Flutter aplikace (klient) posílá zprávu s požadavkem nativní části aplikace (host). Pokud host zprávu zná, obslouží požadavek a vrátí zpět odpověď. Pro správné obsloužení musejí klient a host implementovat stejný protokol zprávy.
Ze schématu je patrné, že jsme schopni prostřednictvím Method channel zavolat libovolné služby nativní platformy, nebo služby, které nabízí knihovny třetích stran. Mimo to se jedná také o jeden ze způsobů, jak pomocí platformě-specifických služeb “naimplentovat” službu, kterou Flutter neumožňuje. Toho lze využít například když nemáme knihovnu s požadovanou službou dostupnou přímo pro Flutter, ale existují knihovny jak pro Android, tak iOS.
Celý princip si ukážeme na konkrétním příkladě – zjištění stavu baterie našeho zařízení. Jelikož Flutter umožňuje psaní zdrojového kódu platformě-specifických částí i v Kotlinu pro Android část a Swiftu pro iOS část, zvolil jsem ukázky v těchto jazycích.
Klient
Klientská část aplikace odešle požadavek na získání úrovně baterie a po obdržení výsledku zobrazí naformátovaný výsledek uprostřed obrazovky. Zpracování zprávy je asynchronní, takže stav baterie můžeme zobrazit až po obdržení výsledku. V ukázce můžete vidět jeden ze způsobů, jak zpracovat takové asynchronní volání.
Důležitou částí je zde konstrukce objektu MethodChannel
, který v parametru konstruktoru očekává název vytvářeného kanálu. Pomocí názvu pak na straně hosta provedeme spojení s klientem. Aby spolu mohli klient a host komunikovat, musejí nejen komunikovat přes stejný kanál, ale také si v tomto kanále posílat a očekávat stejné typy zpráv. K tomu slouží metoda invokeMethod()
, kde v parametru metody definujeme typ zprávy.
Soubor main.dart v adresáři lib by měl pak vypadat následovně:
const platform = const MethodChannel('battery'); void main() { platform .invokeMethod('getBatteryLevel') // odešle zprávu s požadavkem 'getBatteryLevel' .then((result) => 'Battery level at $result %.') .catchError((error) => "Failed to get battery level: '${error.message}'.") .then((msg) => runApp(new Center( child: new Text( msg, textDirection: TextDirection.ltr, ), ))); }
Host
Důležitou částí na straně hosta je zaregistrování handleru na stejném kanálu, který byl definován na straně klienta. Po obdržení konkrétního typu zprávy, který lze zjistit pomocí call.method
, provedeme požadovanou operaci a výsledek uložíme do objektu result
.
Soubor MainActivy.kt v adresáři android/app/src/main/kotlin/package_name by pak měl vypadat následovně:
class MainActivity : FlutterActivity() { private val CHANNEL = "battery" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) GeneratedPluginRegistrant.registerWith(this) MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result -> if (call.method == "getBatteryLevel") { val batteryLevel = getBatteryLevel() if (batteryLevel != -1) { result.success(batteryLevel) } else { result.error("UNAVAILABLE", "Battery level not available.", null) } } else { result.notImplemented() } } } private fun getBatteryLevel(): Int { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) } else { val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) } } }
Soubor AppDelegate.swift v adresáři ios/Runner by pak měl vypadat následovně:
@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { GeneratedPluginRegistrant.register(with: self); let controller : FlutterViewController = window?.rootViewController as! FlutterViewController; let batteryChannel = FlutterMethodChannel.init(name: "battery", binaryMessenger: controller); batteryChannel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in if (call.method == "getBatteryLevel") { let batteryLevel : Int = self.getBatteryLevel(); if(batteryLevel != -1) { result(batteryLevel); } else { result(FlutterError.init(code: "UNAVAILABLE", message: "Battery info unavailable", details: nil)); } } else { result(FlutterMethodNotImplemented); } }); return super.application(application, didFinishLaunchingWithOptions: launchOptions); } private func getBatteryLevel() -> Int { let device = UIDevice.current; device.isBatteryMonitoringEnabled = true; if (device.batteryState == UIDeviceBatteryState.unknown) { return -1; } else { return Int(device.batteryLevel * 100); } } }
Závěr
Dnes jsme si ukázali, v čem a jak psát Flutter aplikaci. Řekli jsme si, z jakých částí se samotná aplikace skládá a z jakých částí se skládá framework Flutter. Na závěr jsme se podívali, jak komunikovat s nativním rozhraním platformy za účelem volání platformě-specifických služeb. Celý dnešní projekt naleznete na eManím GitHubu.
Příště se konečně pustíme do vývoje větší aplikace. Tam zúročíme znalosti nabyté tímto trochu delším, ale snad zajímavým úvodem do Flutteru.