Android, MVVM és a való világ adattárai

(Mike DeMaso) (2020. november 2.)

Hogyan érhető el egy érzékeny felhasználói felület, miközben a kódja tiszta, pontos, olvasható és karbantartható? Ez egy olyan kérdés, amely 2008 óta ugrál az Android fejlesztők agyán.

Miután sok éven át hagyta, hogy az Android fejlesztők önállóan találják ki, a Google nemrégiben adott útmutatást a témához a Útmutató az alkalmazás architektúrájához , amely az MVVM változatát népszerűsíti. Bár ez egy remek kezdet, sok kérdés maradt a csapatok számára, hogy önállóan válaszolhassanak. A Google olyan könyvtárakat is kínál, mint a LiveData , Room és DataBinding a kazánlemezek csökkentésében és a kóddal kapcsolatos aggályok elkülönítésében. Még harmadik felek is segítő kezet nyújtottak a RxJava és a utólagos felszereléssel , ami megkönnyíti számunkra az aszinkron munka kezelésének és a hálózaton keresztüli lehívásának módja. Mindezen dolgok lebegve a nagyobb kérdés apró darabjainak megoldása során, hogyan hozhatnánk össze mindet, hogy könnyen kezelhető felhasználói felületet biztosítsunk, amelyet egyszerűen lehet megvalósítani és fenntartani?

Ebben a cikkben megosztjuk amit az MVVM architektúra létrehozásának korábbi kísérleteiből tanultunk, és mutatunk be egy egyszerű példát arra, hogyan hozhatjuk össze a puzzle ezen darabjait.

Az MVVM rövid leírása

Annak megértése érdekében, hogy egy adattár mit fog megoldani, először merüljünk el az alap MVVM mintában.

Az alap MVVM minta
Alapvető MVVM diagram

A nézet

Ez a grafikus felhasználói felület ábrázolása a kódod. Ezeket XML (elrendezési fájlok) vagy kód (Jetpack Compose) képviselheti. Általában van valamilyen adatkötés, amely összeköti a Nézetet a ViewModellel. Attól függően, hogy hol húzza a határt a nézet és az iratgyűjtő között, az Activity és a Fragment objektum egyikének vagy mindkettőnek tekinthető.

A ViewModel

Ez az adatok átalakítása a a modellt olyan formátumba, amely értelmes a Nézet megjelenítéséhez. Ne hagyja, hogy az a kettős nyílvégű kapcsolat a Nézet és a ViewModel között becsapjon. A ViewModel és az MVP bemutatója közötti legfőbb különbség az, hogy a ViewModel nem tartalmaz hivatkozást a nézetre. Ez egyirányú kapcsolat, vagyis másoknak kell kezelniük a ViewModel objektumot.

A Modell

Ez azokra az adatokra (vagy tartománymodellekre) vonatkozik, amelyeket a ViewModel által a Nézethez szolgáltatott információk forrásaként használnak. Ebben a rétegben a dolgok kissé elmosódnak, ahogyan néhány cikk hivatkozik rá maga az adat, míg mások adatelérési rétegként hivatkoznak rá. Itt lép be a Google adattárának mintája a dolgok tisztázására.

Adja meg a tárat

A Google már ezt a mintát már egy ideje megemlíti . Példáik nagyszerű útmutatást nyújtanak az MVVM lerakattal történő használatának alapelvének megértéséhez, de úgy találjuk, hogy hiányzik belőlük néhány kicsi ( és fontos) útmutatás, amely segíti az embereket, hogy ezeket a részleteket nagyobb, összetettebb projektbe fordítsák.

A Google MVVM diagramja

A lerakat mintája úgy van kialakítva, hogy„ tiszta API-t biztosítson, így hogy az alkalmazás többi része könnyen megszerezheti ezeket az adatokat. ” Sajnos a lerakó hozzáadása az architektúrához nem kényszeríti a kódot tisztára. Még mindig létrehozhat egy kusza rendetlenséget anélkül, hogy megfelelően elkülönítené és egyértelmű szerződést biztosítana a rétegek között.

A DraftKingsnél néhány további irányelvre összpontosítottunk, amelyek segítenek fejlesztőinknek a tiszta kódok következetes előállításában.

Rétegek leválasztása interfészekkel

Itt hozzáadjuk az Interfészeket, hogy arra ösztönözzük a mérnököket, hogy gondolják át, mit tesznek nyilvánosan nyilvánosságra > Javítás a Google diagramján az interfészek hozzáadásával

Megállapítottuk, hogy a rétegek közötti interfészek használata minden szinten segít a mérnököknek a jó kódolási elvek betartásában. Ez segít abban, hogy egységtesztjeink valóban csak egy réteget teszteljenek egyszerre, ezzel csökkentve az írás költségeit és fenntartva egy nagy tesztkészletet. Emellett segít egyértelműen meghatározni a külső szembenéző API-kat, és elhomályosítja a különböző rétegek megvalósítási részleteit. Arra készteti, hogy értékeljük, mit mondunk a külvilágnak az objektum funkcionalitásáról, és beépített lehetőséget biztosít számunkra, hogy kódunk tiszta legyen.

Ez lehetővé teszi számunkra a kódbázisunk különböző rétegeinek hatékonyabb átalakítását is. Mindaddig, amíg az interfész nem változik, érintetlenül hagyhatjuk kódbázisunk azon területeit, amelyek ezeket használják. Például, ha hálózati könyvtárunkat át szeretnénk költöztetni a Volley-ról az Retrofit-re, egyszerűen megváltoztathatnánk a Dagger osztályok azon módszereit, amelyek hozza létre és adja meg a távoli adatforrás feletti interfészt, és nem kell minden változtatást végrehajtania minden olyan tárban, amely ezt a végpontot használja. Ez jelentősen csökkenti az ilyen változtatások körét és csökkenti annak esélyét, hogy hibákat vinnénk be a végtermékbe. p> Itt van egy példa arra, hogy a ViewModel ragaszkodása a lerakat konkrét osztályához nem kívánt viselkedéshez vezethet. A példa kissé kiagyalt és kijavítható a fooPublishSubject itt: FooRepository mint private, de ez a megoldás törékenyebb. FooRepository egy másik körben kell használni, amelyhez hozzáférés szükséges ehhez a paraméterhez, és hozzáférést kell nyitni az olyan esetekhez, amelyek most már zavarosak hu célszerű ezt a tagváltozót közvetlenül használni.

Ezeknek a függőségeknek a kézbesítése

Amint a projekt bonyolultsága növekszik, annál bonyolultabbá válnak a függőségi kapcsolatai. Ez azt jelenti, hogy az emberek általában valamilyen Dependency Injection könyvtárhoz fordulnak (például Tőr vagy Koin ) .

A DI könyvtár nemcsak tiszta és egyszerű módszert kínál a szükséges függőségek lekérésére, hanem arra is gondol, hogy ezekre az objektumokra hány kell az alkalmazásban.

Ez a gondolkodási folyamat arra késztetett bennünket, hogy megalapozzuk a Tőr-grafikonba tartozó objektumok legjobb gyakorlatát. Bármi, amire csak egyetlen példányt szeretnénk, a root / global / application-sced gráfon kell, hogy éljen. Bármit, aminek sok előfordulása lehet, igény szerint létre kell hozni, és megfelelően be kell tartani.

Ez azt jelentette, hogy új adattár objektumaink a Tőr grafikonba tartoznak, mivel azt szeretnénk, ha több ViewModel is hozzáférhet a modellekhez az alapul szolgáló Room vagy Retrofit források egyetlen példányán keresztül. A ViewModeleket viszont minden egyes nézethez újnak kell létrehozni, amelyre szükségük van. Gondoljon egy halom tevékenységre, például egy halom kártyára, és a ViewModel vezérli az öltönyt és az értéket. Nem szeretnénk, ha egy 3 klubot felvennénk a tetejére, és az összes alábbi kártyát 3 klubra is cserélnénk, ezért minden nézetnek saját ViewModel példányra van szüksége az adatok megőrzéséhez és elkülönítéséhez.

Meghatároztuk, hogy mit fog tartani a DI megoldásunk.
Mutassa meg, hogy mely objektumok várhatóan lesznek birtokában a tőr grafikon

Úgy döntöttünk, hogy a ViewModeleinket kizárjuk a Tőr grafikonunkból. Történelmileg kevésbé voltunk egyértelműek ebben a választásban, de úgy éreztük, hogy ez az helyes irány, ha a ViewModelProvider mintát látjuk, amely a androidx.lifecycle formátumban található, és hogyan segít megszilárdítani a tevékenység / töredék közötti kapcsolatot / XML és a ViewModel „egytől egyig”. Eközben a ViewModel-tároló kapcsolat lehet „sok a sokhoz”. A gyakorlatban ez azt jelenti, hogy minden tevékenység / töredék / XML esetében egyetlen ViewModel van, amely kezeli a nézet l ogic, de sok adattárhoz fordulhat a szükséges adatok megszerzéséhez. Mivel az adatokat általában újrafelhasználják és az alkalmazásban megjelenítik, sok különböző ViewModel könnyen és hatékonyan használhatja ugyanazt a lerakatot a Tőrdiagramból.

Az API-t tartalmazza

Bármely vállalatnál nagyságrendileg sok mérnöknek, csapatnak, sőt divíziónak kell eljutnia ahhoz, hogy a projekt a táblától az ügyfél kezébe kerüljön. Itt a DraftKingsnél ez sincs másként. Ahhoz, hogy adatokat szerezzünk az Android alkalmazásba, együtt kell dolgoznunk néhány különböző háttérrel rendelkező csapattal, hogy az adatokat az adatbázisból az API-hoz eljuttassuk az Android klienshez. Tekintettel arra, hogy ez a kód gyakran egy másik csapat tulajdonában van, ez azt jelenti, hogy a háttérrendszer általában más ütemben “mozog”, mint az ügyfél.

Ez különösen igaz egy olyan projekt elején, amely nem API-ja van olyan állapotban, amelyet felhasználhatunk a fejlesztéshez. Szeretnénk meghozni tervezési és megvalósítási döntéseket az adatobjektumok belső továbbításáról az ügyfél számára anélkül, hogy túl sokat aggódnánk az ellentmondó döntések miatt, amelyeket a háttérrendszeren dolgozó mérnökök hoznak meg.

Ezen felül van egy kevés olyan szolgáltatás, amely ugyanazokat az üzleti adatokat adja vissza az ügyfélnek, de mivel különböző csapatok tulajdonában vannak, és különböző problémákat próbálnak megoldani, az API-k révén visszaküldött adatok felépítésében és típusaiban egymástól eltávolodtak.A kliens belsejében ezek valójában ugyanazt jelentik, ezért nagyon értelmes az, hogy ezeket a válaszokat lefordíthatjuk és egyesíthetjük az ügyfél által univerzális dologgá.

Itt láthatja, hogy az API-n két végpont található, amelyek egy „felhasználó” változatait adják vissza; a Bejelentkezés és a Profil végpontok. Ahhoz, hogy egy adattípusba egyesítsük őket, egyszerűen létrehozunk egy konstruktort minden kezelni kívánt változathoz, és most az alkalmazás korlátozhatja a két különböző típusú felhasználó ismeretét, amelyeket az API egyetlen helyre szállít. Ez jelentősen megkönnyíti az adatok megosztását (a Model rétegen keresztül) a szolgáltatások között, ugyanakkor lehetővé teszi az API adatstruktúrájának és típusainak változásainak korlátozását egy végpontra.

Különbséget teszünk a Network Response objektumok között. és üzleti modell objektumok az építészetünkben. Ez segít meghatározni a távoli adatforrás szerepét is, hogy megkapja a hálózati válaszokat és üzleti modellekké alakítsa alkalmazásunk többi részében.

Az ezen rétegek által előállított adattípusok meghatározása segít a távoli adatforrás szerepének meghatározásában is
A rétegek között küldött adatok típusának tisztázása, további újrafelhasználás lehetővé tétele

Példa a kódba

Most elmélyülünk a Google UserRepository példában, és létrehozzuk a saját verziónkat ragaszkodva a fent végrehajtott változásokhoz.

Először vessünk egy pillantást a Google UserRepository változatának végső változatára.

Láthatja, hogy Tőrt (vagy Hilt ) használnak , Kotlin Coroutines , Szoba, és nagy valószínűséggel Retrofit. Az Retrofit szolgáltatás, a Coroutine végrehajtó és a Dao (Data Access Object) biztosítása az adattárnak.

Ennek alapvető folyamata az, hogy hálózati kérelmet kell benyújtani az adatokhoz, és vissza kell adni az adatokat (vagy valamit, ami figyel adatokért) azonnal a Roomból. A hálózati kérelem teljesítése után tegyen meg mindent az adatokkal kapcsolatban, és helyezze be az új objektumot a táblázatba. A beillesztés automatikusan értesíti a korábban visszaküldött adatokat a változásról, új lekérdezést és végül a nézet frissítését kéri.

Néhány beállítás

Mielőtt hozzákezdenénk a UserRepository, először meg kell vizsgálnunk néhány olyan dolgot, amelyre szükségünk lesz, például egy hasznos módszert, amellyel befecskendezhetjük, mely szálakat szeretnénk futtatni.

Ez segít nekünk a későbbi tesztelésben. Állítsa be ezt a Tőr grafikonon, és most könnyedén befecskendezheti a megfelelő szálakat a teljes kódalapra, miközben továbbra is kicserélheti őket egy TestScheduler eszközre. tesztek (egység teszteket írsz… ugye?)

Itt vannak felhasználói osztályaink: UserResponse az, amelyet az API-nk az Retrofit-en keresztül adott vissza, és User, üzleti modellünket, amelyet belsőleg átadunk. Láthatja, hogy egyszerűvé tehetjük a hálózati objektumot, és akár egy kódgenerátort is használhatunk a létrehozásához, miközben üzleti objektumaink jobban megfelelnek a szükségünknek.

Itt definiáljuk az utólagos felszerelés szolgáltatásunkat. Lehet, hogy ezek Observable -et adnának vissza egy Single helyett, ami kissé egyszerűbbé tette volna a downstream logikát, de tetszettek a párhuzamok hogyan működnek a hálózati kérelmek és a Single aszinkron és sikeres vagy sikertelen. Ezt a logikát továbbítjuk a Távoli adatforrás rétegen keresztül is.

A következő a szobánk Dao. Mivel a Room már működik az interfészektől és a kommentároktól, nem éreztük szükségét egy újabb felület, osztály és objektum létrehozására, hogy elhomályosítsuk a tartós adatok kezelését.

Observable segítségével User objektumok kibocsátására reagálunk, és ha a beszúrási műveletünk egy Completable -t ad vissza, hogy segítsen kezelni az esetleges helyiséghibákat.

Végül itt van az utolsó felületünk magához a UserRepository -hez. Ez nagyon egyszerű és tiszta. A szükségeseken kívül az egyetlen további rész a onCleared() módszer, amely segít megtisztítani az alsóbb rétegeinkben lévő meglévő eldobható tárgyakat ViewModel is törlődik.

Megvalósítások

Észre fogja venni, hogy a kivitelező aláírása nagyon hasonlít a Google fenti példájára.Olyan objektumot biztosítunk, amely adatokat tud lekérni a hálózaton keresztül, egy Dao-t, és egy olyan objektumot, amely megmondja, milyen szálakon futtasson.

Még a getUser(userId: Int) módszer is hasonló a működésében a fenti példában. Hozzon létre egy aszinkron hálózati kérést, és azonnal küldjön vissza egy objektumot, amely az adatbázisból nézi az adatokat. Amikor a hálózati kérelem befejeződött, illessze be az adatokat a táblába, ami egy felhasználói felület frissítését indítja el.

Észre fogja venni, hogy itt nem csinálunk semmi túl divatosat az RxJava-val. Létrehozunk egy új PublishSubject elemet, amelyen keresztül adatainkat továbbterjesztjük. Összevonjuk a perzisztens réteg választásával, amely egy Observable értéket ad vissza, és ott is küldünk hibákat a hálózati rétegből. Előre-hátra kerestük, hogyan kell kezelni az egyes rétegektől a fenti rétegekig csak a kívánt hibákat, és ez volt a legegyszerűbb és leginkább testreszabható megoldás. Igen, extra megfigyelhetőségeket hoz létre, de finomabb ellenőrzést biztosít számunkra a hibakezelés felett.

Az utolsó a puzzle darabja a Távoli adatforrás. Itt láthatja, hogyan konvertáljuk a UserResponse -t User -re, lehetővé téve a kódbázis többi részének más ütemben történő mozgását, mint a az API-ból beérkező adatok.

Az összes összecsomagolása

Ezen a ponton ezeket az objektumokat felülről kell hozzáadni a Tőr grafikonhoz. Ne feledje, hogy a megadott módszereknek az interfész típust kell adniuk, és nem a megvalósításokat. Így minden, a Tőr grafikonon fent definiált objektumot felépíthet, és ez nagyon megkönnyíti az adatok megszerzését a UserRepository ből a kódbázis bárhonnan.

Végül, mivel az RxJavát használjuk, mindenképpen meg kell hívnunk a clear() -t bármelyikre Disposable objektumok a ViewModel objektumokban, és hívja a onCleared() -t a UserRepository példány a belső Disposable objektumok megtisztítására.

Most egy meglehetősen egyszerű MVVM megvalósítással rendelkezünk egy tárral, amelyet a a Tőr grafikonunk. Az általunk létrehozott rétegek mindegyike világos célt szolgál, és olyan interfészek tartják őket, amelyek segítenek kiszűrni azt, amit a külvilág elé tárunk. Ez azt is lehetővé teszi számunkra, hogy ezeket a rétegeket egymástól függetlenül, elszigetelten teszteljük, egy RxJava TestScheduler segítségével, amely lehetővé teszi számunkra, hogy teljes mértékben ellenőrizhessük a teszt végrehajtását.

Vélemény, hozzászólás?

Az email címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük