Cégünknél a közelmúltig túlnyomóan az Android és iOS rendszerekre külön történt a fejlesztés. A Flutter bevezetésével az egyszerűbb alkalmazások már cross-platform kerültek fejlesztésre, a hatékonyságot bizonyos esetekben ez nagyban növelte. A Flutter 3-as verziójában már széles körű webes támogatás is megjelent.
Kíváncsiak voltunk, hogy a már meglévő mobilos Flutter moduljainkat mekkora munkával tudnánk webes platformra is fordítani. Hol vannak a buktatók? Mi az, amit nyerünk és mi az, amit vesztünk a közös kódbázissal?
A továbbiakban élő példákon keresztül megpróbálunk választ adni ezekre a kérdésekre, és kipróbáljuk a Flutter webes részét is. Tartsatok velünk!
Mi a Flutter?
A Flutter egy, a Google által fejlesztett keretrendszer, melynek a programozási nyelve a szintén Google által fejlesztett objektumorientált Dart nyelv. Ez egy ingyenes és nyílt forráskódú mobil UI framework, mely 2017. májusában jelent meg. A Flutter egyik előnye a többi cross-platform nyelvhez képest, hogy natív kódra build-elődik, így a cross-platform sebesség problémái megoldottak.
A legfrissebb verziója a 3.0-ás, ami az Androidon és az iOS-en kívül már stabilan támogatja a Windows, Linux, macOS és WEB alkalmazásokat is. Ezzel a harmadik verzióval lett stabil, de beta-ként már a 2.10.5-ös, általunk használt verzióban is volt lehetőség ezekre a platformokra build-elni. Ezzel egy kódbázisból tudunk összesen 6 különböző platformra fejleszteni.
Miért érdemes a Fluttert választani?
A Flutter egy modern keretrendszer, amellyel sokkal egyszerűbb a mobilalkalmazások létrehozása, mintha Java, Swift vagy React Native-ban készülnének. Flutter kód írásakor valós időben tudjuk megtekinteni az eredményeket - ezt hívják Hot-reloadnak -, így csak jelentősebb módosítások esetén van szükség a készülő alkalmazás újraindítására.
A Flutter használatának előnyei:
- ideális induló ötletek prototipizálásához,
- ideális teljes rendszerek kivitelezéséhez,
- olcsón és gyorsan lehet jó minőségű kódot előállítani, amely minden platformon használható,
- jó dokumentációval rendelkezik,
- sok külső féltől származó plugin elérhető,
- az Android Studio és a VS Code is támogatja.
Flutter projekt buildelése webes környezetre
A Flutter kétfajta webes támogatással rendelkezik:
- Single-Page apps (SPAs), melyek egyetlen oldalon futnak, és dinamikusan frissülnek új oldal betöltése nélkül, és
- Progressive web apps (PWAs), melyek asztali alkalmazásként futtathatóak.
Ha egy mobilalkalmazást szeretnénk webalkalmazássá alakítani, első lépésként a Flutter főkönyvtárában ki kell adnunk a
flutter create .
parancsot. Ennek a parancsnak létre kell hoznunk a webes környezetre készülő verzió könyvtárát is az android és ios mappa mellett.
Ezután a legfontosabb lépés, hogy a mobilalkalmazás által használt összes csomagot megvizsgáljuk, hogy létezik-e a webes változata. Ezt a pub.dev oldalon tudjuk megtenni, ahol a keresőbe bemásolva a csomag nevét az kiadja a csomagot, és tudjuk ellenőrizni, hogy a címkék között szerepel-e a web. Ha nem szerepel, kereshetünk alternatívát, vagy saját magunk elkészíthetjük, melyet utána publikálhatunk is a pub.dev-en. A képernyő arányok miatt előfordulhat, hogy ami mobilon jól nézett ki, az a weben nem fog. Ezt javíthatjuk a Layout Builder segítségével.
JavaScript támogatás
Mivel a Flutter legelőször az internetböngészők nyelve volt, tartalmaz egy js csomagot, mely natív átjárhatóságot biztosít a Dart és a JavaScript között. Ahhoz, hogy használni tudjuk a JavaScriptet, a pubspec.yaml-fájlba a dependecies alá fel kell vennünk a js-t.
@JS() annotáció segítségével tudjuk hívni a javaScript kódokat. Ha egyedi nevet szeretnénk adni neki Dart-ba, akkor azt a @JS(“név”) segítségével tehetjük meg.
@JS("Dog")
class DartDog {
...
}
JavaScript objektum paraméterként
Létre kell hoznunk egy osztályt, és meg kell jegyeznünk a @JS() és @anonymous karakterekkel. Az alábbi példa szerint az Options osztályt fogjuk használni.
@JS()
@anonymous
class Options {
external bool get bed;
external String get hardness;
external factory Options({bool bed, String hardness});
}
Funkciók átadása JavaScriptnek
A függvényparaméterhez a függvényünket az allowInterop-pal kell csomagolnunk, így:
dog.jump(allowInterop((int height){
print(height);
}));
Ahhoz, hogy ez működjön, másoljuk a generált Dog.js fájlt a TypeScript fájlból a Flutter projekt webmappájába, az index.html mellé. Ezután nyissuk meg az index.html fájlt, és adjuk hozzá ezt a sort az import main.dart.js szkriptcímke elé.
<script src="Dog.js" type="application/javascript"></script>
A mobil és a webalkalmazás közötti leglényegesebb különbség a navigálás lesz. Míg mobilon kötött útvonal van arra, hogy jutunk el egy-egy oldalra, addig a weben az url-t átírva is megtehejtük ezt, kihagyva már pár lépést és képernyőt. Ennek orvoslására használható a beamer csomag. A beamer csomag a Navigator 2.0 API-t használja és megvalósítja az összes mögöttes logikát.
Egy alap példakód, amely megnyitja az alkalmazás indításakor a HomePage()-et:
class HomeLocation extends BeamLocation {
@override
List<String> get pathBlueprints => ['/'];
@override
List<BeamPage> pagesBuilder(BuildContext context) => [
BeamPage(
key: ValueKey('home'),
child: HomePage(),
),
];
}
Az alábbi példa megmutatja, hogyan lehet megnyitni egy terméket, hogy az url-t változtatjuk:
class ProductsLocation extends BeamLocation {
@override
List<String> get pathBlueprints => ['/products/:productId'];
@override
List<BeamPage> pagesBuilder(BuildContext context) => [
...HomeLocation().pagesBuilder(context),
if (pathSegments.contains('products'))
BeamPage(
key: ValueKey('products-${queryParameters['title'] ?? ''}'),
child: ProductsPage(),
),
if (pathParameters.containsKey('productId'))
BeamPage(
key: ValueKey('product-${pathParameters['productId']}'),
child: ProductDetailsPage(
productId: pathParameters['productId'],
),
),
];
}
Ha a felhasználó megnyitja a /products oldalt, akkor megjelenít minden keresett elemet. Ha a /products:/productId eléréssel nyitja meg, akkor létrejön egy részletező oldal, melyben pathParameters-ként megkapja a productId-t, és kiírja azt például: “product 2”. Ennek a segítségével elkerülhetőek a 404-es hibák, ha olyat akarna felhasználó megnyitni, ami nem létezik.
Zengos Flutter modulok
Ebben a blogposztban 3 általunk készült mobilos modult vizsgáltunk meg, hogy mennyire lehetséges mininális hozzányúlással webes projektekben felhasználni. Ezek a modulok a Magyar Szürkék Útja projektünkhöz készültek.
Térkép modul
Nem lehetséges a közvetlen átállás. Ez a modul egy egyedi Google Maps, amelyet már a régebbi projektekben is használtunk, de most átkerült Flutteres verzióra. Magát a modult úgy alakítottuk ki, hogy támogassa a GMS szolgáltatások nélküli készülékeket is. A Huawei készülékeknél a Petal Map API-hoz kapcsolódik, ami a Huawei Google Mapset kiválató térképes szoftvercsomagja.
Maga a Flutteres Google Maps api nem támogatja natívan a weben történő feldolgozást. Alternatívaként lehetőség van a google_maps_flutter_web használatára, de a Web Maps JavaScriptes változata fizetős, melyet ez a Flutter csomag tartalmaz. A modul a felépítéséből adódóan nem futtatható, csak egy modulba importálva használható (nincsen main run függvénye).
Puzzle játék modul
Körülményes az átállás jelen állapotában, mivel a képvágáshoz használt Image osztály a dart.io-ból jön, de a webes részhez a dart.html-es Image és Picture-re van szükség, amely nem tudja kezelni a feldolgozáshoz és daraboláshoz szükséges dolgokat. A legnagyobb probléma, hogy a projekt kialakítása miatt lokálisan tárolva vannak a content-ek, hogy offline is lehessen használni, azonban a dart.html-el csak Image.asset() és Image.network() képfeldolgozást tud használni.
Ha beégetett vagy az interneten lévő képeket használjuk, akkor már futtatható az alkalmazás, de ezután egy újabb probléma került elő: a dart.html-es Image osztálynak nincs copyResized() funkciója, melynek a segítésével rajzoljuk ki a puzzle egyes elemeit. Maga a tábla és a háttérben lévő kép - amire ki kell rakni a darabokat - mérete a SizeConfig segítségével beállítható.
Véleményem szerint az átalakítás rövidebb időt venne igénybe, mivel a használt dependencies-eknek már van webes támogatása is, így magát a kép betöltéshez és kirajzoláshoz kapcsolodó feldolgozást kellene átalakítani. Lehetőség van konverter írására, mellyel áttudjuk alakítani a képet. Az újraírás több időt venne igénybe, mert akkor a Widget-eket és a puzzle darabolás logikáját is újra el kellene készíteni.
Memória játék
Gond nélkül fordult mobilos projektből webes projektre. Ami átdolgozandó rész, az a screen size config. Maga a modul paraméterként kapja meg az eszköztől a képernyő méreteket, melyeket a MediaQuery-ből kalkulál ki:
import 'package:flutter/widgets.dart';
class SizeConfig {
static late double _screenWidth;
static late double _screenHeight;
static void init(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
_screenWidth = screenSize.width;
_screenHeight = screenSize.height;
}
static double getProportionateScreenWidth(double inputWidth) {
return (inputWidth / 375.0) * _screenWidth;
}
static double getProportionateScreenHeight(double inputHeight) {
return (inputHeight / 812.0) * _screenHeight;
}
}
Ezt a függvényt kell beépíteni és egy kicsit átdolgozni, hogy tökéletes legyen minden képarányon. A modulok úgy lettek elkészítve, hogy paraméterben kapjanak minden értéket (cím, színek, méretek, callback események, képek listája), hogy minél több projektben újra felhasználhatóak legyenek, átalakítások nélkül.
Konklúzió
Maga a Flutter egyre jobban fejlődik és egyre nagyobb szeletet vesz ki a mobilos nyelvek tortájából. Jelen pillanatban, ha mobilon és weben is szeretnénk egy projektet készíteni egy kód bázisból, és úgy is indul a fejlesztés, hogy figyelembe vettük a webes részt is, úgy elég egyszerűen tudunk alkalmazásokat készíteni.
Ha egy már meglévő mobilos projektet szeretnénk webes projektté alakítani, adódhat egy pár döccenő. Van, ahol alaposabb átdolgozásokra is szükség lehet a platformok sajátossága miatt. Szerencsére egyszerűen le tudjuk kérdezni, hogy webes környezetben futunk-e a kIsWeb konstans segítségével, vagy mobilos környezetben (Platform.android || Plaftorm.ios).
A lokális assetek tárolása és elérése (pl.: képek, adatbázisok, szövegek, amelyeket az alkalmazásban használunk) platformonként változó. Így a webes feldolgozásnál más a fájl elérés módja, mint mobilos környezetekben. Emiatt meg kell vizsgálnunk az elérési útvonalakat is, és platform specifikusan módosítani azokat.
else if (node.attributes['src'].startsWith('asset:'))
{ final assetPath =
node.attributes['src'].replaceFirst('asset:', '');
};
Előfordulhat, hogy tesztelés során még a cache-ből olvassa az adatokat, és az új javításokat, változásokat nem lehet még tesztelni. Ennek megoldását az alábbiakban részletezzük.
A projekt létrehozása után az index.html fájl így fog kinézni:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>to_cache_or_not_to_cache</title>
</head>
<body>
<script src="main.dart.js"
type="application/javascript"></script>
</body>
</html>
Ha szeretnénk, hogy hibajavítás után ne a cache-elt verziót használjuk, annyi a teendőnk, hogy a main.dart.js importálásánál egy verziót is adunk meg neki:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>to_cache_or_not_to_cache</title>
</head>
<body>
<script src="main.dart.js?version=1"
type="application/javascript"></script>
</body>
</html>
Ez a megoldás garantálja az alkalmazás újratöltődését, és továbbra is a gyorsítótárban tárolja az adatokat, míg úgy nem döntünk, hogy új frissítést kell végrehajtani. Ekkor csak a verzió számot kell emelnünk. Az az egy kis hátránya van, hogy egy ideig még a felhasználónak az előző verzió maradhat a böngészőjében. Ennek a kiküszöböléséhez szerver-oldali megoldásoknál a Cache-Control-t kell kikapcsolnunk.