Android, MVVM și depozite în lumea reală

(Mike DeMaso) (2 noiembrie 2020)

Cum obțineți o interfață de utilizator receptivă, păstrând în același timp codul curat, precis, lizibil și menținut? Aceasta este o întrebare care revine în creierul dezvoltatorilor de Android din 2008.

După mulți ani, lăsând dezvoltatorii Android să descopere singuri, Google a oferit recent câteva îndrumări cu privire la subiect cu Ghid pentru arhitectura aplicațiilor care promovează o variantă de MVVM . Deși acesta este un început minunat, lasă o mulțime de întrebări persistente pentru ca echipele să răspundă singure. Google oferă, de asemenea, biblioteci precum LiveData , cameră și DataBinding pentru a ne ajuta să reducem cazanele și să separăm preocupările codului nostru. Chiar și terții au dat o mână de ajutor cu RxJava și Retrofit , ceea ce ne oferă o ușurință modalitate de a gestiona munca asincronă și de a prelua lucruri prin rețea. Cu toate aceste lucruri care plutesc în jurul rezolvării unor bucăți mici din întrebarea mai mare, cum le putem reuni pentru a oferi o experiență UI fluidă, care este simplu de implementat și întreținut?

În acest articol, vom împărtăși ceea ce am învățat din încercările noastre anterioare de a crea o arhitectură MVVM și de a arăta un exemplu simplu de cum să reunim toate aceste piese ale puzzle-ului.

O descriere rapidă a MVVM

Pentru a vă oferi o înțelegere a ceea ce va rezolva un depozit, să ne scufundăm mai întâi în modelul MVVM de bază.

Elementul de bază Model MVVM
Diagrama de bază MVVM

Vizualizarea

Aceasta este reprezentarea interfeței grafice a utilizatorului pentru Codul tau. Acestea pot fi reprezentate prin XML (fișiere de aspect) sau cod (Jetpack Compose). De obicei, există o formă de legare a datelor care leagă Vizualizarea de ViewModel. În funcție de locul în care trasați linia dintre o vizualizare și un liant, obiectul Activitate și Fragment poate fi considerat unul sau ambele.

ViewModel

Acesta este responsabil de transformarea datelor din Modelul într-un format care are sens pentru afișarea Vizualizării. Nu lăsați să vă păcălească conexiunea dublă cu săgeată dintre View și ViewModel. Diferența majoră dintre un ViewModel și prezentator în MVP este că ViewModel nu conține o referință la vizualizare. Este o relație unidirecțională, ceea ce înseamnă că altcineva trebuie să gestioneze obiectul ViewModel.

Model

Aceasta se referă la datele (sau modelul de domeniu) care sunt utilizate ca sursă de informații pe care ViewModel le furnizează View. Acest strat este locul în care lucrurile devin puțin neclare, așa cum se referă la unele articole este ca datele în sine, în timp ce alții se referă la acesta ca un strat de acces la date. Aici intervine modelul depozitului Google pentru a clarifica lucrurile.

Introduceți depozitul

Google a fost menționând acest model de ceva timp acum. Exemplele lor sunt un ghid excelent pentru înțelegerea principiului de bază al utilizării MVVM cu un depozit, dar găsim că le lipsesc unele mici ( și important) îndrumare pentru a ajuta oamenii să traducă aceste fragmente într-un proiect mai mare și mai complex.

Diagrama MVVM a Google

Modelul depozitului este conceput pentru a„ oferi un API curat, astfel încât că restul aplicației poate prelua cu ușurință aceste date. ” Din păcate, doar adăugarea unui depozit la arhitectura dvs. nu forțează codul să fie curat. Puteți crea în continuare o mizerie încurcată fără a separa în mod corespunzător și a furniza un contract clar între straturi.

La DraftKings ne-am concentrat pe câteva îndrumări suplimentare pentru a ajuta dezvoltatorii noștri să producă cod curat în mod consecvent.

Decuplați straturile cu interfețe

Aici adăugăm în interfețe pentru a determina inginerii să se gândească la ceea ce expun public
Îmbunătățirea diagramei Google prin adăugarea de interfețe

Am stabilit că utilizarea interfețelor dintre aceste straturi va ajuta inginerii de toate nivelurile să respecte principiile bune de codificare. Acest lucru ajută la asigurarea faptului că testele noastre unitare testează cu adevărat doar un strat la un moment dat, reducând cheltuielile de scriere și menținând o suită de testare mare. De asemenea, ajută la definirea clară a API-urilor externe și ofensează detaliile de implementare ale diferitelor straturi. Ne îndeamnă să evaluăm ceea ce le spunem lumii exterioare despre funcționalitatea acestui obiect și ne oferă posibilitatea integrată de a ne asigura că codul nostru este curat.

Ne oferă, de asemenea, capacitatea de a refactora diferite straturi din baza noastră de cod mai eficient. Atâta timp cât interfața nu se schimbă, putem lăsa neatinse zonele bazei noastre de cod care le folosesc. De exemplu, dacă am dori să migram biblioteca noastră de rețea de la Volley la Retrofit, am putea schimba pur și simplu metodele din clasele noastre Dagger care produceți și furnizați interfața deasupra sursei de date la distanță și nu trebuie să faceți modificări în fiecare depozit care utilizează acel punct final. Acest lucru reduce considerabil sfera acestor modificări și scade șansa de a introduce bug-uri în produsul final.

Aici avem un exemplu în care lăsarea unui ViewModel să se țină de clasa concretă a unui depozit poate duce la comportamente neintenționate. Exemplul este puțin inventat și poate fi corectat prin simpla marcare a fooPublishSubject în FooRepository ca private, dar acea soluție este mai fragilă. FooRepository trebuie să fie utilizate într-un alt domeniu de aplicare care necesită acces la acel parametru și deschiderea accesului pentru instanțele care acum confundă wh ro este adecvat să utilizați direct variabila respectivă.

Livrarea acestor dependențe

Pe măsură ce complexitatea proiectului crește, cu atât relațiile de dependență devin mai complicate. Aceasta înseamnă că, în general, oamenii apelează la un fel de bibliotecă de injectare a dependenței (cum ar fi Dagger sau Koin ) .

Nu numai că o bibliotecă DI oferă o modalitate simplă și simplă de a vă recupera dependențele necesare, ci vă permite să vă gândiți la câte dintre aceste obiecte veți avea nevoie în aplicație.

Acest proces de gândire ne-a determinat să stabilim cea mai bună practică a obiectelor care aparțin graficului Dagger. Orice lucru pe care îl dorim doar o singură instanță, ar trebui să trăiască în graficul rădăcină / global / aplicație. Orice lucru din care ar putea exista numeroase instanțe ar trebui creat la cerere și păstrat în mod corespunzător.

Acest lucru a însemnat că noile noastre obiecte din depozit aparțin graficului Dagger, deoarece dorim ca mai multe modele ViewModels să poată accesa modele. printr-o singură instanță a sursei subiacente Room sau Retrofit. ViewModels, pe de altă parte, trebuie să fie create noi pentru fiecare View care are nevoie de ele. Gândiți-vă la un teanc de activități, cum ar fi un teanc de cărți de joc, iar ViewModel conduce costumul și valoarea. Nu ne-am dori ca actul de a adăuga un 3 de cluburi în partea de sus pentru a schimba toate cărțile de mai jos într-un 3 de cluburi, de aceea fiecare View are nevoie de propria instanță a unui ViewModel pentru a-și păstra și izola datele.

Am definit acum ce va rezolva soluția noastră DI.
Arată ce obiecte se așteaptă să dețină graficul pumnal

Am decis să ne menținem ViewModels-ul în afara graficului nostru pumnal. Din punct de vedere istoric, am fost mai puțin expliciți despre această alegere, dar am considerat că acesta este direcția corectă având în vedere modelul ViewModelProvider care vine în androidx.lifecycle și modul în care ne ajută să solidificăm relația dintre activitate / fragment / XML și ViewModel ca „unu la unu”. Între timp, relația ViewModel la depozit poate fi „de la mulți la mulți”. În practică, acest lucru înseamnă că pentru fiecare activitate / fragment / XML avem un singur ViewModel care gestionează ogic, dar poate ajunge la multe depozite pentru a obține datele necesare. Deoarece datele sunt, în general, reutilizate și afișate în întreaga aplicație, multe ViewModels diferite pot utiliza cu ușurință și eficient aceeași instanță a depozitului din Dagger Graph.

Conținând API

În orice companie la scară, este nevoie de mulți ingineri, echipe și chiar divizii pentru a obține un proiect de pe tablă albă în mâinile clientului. Aici, la DraftKings, nu este diferit. Pentru a obține date în aplicația Android, trebuie să lucrăm cu câteva echipe diferite pe backend pentru a obține datele din baza de date către API către clientul Android. Având în vedere că acest cod este adesea deținut de o altă echipă, înseamnă că backend-ul se va „muta” în general cu un ritm diferit de cel al clientului.

Acest lucru este valabil mai ales la începutul unui proiect care nu avem un API într-o stare pe care o putem folosi pentru dezvoltarea noastră. Am dori să luăm decizii de proiectare și implementare cu privire la obiectele de date care sunt transmise intern clientului fără a ne îngrijora prea mult de deciziile conflictuale pe care le iau inginerii care lucrează în backend.

Dincolo de asta, avem un puține servicii care returnează aceleași date de afaceri clientului, dar pentru că sunt deținute de echipe diferite și încearcă să rezolve diferite probleme, s-au îndepărtat unul de celălalt în structura și tipurile de date returnate prin intermediul API-urilor.Interne pentru client, acestea reprezintă de fapt același lucru, astfel încât posibilitatea de a traduce și combina aceste răspunsuri în ceva universal pentru client are mult sens.

Aici puteți vedea că există două puncte finale pe API care returnează variații ale unui „utilizator;” punctele finale de autentificare și profil. Pentru a le îmbina într-un singur tip de date, pur și simplu creăm un constructor pentru fiecare variantă pe care dorim să o gestionăm și acum aplicația poate limita cunoașterea celor două tipuri diferite de utilizatori pe care API-ul le oferă într-un singur loc. Acest lucru face ca partajarea datelor (prin stratul Model) între funcții să fie mult mai ușoară, permițând totuși limitarea modificărilor în structura și tipurile de date API la un singur punct final. și obiecte Business Model din arhitectura noastră. Acest lucru ajută, de asemenea, la definirea rolului sursei de date la distanță pentru a lua răspunsuri la rețea și a le transforma în modele de afaceri pentru restul aplicației noastre.

Definirea tipurilor de date pe care le produc aceste straturi, aceasta ajută și la definirea rolului sursei de date la distanță
Clarificarea tipului de date trimise între straturi, permițând o mai mare reutilizare

Un exemplu în cod

Acum vom intra în exemplul Google UserRepository și vom crea propria noastră versiune ținând cont de modificările pe care le-am făcut mai sus.

În primul rând, să aruncăm o privire la versiunea finală Google a UserRepository.

Puteți vedea că utilizează Dagger (sau Hilt ) , Kotlin Coroutines , cameră și, cel mai probabil, Retrofit. Furnizarea serviciului Retrofit, executant Coroutine și Dao (Data Access Object) către depozit.

Fluxul de bază al acestui lucru este de a face o cerere de rețea pentru date și de a returna datele (sau ceva care urmărește) pentru date) din cameră imediat. După finalizarea cererii de rețea, faceți tot ce trebuie să faceți cu datele și introduceți noul obiect în tabel. Inserarea notifică automat datele returnate anterior că s-au modificat, solicitând o nouă recuperare și, în cele din urmă, o actualizare a vizualizării.

Unele configurări

Înainte de a începe să creăm UserRepository, ar trebui să abordăm mai întâi câteva lucruri de care vom avea nevoie, cum ar fi un mod util de a injecta pe ce fire dorim să rulăm.

Acest lucru ne ajută la testarea ulterioară. Configurați acest lucru în graficul Pumnal și acum puteți injecta cu ușurință firele corecte pe întreaga bază de cod, în timp ce le puteți schimba cu un TestScheduler în unitatea dvs. teste (scrieți teste unitare … nu?)

Iată clasele noastre de utilizatori, UserResponse fiind cea returnată de API-ul nostru prin Retrofit și , modelul nostru de afaceri îl prezentăm intern. Puteți vedea că putem face obiectul de rețea simplu și chiar să folosim un generator de cod pentru a-l crea, în timp ce obiectele noastre comerciale pot fi mai în concordanță cu ceea ce avem nevoie.

Aici ne definim serviciul Retrofit. Am fi putut avea aceste returnări Observable în loc de Single, ceea ce ar fi făcut logica din aval un pic mai simplă, dar ne-au plăcut paralelele modul în care funcționează solicitările de rețea și Single, atât asincrone, fie reușite sau nereușite. Transportăm această logică și prin stratul Sursă de date la distanță.

Urmează camera noastră Dao. Deoarece Room funcționează deja de la interfețe și adnotări, nu am simțit nevoia să creăm o altă interfață, clasă și obiect pentru a ofensa modul în care gestionăm datele persistente.

Folosim un Observable pentru a reacționa la emisiile de obiecte User dacă acțiunea noastră de inserare returnează un Completable pentru a ne ajuta să rezolvăm orice erori de cameră care ar putea apărea.

În cele din urmă, iată ultima noastră interfață pentru UserRepository în sine. Este foarte simplu și curat. Singura parte suplimentară dincolo de ceea ce este necesar este metoda onCleared() care ne va ajuta să clarificăm orice produse de unică folosință existente în straturile noastre inferioare ca este șters și ea.

Implementări

Veți observa că semnătura constructorului este foarte asemănătoare cu exemplul Google de mai sus.Oferim un obiect care poate prelua date prin rețea, un Dao și un obiect care ne spune pe ce fire trebuie să rulăm.

Chiar și metoda getUser(userId: Int) este similar în modul în care funcționează în exemplul de mai sus. Creați o cerere de rețea asincronă și returnați imediat un obiect care urmărește datele din baza de date. Când acea solicitare de rețea se finalizează, introduceți aceste date în tabel, ceea ce va declanșa o actualizare a interfeței de utilizare.

Veți observa că nu facem nimic prea elegant cu RxJava aici. Creăm un nou PublishSubject prin care ne canalizăm datele. Îl îmbinăm cu selectarea stratului persistent, care returnează un Observable și, de asemenea, trimitem erori și din stratul de rețea. Am mers înainte și înapoi cu privire la modul de gestionare a obținerii numai a erorilor pe care le dorim de la fiecare strat la stratul de mai sus și aceasta a fost cea mai simplă și mai personalizabilă soluție la care am ajuns. Da, creează observabile suplimentare, dar ne oferă un control mai bun asupra gestionării erorilor.

Ultimul piesa puzzle-ului este Sursa de date la distanță. Aici puteți vedea cum convertim UserResponse într-un User, permițând restului bazei de cod să se deplaseze într-un ritm diferit față de datele care provin din API.

Înfășurând totul

În acest moment, ar trebui să adăugați aceste obiecte de sus la graficul Dagger. Nu uitați că metodele furnizate ar trebui să returneze tipul de interfață și nu implementările. În acest fel, puteți construi fiecare obiect definit mai sus în graficul Dagger, ceea ce face foarte ușor să obțineți date de la un UserRepository oriunde în baza de cod.

În cele din urmă, deoarece utilizăm RxJava, ar trebui să ne asigurăm că apelăm clear() Disposable obiectele din ViewModel și apelăm și onCleared() pe UserRepository instanță pentru a curăța orice Disposable obiecte interne.

Acum avem o implementare MVVM destul de simplă cu un depozit care ne este furnizat prin graficul nostru Dagger. Straturile pe care le-am creat fiecare au un scop clar și sunt ținute de interfețe care ne ajută să filtrăm ceea ce expunem lumii exterioare. Acest lucru ne permite, de asemenea, să testăm aceste straturi independent unul de celălalt izolat folosind un RxJava TestScheduler care ne permite să avem un control complet asupra execuției testului.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *