Oddělení zájmů v aplikacích Flutter
Engineering at ClickUp

Oddělení zájmů v aplikacích Flutter

Nedávno jsem musel implementovat průvodce pro nové uživatele ClickUp! Byla to opravdu důležitá úloha, protože mnoho nových uživatelů se chystalo objevit platformu díky neuvěřitelně vtipné reklamě, kterou jsme uvedli na Super Bowlu! ✨

prostřednictvím ClickUp

Díky tomuto návodu mohou naši četní noví uživatelé, kteří možná ClickUp ještě neznají, rychle pochopit, jak používat několik funkcí aplikace. Jedná se o průběžnou snahu, stejně jako nový zdroj ClickUp University, na kterém pracujeme! 🚀

Naštěstí mi softwarová architektura mobilní aplikace ClickUp Flutter umožnila tuto funkci implementovat poměrně rychle, a to i díky opětovnému použití skutečných widgetů z aplikace! To znamená, že průvodce je dynamický, responzivní a přesně odpovídá skutečným obrazovkám aplikace – a bude tomu tak i nadále, i když se widgety budou vyvíjet.

Funkčnost se mi podařilo implementovat také díky správnému oddělení jednotlivých oblastí.

Podívejme se, co tím myslím. 🤔

Oddělení zájmů

Návrh softwarové architektury je jedním z nejkomplexnějších témat pro inženýrské týmy. Ze všech povinností je vždy obtížné předvídat budoucí vývoj softwaru. Proto vám a vašim kolegům může vytvoření dobře vrstvené a oddělené architektury pomoci v mnoha ohledech!

Hlavní výhodou vytváření malých oddělených systémů je bezpochyby testovatelnost! A právě to mi pomohlo vytvořit demo alternativu stávajících obrazovek z aplikace!

Podrobný průvodce

Jak bychom nyní mohli tyto principy aplikovat na aplikaci Flutter?

Podělíme se o několik technik, které používáme k vývoji ClickUp, s jednoduchým příkladem.

Příklad je tak jednoduchý, že možná neosvětluje všechny výhody, které se za ním skrývají, ale věřte mi, že vám pomůže vytvořit mnohem lépe udržovatelné aplikace Flutter se složitými kódovými základnami. 💡

Aplikace

Jako příklad vytvoříme aplikaci, která zobrazuje počet obyvatel USA za každý rok.

prostřednictvím ClickUp

Máme zde dvě obrazovky:

  • HomeScreen: jednoduše zobrazuje seznam všech let od roku 2000 do současnosti. Když uživatel klepne na dlaždici s rokem, přejde na DetailScreen s navigačním argumentem nastaveným na vybraný rok.
  • DetailScreen : získá rok z navigačního argumentu, volá API datausa.io pro tento rok a analyzuje data JSON, aby extrahoval související hodnotu populace. Pokud jsou data k dispozici, zobrazí se štítek s počtem obyvatel.

Zaměříme se na implementaci DetailScreen, protože je nejzajímavější díky asynchronnímu volání.

Krok 1. Naivní přístup

Naivní přístup Widget se stavem
prostřednictvím ClickUp

Nejviditelnější implementací pro naši aplikaci je použití jediného StatefulWidget pro celou logiku.

Přístup k argumentu navigace po letech

Pro přístup k požadovanému roku čteme RouteSettings z widgetu zděděného z ModalRoute.

HTTP volání

Tento příklad volá funkci get z balíčku http, aby získal data z API datausa.io, analyzuje výsledný JSON pomocí metody jsonDecode z knihovny dart:convert a uchovává Future jako součást stavu s vlastností nazvanou _future.

Renderování

K vytvoření stromu widgetů používáme FutureBuilder, který se sám přestavuje podle aktuálního stavu našeho asynchronního volání _future.

Recenze

Dobře, implementace je krátká a používá pouze vestavěné widgety, ale teď si vzpomeňte na náš původní záměr: vytvořit demo alternativy (nebo testy) pro tuto obrazovku. Je velmi obtížné kontrolovat výsledek HTTP volání, aby se aplikace vykreslila v určitém stavu.

Právě zde nám pomůže koncept inverze řízení. 🧐

Krok 2. Inverze řízení

Inverze řízení na poskytovatele a klienta API
prostřednictvím ClickUp

Tento princip může být pro nové vývojáře těžko pochopitelný (a také těžko vysvětlitelný), ale celková myšlenka spočívá v extrakci problémů mimo naše komponenty, aby nebyly zodpovědné za výběr chování, a místo toho je delegovat.

V běžnější situaci se jedná jednoduše o vytvoření abstrakcí a vložení implementací do našich komponent, aby jejich implementace mohla být v případě potřeby později změněna.

Ale nebojte se, po našem dalším příkladu to bude dávat větší smysl! 👀

Vytvoření objektu API klienta

Abychom mohli kontrolovat HTTP volání do našeho API, izolovali jsme naši implementaci do speciální třídy DataUsaApiClient. Vytvořili jsme také třídu Measure, která usnadňuje manipulaci s daty a jejich údržbu.

Poskytněte API klienta

V našem příkladu používáme známý balíček provider k vložení instance DataUsaApiClient do kořenového adresáře našeho stromu.

Použití API klienta

Poskytovatel umožňuje jakémukoli potomkovi widgetu (jako náš DetailScreen) číst nejbližší DataUsaApiClient v horní části stromu. Poté můžeme použít jeho metodu getMeasure k zahájení našeho Future namísto skutečné implementace HTTP.

Ukázka API klienta

Nyní toho můžeme využít!

Pokud jste to nevěděli: všechny třídy v jazyce Dart implicitně definují také přidružené rozhraní. To nám umožňuje poskytnout alternativní implementaci DataUsaApiClient, která vždy vrací stejnou instanci z volání metody getMeasure.

Tato metoda

Tato metoda

Zobrazení demo stránky

Nyní máme všechny klíče k zobrazení demo instance DetailPage!

Jednoduše přepisujeme aktuálně poskytovanou instanci DataUsaApiClient tím, že zabalíme náš DetailScreen do poskytovatele, který místo toho vytvoří instanci DemoDataUsaApiClient!

A to je vše – naše DetailScreen místo toho čte tuto demo instanci a místo HTTP volání používá naše demoMeasure data.

Recenze

Toto je skvělý příklad inverze řízení. Náš widget DetailScreen již není zodpovědný za logiku získávání dat, ale místo toho ji deleguje na specializovaný klientský objekt. A nyní jsme schopni vytvořit demo instance obrazovky nebo implementovat testy widgetů pro naši obrazovku! Skvělé! 👏

Ale můžeme to udělat ještě lépe!

Jelikož například nejsme schopni simulovat stav načítání, nemáme plnou kontrolu nad žádnou změnou stavu na úrovni našich widgetů.

Krok 3. Správa stavu

Správa příkazů v aplikacích Flutter
prostřednictvím ClickUp

Toto je v Flutteru velmi aktuální téma!

Jistě jste již četli dlouhé diskuze lidí, kteří se snaží vybrat nejlepší řešení pro správu stavu pro Flutter. Aby bylo jasno, v tomto článku se tím nebudeme zabývat. Podle našeho názoru stačí, když oddělíte svou obchodní logiku od vizuální logiky! Vytvoření těchto vrstev je opravdu důležité pro udržovatelnost. Náš příklad je jednoduchý, ale v reálných aplikacích se logika může rychle stát složitou a takové oddělení výrazně usnadňuje nalezení čistých logických algoritmů. Toto téma se často shrnuje jako správa stavu.

V tomto příkladu používáme základní ValueNotifier spolu s Provider. Mohli jsme ale použít také flutter_bloc nebo riverpod (nebo jiné řešení), a fungovalo by to také skvěle. Principy zůstávají stejné a pokud jste oddělili stavy a logiku, je dokonce možné přenést kódovou základnu z jednoho z jiných řešení.

Toto oddělení nám také pomáhá kontrolovat jakýkoli stav našich widgetů, takže je můžeme simulovat všemi možnými způsoby!

Vytvoření vyhrazeného stavu

Místo toho, abychom se spoléhali na AsyncSnapshot z frameworku, nyní reprezentujeme stav naší obrazovky jako objekt DetailState.

Je také důležité implementovat metody hashCode a operátor ==, aby byl náš objekt srovnatelný podle hodnoty. To nám umožňuje detekovat, zda by měly být dvě instance považovány za odlišné.

💡 Balíčky equatable nebo freezed jsou skvělou volbou, která vám pomůže implementovat metody hashCode a operátor ==!

💡 Balíčky equatable nebo freezed jsou skvělými možnostmi, které vám pomohou implementovat metody hashCode a operátor ==!

Náš stav je vždy spojen s rokem, ale máme také čtyři odlišné možné stavy týkající se toho, co chceme uživateli ukázat:

  • NotLoadedDetailState: aktualizace dat ještě nezačala
  • LoadingDetailState: data se právě načítají
  • LoadedDetailState: data byla úspěšně načtena s přidruženou měrou.
  • NoDataDetailState: data byla načtena, ale nejsou k dispozici žádná data.
  • UnknownErrorDetailState: operace selhala z důvodu neznámé chyby

Tyto stavy jsou jasnější než AsyncSnapshot, protože skutečně reprezentují naše případy použití. A opět, díky tomu je náš kód lépe udržovatelný!

💡 Pro vyjádření logických stavů důrazně doporučujeme typy Union z balíčku freezed! Přidávají mnoho užitečných funkcí, jako jsou metody copyWith nebo map.

💡 Pro vyjádření logických stavů důrazně doporučujeme typy Union z balíčku freezed! Přidávají mnoho užitečných funkcí, jako jsou metody copyWith nebo map.

Vložení logiky do Notifieru

Nyní, když máme reprezentaci našeho stavu, musíme jej někde uložit – to je účel DetailNotifier. Uloží aktuální instanci DetailState do své vlastnosti value a poskytne metody pro aktualizaci stavu.

Poskytujeme počáteční stav NotLoadedDetailState a metodu obnovení pro načtení dat z API a aktualizaci aktuální hodnoty.

Poskytněte stav pro zobrazení

K instanciování a sledování stavu naší obrazovky se také spoléháme na poskytovatele a jeho ChangeNotifierProvider. Tento druh poskytovatele automaticky vyhledává všechny vytvořené ChangeListener a pokaždé, když je informován o změně (když se hodnota našeho oznamovatele liší od předchozí), spustí přestavbu z Consumer.

Recenze

Skvělé! Architektura naší aplikace začíná vypadat docela dobře. Vše je rozděleno do dobře definovaných vrstev se specifickými úkoly! 🤗

Pro testovatelnost však stále chybí jedna věc: chceme definovat aktuální DetailState, abychom mohli ovládat stav našeho přidruženého DetailScreen.

Krok 4. Vizuální widget pro specializované rozvržení

Vizuální widgety pro specializované rozvržení v aplikacích Flutter
prostřednictvím ClickUp

V posledním kroku jsme widgetu DetailScreen přidělili příliš velkou odpovědnost: byl zodpovědný za instanciování DetailNotifier. A jak jsme viděli dříve, snažíme se vyhnout jakékoli logické odpovědnosti ve vrstvě zobrazení!

Tento problém můžeme snadno vyřešit vytvořením další vrstvy pro náš widget obrazovky: rozdělíme náš widget DetailScreen na dva:

  • DetailScreen je zodpovědný za nastavení různých závislostí naší obrazovky z aktuálního stavu aplikace (navigace, oznámení, stav, služby, …),
  • DetailLayout jednoduše převádí DetailState na specializovaný strom widgetů.

Kombinací těchto dvou prvků budeme moci jednoduše vytvořit demo/testovací instance DetailLayout, ale mít DetailScreen pro skutečné použití v naší aplikaci.

Vyhrazené rozvržení

Abychom dosáhli lepšího oddělení zájmů, přesunuli jsme vše pod widget Consumer do speciálního widgetu DetailLayout. Tento nový widget pouze spotřebovává data a není zodpovědný za žádné instancování. Pouze převádí stav čtení na konkrétní strom widgetů.

ModalRoute. volání a instance ChangeNotifierProvider zůstávají v DetailScreen a tento widget jednoduše vrací DetailLayout s předem nakonfigurovaným stromem závislostí!

Toto drobné vylepšení se týká konkrétně použití poskytovatele, ale všimnete si, že jsme také přidali ProxyProvider, aby jakýkoli potomský widget mohl přímo využívat DetailState. To usnadní simulování dat.

Extrahování widgetů jako specializovaných tříd

Neváhejte extrahovat strom widgetů do samostatné třídy! Zlepší to výkon a usnadní údržbu kódu.

V našem příkladu jsme vytvořili jeden vizuální widget rozvržení pro každý z přidružených typů stavů:

Ukázkové instance

Nyní máme plnou kontrolu nad tím, co můžeme simulovat a zobrazit na naší obrazovce!

Stačí zabalit DetailLayout do Provideru, abychom simulovali stav rozvržení.

Závěr

Vytvoření udržitelné softwarové architektury rozhodně není snadné! Předvídání budoucích scénářů může vyžadovat hodně úsilí, ale doufám, že několik tipů, které jsem se s vámi podělil, vám v budoucnu pomůže!

Příklady mohou vypadat jednoduše – možná se dokonce může zdát, že to přeháníme – ale jak bude vaše aplikace složitější, tyto standardy vám velmi pomohou! 💪

Bavte se s Flutterem a sledujte blog, abyste získali další technické články, jako je tento! Zůstaňte naladěni!

ClickUp Logo

Jedna aplikace, která nahradí všechny ostatní