Fa iskola a Flutterben? „Kertész leszek, fát nevelek, kelő nappal én is kelek.” – mondta ezt anno József Attila, akinek természetesen köze nem volt a Flutterhez.
Jogos a kérdés, hogy akkor meg mi ez a hülyeség???? 🙂 🙂 🙂
A jó hír, hogy nem ment el az eszem (még). És igen, a Flutterben léteznek fák. Még hozzá három is:
- Widget Tree (Widget fa)
- Element Tree (elem fa)
- Render Tree (renderelési fa)
A legjobb lesz, ha elmagyarázom ezeket és nem húzom tovább az időt.
Azzal kezdem, hogy az Element és a Render Tree olyan fák, amikkel fejlesztőként nem sokat fogunk találkozni. Ezek kvázi láthatatlanok.
Widget Tree
A Widget Tree az nem más mint widgetek hierarchiája, kombinációja a programban.
void main() {
runApp(
const MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Fejléc címe');
), // AppBar
body: GradientContainer.purple(),
), // Scaffold
), // MaterialApp
);
}
Látható, hogy van itt néhány widget, amik egymás belsejében vannak (widget a widgeten belül), tehát egy fát alkotnak.
Element Tree
A Flutter a memóriában is elkészíti ezt a fát a Widget Tree alapján. Mindezt azért teszi, hogy neki sokkal könnyebb legyen hatékonyan frissíteni a felhasználó felületet. Szóval ez a Flutter biznisze a színfalak mögött.
Tehát minden egyes Widgetnek, amit a kódban használtunk, írtunk, van megfeleltethető párja a memóriában.
Egy widget build metódusa elég gyakran lefut. Például azért, mert meghívjuk a setState() metódust, aminek a változásokat vissza kell vezetnie a képernyőre. Valahányszor egy widgetnek lefut a build metódusa, az ezen a widgeten belüli gyerek widgetek build metódusai is lefutnak. Mivel a Flutternek ellenőriznie kell, hogy a teljes widget fa és az abban történt változások megjelenjenek a képernyőn.
De a Flutternek nem szükségszerű az összes objektumot újra létrehoznia. Főleg azokat nem, amik már egyébként is léteznek. Ehelyett a teljes fának csak azon részeit frissíti, ahová új widgetek kerülnek be, vagy meglévő widgetek törlődnek.
A legfontosabb tehát, hogy a fában az elemek újrahasznosításra kerülnek, és nincsenek újra létrehozva minden egyes esetben, amikor frissül a felhasználói felület. Akkor kerülnek csak létrehozásra, amikor a widget legelőször a fába kerül. És ez nagyon hatékony működést tesz lehetővé!
Render Tree
A Flutter az Element Tree-t használja arra, hogy a képernyő „rajzolásakor” meg tudja határozni, hogy mely felhasználói felület elemeket kell neki meg- vagy újra rajzolnia (renderelnie).
A Render Tree a látható felhasználói elemeknek a fája, kombinációja. Tehát, amit a felhasználó lát a mobil képernyőjén.
A hatékony működés érdekében a Flutter tehát a képernyő azaz Render Tree nem változó elemeit békén hagyja. Amikor tehát valami megváltozik, akkor a Flutter megnézi az Element fát, hogy mi változott, és csak ezeket a változásokat vezeti át a Render fára, vagyis a képernyőre.
Felhasználói felület frissítésének folyamata
A lényeg tehát, hogy amikor a build() metódus hívásra kerül, a Flutter csekkolja az Element fát, szükség szerint újrahasználja a fában már létező elemeket. Aztán összehasonlítja az új Element fát a régivel, és, ha bármi eltérést talál, akkor ezeket a változásokat szépen átvezeti a Render fába és a képernyőn megjelennek a változások.
Nem rendereli tehát újra az egész képernyőt, hanem csak a változásokat.
És mindez az egész azért jó, mert nekünk fejlesztőknek a kisujjunkat sem kell mindezért mozdítani. A Flutter megcsinál mindent.
A mi reszortunk csak a Widget fa. Mi csak a widgeteket írogatjuk és hívogatjuk, építjük a widget fát, a többit a Flutter intézi az Element- és a Render fával. Fejlesztőként azonban nem árt tisztában lenni azzal, hogy mi zajlik a háttérben.
A felesleges widget buildek elkerülése
Az már világos, hogy a Flutter csak azokat a widgeteket fogja kirenderelni a képernyőre, amik változtak.
Amikor van egy StatefulWidget-ünk és valami kiváltja a setState()-et, akkor ez minden alkalommal futtatja a widgetnek a build metódusát. Ez egyúttal azt is jelenti, hogy azoknak a Widgeteknek a build metódusai is lefutnak, amik a Stateful widgeten belül vannak. Ezt viszont nem minden esetben akarjuk.
Itt van ez a példa kód:
class _UIUpdatesDemo extends State<UIUpdatesDemo> {
var _isUnderstood = false;
@override
Widget build(BuildContext context) {
print('UIUpdatesDemo BUILD called');
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
'Every Flutter developer should have a basic understanding of Flutter\'s internals!',
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text(
'Do you understand how Flutter updates UIs?',
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
setState(() {
_isUnderstood = false;
});
},
child: const Text('No'),
),
TextButton(
onPressed: () {
setState(() {
_isUnderstood = true;
});
},
child: const Text('Yes'),
),
],
),
if (_isUnderstood) const Text('Awesome!'),
],
),
),
);
}
}
Amikor lefut a setState és ezáltal meghívódik a build metód, akkor a Flutter az összes gyerek widgetnek is futtatja a build metódusát. Ha változik valami a képernyőn, ha nem, akkor is.
Ha pontosan tudjuk, hogy mely widget fog változni a képernyőn, azaz melyiket kell újra renderelni, akkor jó lenne csak ennek az egynek a build metódusát lefuttatni nem? És a többit meg békén hagyni. Ez elég hasznos lehet egy bazi nagy app esetében ahol kismillió widget van egymásba ágyazva, magyarul a widget tree egy kész Mammutfenyő.
A fenti példában mindössze egyetlenegy widgetnek kell a képernyőn frissülnie, az pedig ez a feltételtől függően megjelenő / eltűnő Text widget:
if (_isUnderstood) const Text('Awesome!'),
Szóval mi a bánatnak kellene akkor az összes többinek is feleslegesen hívogatni a build metódusát?
A megoldás tehát, hogy csak azok a widgetek maradjanak a StatefulWidget-ben, amik valóban változnak. Ami úgymond statikus, az kerüljön át inkább egy StatelessWidget-be.
Illetve még egy jó tanács, hogy egy StatefulWidge legyen olyan kicsi, amilyen kicsi csak lehet, hogy ezzel is növeljük a performanciáját / teljesítményét, gyorsaságát az alkalmazásunknak.