Vyvíjíme mobilní aplikaci ve Flutteru (2/3)

flutter app eMan

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.

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átorexpr1 ?? expr2 – pokud není výraz expr1 roven null, vyhodnotí se celý výraz jako expr1, jinak expr2
  • Null-aware assignmentvariable ??= expr – pokud je hodnota variable rovna null, přiřadí se do proměnné výraz expr
  • Null-aware accessvariable?.method – pokud není hodnota variable rovna null, provede se zavolání metody method 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.

 

Zdroje

Filip Šmíd
Android Developer

RSS