Android, MVVM a repozitáře ve skutečném světě

(Mike DeMaso) (2. listopadu 2020)

Jak dosáhnete responzivního uživatelského rozhraní při zachování čistého, přesného, ​​čitelného a udržovatelného kódu? To je otázka, která skáče mozkům vývojářů Android od roku 2008.

Po mnoha letech, kdy vývojářům Androidu umožňoval, aby si to vyřešili sami, Google v poslední době poskytl určité pokyny k tomuto tématu s jejich Průvodce architekturou aplikace , který propaguje variantu MVVM . I když je to skvělý začátek, týmům přetrvává spousta otázek, na které si budou moci odpovědět sami. Google také poskytuje knihovny jako LiveData , Místnost a DataBinding , které nám pomohou snížit počet kotlů a oddělit obavy našeho kódu. Dokonce i třetí strany poskytly pomocnou ruku s RxJava a Retrofit , což nám umožňuje způsob, jak zvládnout asynchronní práci a načítat věci po síti. Když se všechny tyto věci vznášejí kolem řešení malých kousků větší otázky, jak je můžeme spojit dohromady, abychom poskytli plynulé prostředí uživatelského rozhraní, které se snadno implementuje a udržuje?

V tomto článku se budeme podělit co jsme se naučili z našich dřívějších pokusů o vytvoření architektury MVVM a ukážeme jednoduchý příklad, jak spojit všechny tyto kousky skládačky.

Stručný popis MVVM

Abychom vám porozuměli, co úložiště bude řešit, ponořme se nejprve do základního vzoru MVVM.

Základní Vzor MVVM
Základní diagram MVVM

Pohled

Toto je znázornění grafického uživatelského rozhraní pro tvůj kód. Ty mohou být reprezentovány XML (soubory rozložení) nebo kódem (Jetpack Compose). Obvykle existuje nějaká forma datové vazby, která propojuje View s ViewModel. V závislosti na tom, kde nakreslíte čáru mezi pohledem a pořadačem, lze objekt Activity a Fragment považovat za jeden nebo oba.

The ViewModel

Toto má na starosti transformaci dat z model do formátu, který dává smysl zobrazení zobrazit. Nenechte se zmást tím dvojitým spojením se šipkou mezi View a ViewModel. Hlavní rozdíl mezi ViewModel a Presenter v MVP je ViewModel, který neobsahuje odkaz na pohled. Jedná se o jednosměrný vztah, což znamená, že ke správě objektu ViewModel je třeba něco jiného.

Model

Týká se to dat (nebo doménového modelu), které se používají jako zdroj informací, které ViewModel poskytuje zobrazení. V této vrstvě se věci trochu rozmazávají, jak odkazují některé články to jako samotné údaje, zatímco ostatní to označují jako vrstvu pro přístup k datům. To je místo, kde postupuje vzor úložiště Google, aby věci uklidil.

Zadejte úložiště

Google byl zmiňuji tento vzor již nějakou dobu . Jejich příklady jsou skvělým vodítkem k pochopení základního principu používání MVVM s úložištěm, ale zjistíme, že jim chybí některé malé ( a důležité) pokyny, které lidem pomohou převést tyto úryvky do většího a složitějšího projektu.

Diagram MVVM Google

Vzor úložiště je navržen tak, aby„ poskytoval čisté rozhraní API, že zbytek aplikace může tato data snadno načíst. “ Pouhé přidání úložiště do vaší architektury bohužel nevynucuje, aby byl váš kód čistý. Stále můžete vytvářet zamotaný nepořádek, aniž byste řádně oddělili a poskytli jasný kontrakt mezi vrstvami.

V DraftKings jsme se zaměřili na několik dalších pokynů, které našim vývojářům pomohou důsledně vytvářet čistý kód.

Oddělit vrstvy pomocí rozhraní

Zde přidáváme do rozhraní, abychom inženýry přiměli přemýšlet o tom, co veřejně odhalují
Vylepšení diagramu Google přidáním rozhraní

Zjistili jsme, že používání rozhraní mezi těmito vrstvami pomůže technikům na všech úrovních dodržovat zásady dobrého kódování. To pomáhá zajistit, aby naše testy jednotek skutečně testovaly pouze jednu vrstvu najednou, což snižuje režii psaní a udržuje velkou sadu testů. Pomáhá také jasně definovat externí rozhraní API a zamlžuje podrobnosti implementace různých vrstev. Vyzývá nás k vyhodnocení toho, co říkáme vnějšímu světu o funkčnosti tohoto objektu, a poskytuje nám vestavěnou příležitost zajistit, aby byl náš kód čistý.

Poskytuje nám také možnost efektivněji refaktorovat různé vrstvy v naší kódové základně. Dokud se rozhraní nezmění, můžeme nechat oblasti naší kódové základny, které je používají, nedotčené. Například pokud bychom chtěli migrovat naši síťovou knihovnu z Volley do Retrofit, mohli bychom jednoduše změnit metody v našich třídách Dagger, které vytvářet a poskytovat rozhraní nad vzdáleným zdrojem dat a nemusí provádět změny v každém úložišti, které tento koncový bod používá. To výrazně snižuje rozsah těchto změn a snižuje šanci, že v konečném produktu zavedeme chyby.

Zde máme příklad toho, jak nechat ViewModel držet se konkrétní třídy úložiště může vést k nechtěnému chování. Příklad je trochu vymyšlený a lze jej napravit jednoduchým označením fooPublishSubject in FooRepository as private, ale toto řešení je křehčí. FooRepository může je třeba použít v jiném rozsahu vyžadujícím přístup k tomuto parametru a otevření přístupu pro instance nyní muddles wh en je vhodné tuto členskou proměnnou použít přímo.

Poskytování těchto závislostí

Jak roste složitost vašeho projektu, tím složitější jsou vaše vztahy závislostí. To znamená, že se lidé obvykle obracejí k nějaké knihovně Dependency Injection (jako Dagger nebo Koin ) .

Knihovna DI poskytuje nejen čistý a snadný způsob získání požadovaných závislostí, ale také vám umožňuje přemýšlet o tom, kolik z těchto objektů budete v aplikaci potřebovat.

Tento myšlenkový proces nás vedl k zavedení nejlepší praxe toho, jaké objekty patří do Daggerova grafu. Cokoli, od čeho chceme jen jednu instanci, by mělo žít v kořenovém / globálním / grafu s rozsahem aplikací. Cokoli, čeho by mohlo být mnoho, by mělo být vytvořeno na vyžádání a vhodně přidrženo.

To znamenalo, že naše nové objekty úložiště patří do grafu Dagger, protože chceme, aby více modelů ViewModels mělo přístup k modelům prostřednictvím jedné instance podkladových zdrojů Room nebo Retrofit. ViewModels na druhé straně je třeba vytvořit nový pro každý View, který je potřebuje. Představte si hromadu aktivit jako hromadu hracích karet a model ViewModel pohání barvu a hodnotu. Nechtěli bychom, aby akt přidání 3 klubů na začátek změnil také všechny níže uvedené karty na 3 kluby, takže každý View potřebuje vlastní instanci ViewModel, aby uchoval a izoloval svá data.

Nyní jsme definovali, co bude naše DI řešení nyní obsahovat.
Ukažte, jaké objekty budou pravděpodobně drženy Dagger Graph

Rozhodli jsme se, že nebudeme držet naše ViewModels z našeho Daggerova grafu. Historicky jsme byli ohledně této volby méně explicitní, ale cítili jsme, že toto je správný směr vzhledem k vzoru ViewModelProvider , který přichází v androidx.lifecycle a jak nám pomáhá upevnit vztah mezi aktivitou / fragmentem / XML a ViewModel jako „jeden na jednoho“. Mezitím může být vztah ViewModel k úložišti „mnoho k mnoha“. V praxi to znamená, že pro každou aktivitu / fragment / XML máme jeden ViewModel, který zpracovává tento pohled ogic, ale může oslovit mnoho úložišť a získat požadovaná data. Protože se data obecně znovu používají a zobrazují v celé aplikaci, mnoho různých ViewModels může snadno a efektivně používat stejnou instanci úložiště z Dagger Graph.

Obsahující API

V jakékoli společnosti v měřítku trvá mnoho inženýrů, týmů a dokonce i divizí, aby se projekt dostal z tabule do rukou zákazníka. Tady na DraftKings se to nijak neliší. Chcete-li získat data do aplikace pro Android, musíme pracovat s několika různými týmy na back-endu, abychom získali data z databáze do rozhraní API pro klienta Android. Vzhledem k tomu, že tento kód často vlastní jiný tým, znamená to, že se back-end obecně „pohybuje“ jiným tempem než klient.

To platí zejména na začátku projektu, který mít API ve stavu, který můžeme použít pro náš vývoj. Chtěli bychom dělat designová a implementační rozhodnutí o datových objektech, které se interně předávají klientovi, aniž bychom se příliš starali o konfliktní rozhodnutí, která dělají inženýři pracující na backendu.

Kromě toho máme několik služeb, které klientovi vracejí stejná obchodní data, ale protože jsou vlastněny různými týmy a snaží se řešit různé problémy, od sebe se oddělily ve struktuře a typech dat vrácených prostřednictvím API.Interní pro klienta ve skutečnosti představují totéž, takže schopnost překládat a kombinovat tyto odpovědi do něčeho univerzálního pro klienta má velký smysl.

Zde vidíte, že v rozhraní API existují dva koncové body, které vracejí varianty „uživatele“; koncové body přihlášení a profil. Chcete-li je sloučit do jednoho datového typu, jednoduše vytvoříme konstruktor pro každou variantu, kterou chceme zpracovat, a aplikace může nyní omezit znalosti dvou různých typů uživatelů, které rozhraní API doručuje na jedno místo. Díky tomu je sdílení dat (prostřednictvím modelové vrstvy) mezi funkcemi mnohem jednodušší a zároveň umožňuje omezit změny v datové struktuře a typech API na jeden koncový bod.

Děláme rozdíl mezi objekty Network Response a objekty obchodního modelu v naší architektuře. To také pomáhá definovat roli vzdáleného zdroje dat, aby převzal síťové odpovědi a transformoval je do obchodních modelů pro zbytek naší aplikace.

Definování datových typů, které tyto vrstvy vytvářejí, také pomáhá definovat roli vzdáleného zdroje dat
Upřesnění typu dat odesílaných mezi vrstvami, což umožňuje další opakované použití

Příklad v kódu

Nyní se ponoříme do příkladu Google UserRepository a vytvoříme vlastní verzi držíme se změn, které jsme provedli výše.

Nejprve se podívejme na finální verzi UserRepository společnosti Google.

Vidíte, že používají Dagger (nebo Hilt ) , Kotlin Coroutines , Room, as největší pravděpodobností Retrofit. Poskytování služby Retrofit, prováděcího programu Coroutine a Dao (Data Access Object) do úložiště.

Základním tokem je vytvořit síťový požadavek na data a vrátit data (nebo něco, co sleduje) pro data) z místnosti okamžitě. Jakmile je požadavek na síť dokončen, proveďte vše, co musíte s daty udělat, a vložte nový objekt do tabulky. Vložení automaticky upozorní dříve vrácená data, že se změnilo, vyzve k novému načtení a nakonec aktualizaci zobrazení.

Některá nastavení

Než se dostaneme k vytvoření UserRepository, měli bychom se nejprve zabývat některými věcmi, které budeme potřebovat, například užitečným způsobem, jak vložit, na která vlákna chceme běžet.

Pomůže nám to s testováním později. Nastavte to v grafu Dagger a nyní můžete snadno vkládat správná vlákna do celé kódové základny a přitom je stále možné vyměnit za TestScheduler ve vaší jednotce testy (píšete jednotkové testy … správně?)

Zde jsou naše uživatelské třídy, UserResponse jsou ty, které naše API vrátilo prostřednictvím Retrofit a User, náš obchodní model, který interně předáváme. Vidíte, že můžeme síťový objekt zjednodušit a dokonce k jeho vytvoření použít generátor kódu, zatímco naše obchodní objekty mohou být více v souladu s tím, co potřebujeme.

Zde definujeme naši službu Retrofit. Mohli jsme mít tyto vrácené Observable místo Single, což by logiku downstream trochu zjednodušilo, ale líbily se nám paralely jak fungují síťové požadavky a Single, asynchronní a buď úspěšné, nebo neúspěšné. Tuto logiku přenášíme i přes vrstvu vzdáleného zdroje dat.

Další na řadě je náš pokoj Dao. Vzhledem k tomu, že Room již pracuje s rozhraními a anotacemi, necítili jsme potřebu vytvořit další rozhraní, třídu a objekt, abychom zamaskovali, jak zacházíme s trvalými daty.

Pomocí Observable reagujeme na emise User objektů a naše akce vložení vrátí Completable, který nám pomůže vyřešit případné chyby místnosti.

Konečně je zde naše poslední rozhraní pro samotný UserRepository. Je to velmi jednoduché a čisté. Jedinou další částí nad rámec toho, co je požadováno, je metoda onCleared(), která nám pomůže vyčistit všechny existující jednorázové položky v našich spodních vrstvách jako ViewModel bude také vymazán.

Implementace

Všimnete si, že podpis konstruktoru je velmi podobný výše uvedenému příkladu Google.Poskytujeme objekt, který může načítat data po síti, Dao a objekt, který nám říká, na kterých vláknech se má běžet.

Dokonce i metoda getUser(userId: Int) je podobný v tom, jak to funguje ve výše uvedeném příkladu. Vytvořte asynchronní síťový požadavek a okamžitě vraťte objekt, který sleduje data z databáze. Po dokončení tohoto síťového požadavku vložte tato data do tabulky, což spustí aktualizaci uživatelského rozhraní.

Zjistíte, že zde s RxJava neděláme nic moc fantastického. Vytvoříme nový PublishSubject, kterým procházíme naše data. Sloučíme to s výběrem trvalé vrstvy, který vrací Observable a také tam posíláme chyby ze síťové vrstvy. Šli jsme tam a zpět, jak zvládnout získávání pouze chyb, které chceme, z každé vrstvy do vrstvy výše, a toto bylo nejjednodušší a nejvíce přizpůsobitelné řešení, ke kterému jsme přišli. Ano, vytváří další pozorovatelné objekty, ale poskytuje nám jemnější kontrolu nad zpracováním chyb.

Poslední součástí skládačky je vzdálený zdroj dat. Zde vidíte, jak převádíme UserResponse na User, což zbytku kódové základny umožňuje pohybovat se jiným tempem než data přicházející z API.

Zabalení všeho

V tomto okamžiku byste měli přidat tyto objekty shora do Daggerova grafu. Nezapomeňte, že poskytnuté metody by měly vrátit typ rozhraní, nikoli implementace. Tímto způsobem můžete vytvořit každý objekt definovaný výše v Daggerově grafu a díky tomu je velmi snadné získat data z UserRepository kdekoli v základně kódu.

Nakonec, protože používáme RxJava, měli bychom zavolat clear() na libovolné Disposable objekty v našich ViewModel a také zavolejte onCleared() na naši UserRepository instance k vyčištění všech interních Disposable objektů.

Nyní máme poměrně přímočarou implementaci MVVM s úložištěm, které nám poskytujeme prostřednictvím náš Daggerův graf. Jednotlivé vrstvy, které jsme vytvořili, mají jasný účel a jsou drženy rozhraními, která nám pomáhají filtrovat to, co vystavujeme vnějšímu světu. To nám také umožňuje testovat tyto vrstvy nezávisle na sobě odděleně pomocí RxJava TestScheduler, který nám umožňuje mít úplnou kontrolu nad provedením testu.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *