A feladatok szétválasztása a Flutter alkalmazásokban
Engineering at ClickUp

A feladatok szétválasztása a Flutter alkalmazásokban

Nemrégiben bevezető útmutatókat kellett készítenem a ClickUp újoncainak! Ez egy nagyon fontos feladat volt, mert sok új felhasználó készült felfedezni a platformot a Super Bowl-on bemutatott hihetetlenül vicces reklámunknak köszönhetően! ✨

via ClickUp

A bemutató segítségével számos új felhasználónk, akik talán még nem ismerik a ClickUp-ot, gyorsan megértheti az alkalmazás több funkciójának használatát. Ez egy folyamatos erőfeszítés, akárcsak az új ClickUp University erőforrás, amelyet jelenleg fejlesztünk! 🚀

Szerencsére a ClickUp Flutter mobilalkalmazás mögötti szoftverarchitektúra lehetővé tette, hogy ezt a funkciót meglehetősen gyorsan megvalósítsam, még az alkalmazás valódi widgetjeinek újrafelhasználásával is! Ez azt jelenti, hogy a bemutató dinamikus, reszponzív és pontosan megegyezik az alkalmazás valódi képernyőivel – és ez akkor is így marad, ha a widgetek továbbfejlődnek.

A funkciót a megfelelő feladatok szétválasztásának köszönhetően is sikerült megvalósítanom.

Nézzük meg, mire gondolok! 🤔

A feladatok szétválasztása

A szoftverarchitektúra tervezése az egyik legbonyolultabb feladat a mérnöki csapatok számára. Az összes feladat közül mindig nehéz előre látni a szoftverek jövőbeli fejlődését. Éppen ezért egy jól rétegzett és szétválasztott architektúra létrehozása sok mindenben segíthet Önnek és csapattársainak!

A kis, egymástól független rendszerek létrehozásának fő előnye kétségkívül a tesztelhetőség! És ez segített nekem az alkalmazás meglévő képernyőinek demo alternatívájának létrehozásában!

Lépésről lépésre

Hogyan alkalmazhatjuk ezeket az elveket egy Flutter alkalmazásra?

Néhány technikát, amelyet a ClickUp fejlesztéséhez használunk, egyszerű példákkal mutatunk be.

A példa annyira egyszerű, hogy talán nem világít rá minden előnyére, de higgye el, segítségével sokkal könnyebben karbantartható Flutter alkalmazásokat hozhat létre komplex kódbázisokkal. 💡

Az alkalmazás

Példaként létrehozunk egy alkalmazást, amely az Egyesült Államok népességét mutatja be évente.

via ClickUp

Két képernyőnk van itt:

  • HomeScreen: egyszerűen felsorolja az összes évet 2000-től napjainkig. Amikor a felhasználó rákattint egy évre, a DetailScreen képernyőre navigál, ahol a navigációs argumentum a kiválasztott évre van beállítva.
  • DetailScreen : lekérdezi az évet a navigációs argumentumból, meghívja a datausa.io API-t az adott évre, és elemezi a JSON adatokat, hogy kivonja a kapcsolódó népességi értéket. Ha adatok állnak rendelkezésre, megjelenik egy címke a népességszámmal.

A DetailScreen implementációjára fogunk összpontosítani, mivel az aszinkron hívásával a legérdekesebb.

1. lépés: Naiv megközelítés

Naiv megközelítés Állapotfüggő widget
via ClickUp

Alkalmazásunk legkézenfekvőbb megvalósítása az, ha egyetlen StatefulWidget-et használunk az egész logikához.

Hozzáférés az év navigációs argumentumához

A kért év eléréséhez elolvassuk a RouteSettings-et a ModalRoute örökölt widgetből.

HTTP-hívás

Ez a példa a http csomag get funkcióját hívja meg, hogy lekérje az adatokat a datausa.io API-ból, a kapott JSON-t a dart:convert könyvtár jsonDecode metódusával elemzi, és a Future-t a _future nevű tulajdonsággal az állapot részeként tárolja.

Renderelés

A widgetfa létrehozásához FutureBuilder-t használunk, amely a _future aszinkron hívás aktuális állapotának megfelelően újjáépíti magát.

Áttekintés

Rendben, a megvalósítás rövid és csak beépített widgeteket használ, de most gondoljon vissza eredeti szándékunkra: demo alternatívák (vagy tesztek) készítése ehhez a képernyőhöz. Nagyon nehéz ellenőrizni a HTTP-hívás eredményét, hogy az alkalmazás egy bizonyos állapotban jelenjen meg.

Itt segít nekünk az irányítás megfordítása koncepciója. 🧐

2. lépés: A vezérlés megfordítása

A vezérlés átadása a Providernek és az api Clientnek
via ClickUp

Ez az elv új fejlesztők számára nehezen érthető (és nehezen magyarázható), de az általános elképzelés az, hogy a komponenseinken kívüli problémákat kivonjuk – hogy azok ne legyenek felelősek a viselkedés kiválasztásáért – és helyette delegáljuk azokat.

Egy általánosabb helyzetben ez egyszerűen abból áll, hogy absztrakciókat hozunk létre és implementációkat illesztünk be komponenseinkbe, hogy azok implementációja később szükség esetén megváltoztatható legyen.

De ne aggódjon, a következő példánk után minden világosabb lesz! 👀

API kliens objektum létrehozása

Az API-hoz intézett HTTP-hívások ellenőrzése érdekében a megvalósítást egy külön DataUsaApiClient osztályba különítettük el. Emellett létrehoztunk egy Measure osztályt is, hogy az adatok kezelése és karbantartása egyszerűbb legyen.

API-kliens biztosítása

Példánkban a jól ismert provider csomagot használjuk, hogy egy DataUsaApiClient példányt illesszünk be a fa gyökérkönyvtárába.

Az API kliens használata

A szolgáltató lehetővé teszi, hogy bármely leszármazott widget (például a DetailScreen) elolvassa a fa legközelebbi DataUsaApiClient felső elemét. Ezután a getMeasure metódusával elindíthatjuk a Future-t, a tényleges HTTP implementáció helyett.

Demo API kliens

Most már kihasználhatjuk ezt az előnyt!

Ha még nem tudta: a dart bármely osztálya implicit módon meghatározza a hozzá tartozó interfészt is. Ez lehetővé teszi számunkra, hogy a DataUsaApiClient alternatív implementációját biztosítsuk, amely mindig ugyanazt az instanciát adja vissza a getMeasure metódushívásokból.

Ez a módszer

Ez a módszer

Demo oldal megjelenítése

Most már minden kulcsunk megvan a DetailPage demo példájának megjelenítéséhez!

Egyszerűen felülírjuk a jelenleg biztosított DataUsaApiClient példányt azzal, hogy a DetailScreen-t egy olyan szolgáltatóba csomagoljuk, amely helyette egy DemoDataUsaApiClient példányt hoz létre!

Ennyi az egész – a DetailScreen ehelyett ezt a demo példát olvassa be, és HTTP-hívás helyett a demoMeasure adatainkat használja.

Áttekintés

Ez egy remek példa az inverziós vezérlésre. A DetailScreen widgetünk már nem felelős az adatok lekérésének logikájáért, hanem ezt egy dedikált kliens objektumra ruházza át. Most már képesek vagyunk létrehozni a képernyő demo példányait, vagy widget teszteket implementálni a képernyőnkre! Fantasztikus! 👏

De még ennél is jobbat tudunk!

Mivel például nem tudjuk szimulálni a betöltési állapotot, nem tudjuk teljes mértékben ellenőrizni az állapotváltozásokat a widget szintjén.

3. lépés: Állapotkezelés

Nyilatkozatkezelés Flutter alkalmazásokban
via ClickUp

Ez egy forró téma a Flutterben!

Biztosan már olvastál hosszú fórumszálakat azokról, akik megpróbálják kiválasztani a legjobb állapotkezelési megoldást a Flutterhez. Hogy egyértelmű legyek, ebben a cikkben nem ezt fogjuk tenni. Véleményünk szerint, amíg elkülöníti az üzleti logikát a vizuális logikától, addig nincs gond! Ezeknek a rétegeknek a létrehozása nagyon fontos a karbantarthatóság szempontjából. Példánk egyszerű, de a valós alkalmazásokban a logika gyorsan bonyolulttá válhat, és az ilyen elkülönítés sokkal könnyebbé teszi a tiszta logikai algoritmusok megtalálását. Ezt a témát gyakran állapotkezelésként foglalják össze.

Ebben a példában egy alapvető ValueNotifier-t használunk egy Provider mellett. De használhattunk volna flutter_bloc-ot vagy riverpod-ot (vagy más megoldást), és az is remekül működött volna. Az elvek ugyanazok maradnak, és ha elkülönítette az állapotokat és a logikát, akkor akár az egyik másik megoldásból is átviheti a kódbázisát.

Ez a szétválasztás segít nekünk a widgetjeink állapotának ellenőrzésében is, így minden lehetséges módon modellezhetjük azokat!

Dedikált állapot létrehozása

Ahelyett, hogy a keretrendszer AsyncSnapshot funkciójára támaszkodnánk, most a képernyő állapotát DetailState objektumként ábrázoljuk.

Fontos továbbá a hashCode és az operator == metódusok implementálása is, hogy objektumainkat érték szerint összehasonlíthatóvá tegyük. Ez lehetővé teszi számunkra, hogy felismerjük, ha két példányt különnek kell tekintenünk.

💡 Az egyenértékű vagy befagyasztott csomagok kiváló lehetőségek a hashCode és az operator == módszerek implementálásához!

💡 Az egyenértékű vagy befagyasztott csomagok kiváló lehetőségek a hashCode és az operator == módszerek implementálásához!

Állapotunk mindig egy évhez kapcsolódik, de négy különböző lehetséges állapotunk is van attól függően, hogy mit szeretnénk megmutatni a felhasználónak:

  • NotLoadedDetailState: az adatok frissítése még nem kezdődött el
  • LoadingDetailState: az adatok betöltése folyamatban van
  • LoadedDetailState: az adatok sikeresen betöltődtek a kapcsolódó mérőszámmal
  • NoDataDetailState: az adatok betöltődtek, de nincs elérhető adat
  • UnknownErrorDetailState: a művelet ismeretlen hiba miatt sikertelen volt.

Ezek az állapotok egyértelműbbek, mint az AsyncSnapshot, mivel valóban a mi felhasználási eseteinket képviselik. És ez ismét javítja a kódunk karbantarthatóságát!

💡 Javasoljuk, hogy a logikai állapotok ábrázolásához használja a freezed csomag Union típusait! Ez számos hasznos funkciót kínál, például a copyWith vagy a map metódusokat.

💡 Javasoljuk, hogy a logikai állapotok ábrázolásához használja a freezed csomag Union típusait! Ez számos hasznos funkciót kínál, például a copyWith vagy a map metódusokat.

A logika beépítése a Notifierbe

Most, hogy már rendelkezünk az állapotunk ábrázolásával, el kell tárolnunk valahol annak egy példányát – ez a DetailNotifier célja. A DetailNotifier az aktuális DetailState példányt az érték tulajdonságában tárolja, és módszereket biztosít az állapot frissítéséhez.

NotLoadedDetailState kezdeti állapotot és frissítési módszert biztosítunk az adatok API-ból való betöltéséhez és az aktuális érték frissítéséhez.

Állapot megadása a nézethez

A képernyőnk állapotának instantiálásához és megfigyeléséhez a szolgáltatóra és annak ChangeNotifierProvider szolgáltatására is támaszkodunk. Ez a fajta szolgáltató automatikusan megkeresi az összes létrehozott ChangeListener-t, és minden alkalommal, amikor változásról értesítést kap (amikor értesítő értékünk eltér az előzőtől), a Consumer újjáépítését indítja el.

Áttekintés

Remek! Alkalmazásunk architektúrája kezd nagyon jól kinézni. Minden jól definiált rétegekre van felosztva, mindegyiknek megvan a maga feladata! 🤗

A tesztelhetőséghez azonban még egy dolog hiányzik: meg akarjuk határozni a jelenlegi DetailState-et, hogy ellenőrizzük a kapcsolódó DetailScreen állapotát.

4. lépés: Vizuális dedikált elrendezés widget

Vizuális, dedikált elrendezésű widgetek a Flutter alkalmazásokban
via ClickUp

Az utolsó lépésben kissé túl sok felelősséget ruháztunk a DetailScreen widgetre: ez volt felelős a DetailNotifier példányosításáért. És ahogy azt korábban láttuk, igyekszünk elkerülni a logikai felelősséget a nézet rétegben!

Ezt könnyen megoldhatjuk úgy, hogy egy újabb réteget hozunk létre a képernyő widgetünkhöz: a DetailScreen widgetünket két részre osztjuk:

  • A DetailScreen feladata a képernyő különböző függőségeinek beállítása a jelenlegi alkalmazás állapotából (navigáció, értesítők, állapot, szolgáltatások stb.).
  • A DetailLayout egyszerűen átalakítja a DetailState-et egy dedikált widgetfa-struktúrává.

A kettő kombinálásával egyszerűen létrehozhatunk DetailLayout demo/teszt példákat, de a DetailScreen-t a valódi felhasználási esethez használhatjuk az alkalmazásunkban.

Dedikált elrendezés

A feladatok jobb szétválasztása érdekében a Consumer widget alatt található összes elemet egy külön DetailLayout widgetbe helyeztük át. Ez az új widget csak adatokat fogyaszt, és nem felelős semmilyen instantiálásért. Csak a read állapotot konvertálja egy adott widget fává.

A ModalRoute. call és a ChangeNotifierProvider példány a DetailScreen-ben marad, és ez a widget egyszerűen visszatér a DetailLayout-hoz egy előre konfigurált függőségi fával!

Ez a kisebb fejlesztés kifejezetten a szolgáltatók használatára vonatkozik, de észreveheti, hogy hozzáadtunk egy ProxyProvider-t is, így bármely leszármazott widget közvetlenül felhasználhatja a DetailState-et. Ez megkönnyíti az adatok modellezését.

Widgetek kivonása dedikált osztályokként

Ne habozzon widgetfát külön osztályba kivonni! Ez javítja a teljesítményt és könnyebbé teszi a kód karbantartását.

Példánkban minden egyes társított állapot típushoz egy vizuális elrendezési widgetet hoztunk létre:

Bemutató példák

Most már teljes ellenőrzésünk van felett, hogy mit tudunk modellezni és megjeleníteni a képernyőn!

Csak be kell burkolnunk a DetailLayout-ot egy Provider-rel, hogy szimuláljuk a layout állapotát.

Következtetés

Kétségtelen, hogy nem könnyű fenntartható szoftverarchitektúrát létrehozni! A jövőbeli forgatókönyvek előrejelzése sok erőfeszítést igényelhet, de remélem, hogy az általam megosztott néhány tipp segít Önnek a jövőben!

A példák egyszerűnek tűnhetnek – akár úgy is tűnhet, hogy túlkomplikáljuk a dolgokat –, de az alkalmazás komplexitásának növekedésével ezek a szabványok nagy segítséget nyújtanak majd Önnek! 💪

Élvezze a Flutter használatát, és kövesse a blogot, hogy több ilyen technikai cikket olvashasson! Maradjon velünk!

ClickUp Logo

Egyetlen alkalmazás, ami az összes többit kiváltja