PATTERN ARCHITETTURALI IN ANDROID: Introduzione
La programmazione per Android è un ambito di lavoro molto vasto che richiede numerose conoscenze. Una app Android non è soltanto una schermata di interazione con l’utente (front-end) ma è anche un’applicazione back-end, che può comprendere anche la gestione di un database, prestando al tempo stesso molta attenzione alle prestazioni e alla reattività, e quindi al lavoro distribuito sui diversi (per forza di cose) thread. Ciò che in una applicazione web a microservizi troviamo diviso su differenti applicazioni, magari anche eseguiti su server diversi, in Android lo troviamo tutto in un un’unica applicazione, eseguita sul palmo della vostra mano. Non è un mondo, ma un cosmo: un microcosmo che contiene galassie di concetti da sapere e conoscere, e nel quale è anche facile perdersi. Ma non temete: noi di AIknow possiamo darvi qualche consiglio per mantenere la rotta della vostra nave spaziale, alla conquista dell’universo Android, iniziando dal primo pianeta: i pattern architetturali in Android, ergo, come impostare l’architettura della vostra applicazione in maniera ordinata ed efficiente.
CONCETTI DI BASE
Per chi non avesse già dimestichezza con Android, presentiamo una rapida panoramica degli elementi di base.
Il punto di partenza di ogni progetto Android è l’Activity.
Per Activity si intende una funzionalità dell’applicazione, composta da
- una parte grafica per interagire con l’utente. Consiste in un file XML che può essere anche “disegnato” in maniera visuale in un’apposita sezione di Android Studio
- una parte logica, ovvero le operazioni da svolgere all’avvio e/o in risposta alle azioni dell’utente. Consiste in una classe Java o Kotlin che estende la classe di Android AppCompatActivity.
Ogni applicazione Android conteine almeno una Activity. Se l’applicazione contiene più activity, una di queste viene etichettata come Activity principale e viene eseguita per prima ad ogni avvio, e da questa si può navigare (tramite pulsanti o altro) alle altre Activity.
Ogni Activity può essere suddivisa in più sotto-elementi, o frazioni della stessa funzionalità, chiamate appunto Fragment. Esattamente come per l’Activity, anche il fragment è composto da un XML e una classe Java o Kotlin.
Ogni Activity o Fragment possiedono un cliclo di vita (in inglese lifecycle), ovvero una serie di metodi che vengono eseguiti in conseguenza a eventi del sistema operativo. Per citarne alcuni:
- onStart(): la activity (o fragment) è stata avviata
- onResume(): la activity (o fragment) è stata “riesumata” dopo uno stato di pausa
- onPause(): la activity (o fragment) è stata messa in pausa (ad esempio perchè è arrivata una telefonata o è stato bloccato lo schermo)
- onDestroy(): la activity (o fragment) è stata distrutta (ad esempio perchè si è passati a una nuova activity oppure è stata chiusa la app)
Per chi si cimenta nello scrivere la prima app Android con Android Studio, dopo aver preso dimestichezza con i concetti di Activity, Fragment e lifecycle, può venire spontaneo aggiungere alla Activity tutto il codice Java (o Kotlin) necessario per recuperare i dati dal database, mostrarli a video, gestire gli eventi, le notifiche, chiamare delle API… Ciò è comprensibile, ma il rischio che si incontra con tale approccio è che le Activity e i Fragment raggiungano dimensioni astronomiche, e contengano svariati compiti anche molto diversi tra loro, per non contare la difficoltà di mantenere ordine in mezzo a tutto questo marasma di codice. Per ovviare a questo problema, o almeno ridurre questi rischi, ci vengono in aiuto i pattern architetturali.
PATTERN ARCHITETTURALI : COSA E PERCHÈ
I design pattern di tipo architetturale (da Wikipedia) esprimono schemi di base per impostare l’organizzazione strutturale di un sistema software.
Un pattern architetturale è quindi un insieme di accorgimenti da seguire per mantenere ben separati gli strati che compongono la nostra app. Non è necessario per il corretto funzionamento della app, tuttavia è uno strumento fondamentale per:
- mantenere ordine in applicazioni di grandi dimensioni: un’applicazione ordinata e pulita è molto più facile da mantenere e da evolvere, e quindi meno costosa
- impostare in maniera corretta l’architettura di una app di piccole dimensioni che potrebbe in futuro essere scalata: usare un patter architetturale permette di scalare agilmente, senza dover affrontare un oneroso refactoring totale.
- testare facilmente singole porzioni di business logic o di UI
Nei paragrafi successivi descriveremo, con esempi pratici, uno dei pattern architetturali in Android piú diffuso, il pattern MVP.
MVP (Model – View – Presenter)
Figlio del ben noto MVC (Model – View – Controller), uno dei pattern architetturali più conosciuti nell’ambito della programmazione a oggetti, MVP è un pattern molto simile utilizzato nella programmazione Android, in cui il Presenter fa le veci del Controller.
L’obbiettivo del MVP (cosi come del MVC) è separare la logica di presentazione dalla logica di business. MVP e MVC differiscono per il modo in cui i tre elementi comunicano tra loro (ne parleremo tra poco). Passiamo in rassegna i tre componenti:
MODEL
Il Model è lo strato che ha il compito di gestire i dati, quindi fornire o aggiornare, a prescindere da quale sia la sorgente (database, preferences, api). Lo strato applicativo che richiede i dati (il Presenter) è agnostico sulla modalità con cui vengono recuperati i dati: è in carico al Model applicare le logiche necessarie per garantire che vengano forniti i dati richiesti. Ad esempio, una app potrebbe avere una classe StarshipsRepository con un metodo getStarships() che restituisce la lista delle navi spaziali applicando la seguente logica:
- se il dispositivo è connesso a internet, chiama una API esterna per ottenere la lista aggiornata delle navi spaziali
- se il dispositivo è offline, mostra le navi spaziali salvate nel database.
Il Model, inoltre, deve essere costituito da classi che non estendono le classi di Android (Activity, Fragment, eccetera), e non deve avere riferimenti diretti con esse. Soltanto le classi del Presenter possono chiamare direttamente le classi del Model per ottenere i dati. In questo, il pattern MVP si differenzia dal pattern MVC, nel quale invece le classi del Model possono interagire direttamente con le classi della View.
@Singleton public class StarshipsRepository { private ConnectivityManager connectivityManager; private EngineManager engineManager; public List getStarships() { if (isNetworkAvailable()) { return this.serverApiManager.getStarships(); } else { return this.databaseManager.getStarships(); } } public void launch() { // to implement: launch a starship } ... }
VIEW
Nei pattern architetturali in Android, una View, al contrario, è lo strato che si occupa di interagire con l’utente. Rientrano quindi nella View tutte le Activity, i Fragment, e più in generale tutte le classi che rappresentano le viste, ovvero gli elementi visuali che mostrano i dati e recepiscono le azioni dell’utente (i tocchi sullo schermo, le gesture, l’utilizzo della fotocamera, eccetera).
Le view devono essere “stupide”, nel senso che non devono contenere la logica di business, ma soltanto gestire l’interazione con l’utente e inoltrare gli input allo strato applicativo più basso (il Presenter). in modo da garantire la separazione dei compiti (nei limiti del possibile, ricordiamo che i pattern devono essere un aiuto, non una limitazione). Lo strato della View ha semplicemente due compiti:
- Recepire le interazioni dell’utente con il dispositivo e inoltrarle al Presenter
- Esporre dei metodi che ricevono in input i dati da visualizzare, e li mostrano all’utente.
Questi metodi dovranno essere utilizzati dal Presenter. La View ha quindi un ruolo particolare: non deve la logica applicativa, ma allo stesso tempo, essendo soggetto al lifecycle, tutto inizia e finisce con essa, quindi tutta l’applicazione dipende da essa. Può fare il bello e il cattivo tempo, e gli altri strati applicativi non possono far altro che adattarsi al meglio.
public class StarshipFragment extends Fragment implements StarshipContract.View { // Il Fragment (la View) ha un riferimento con il proprio Presenter private StarshipContract.Presenter starshipPresenter; private RecyclerView mRecyclerView; protected RecyclerView.LayoutManager mLayoutManager; private FloatingActionButton takeOffButton; public StarshipFragment() { this.starshipPresenter = new StarshipPresenter(this); } public static StarshipFragment newInstance() { return new StarshipFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_starship, container, false); // ************************ // Launch button // ************************ takeOffButton = root.findViewById(R.id.takeOffButton); // Ad ogni click sul pulsante, il Fragment non fa altro che chiamare un metodo del Presenter takeOffButton.setOnClickListener( click -> starshipPresenter.launchStarship()); // ************************ // Starships list // ************************ mRecyclerView = (RecyclerView) root.findViewById(R.id.starshipOverview); mLayoutManager = new LinearLayoutManager(getActivity()); setRecyclerViewLayoutManager(); mRecyclerView.setAdapter(starshipsListAdapter); return root; } @Override public void onResume() { super.onResume(); // Ogni volta che il Fragement si avvia (o si riavvia) si esegue il metodo start() del Presenter // come detto, ogni decisione (comprese le operazioni da fare all’avvio) sono delegate al Presenter starshipPresenter.start(); } // ########################################################### // Override from contract // ########################################################### @Override public void showStarshipsList(List starshipList) { this.starshipsListAdapter.updateList(starshipList); } @Override public void showErrorMessage(int error) { snackbar.setText(getString(error)); snackbar.show(); } ... }
PRESENTER
Il Presenter è il detentore della business logic. Lo dobbiamo immaginare come il capitano di una nave (spaziale): assegna i compiti ai vari membri dell’equipaggio e è responsabile di prendere le decisioni che riguardano il destino della nave. Interagisce sia con il Model, sia con la View:
- Dalla View riceve gli input dell’utente e “decide” cosa fare con questi input, applicando la vera e propria logica applicativa.
- Dal Model decide quali dati recuperare, e li passa alla View per essere mostrati all’utente.
Il Presenter, inoltre, ha il compito importantissimo di orchestrare i thread correttamente. Senza entrare troppo nel dettaglio, deve evitare che il thread principale (dedicato all’interfaccia utente) venga impiegato in operazioni gravose dal punto di vista computazionale, che potrebbero portare a un calo della reattività (o addirittura a una interruzione della app, l’incubo di ogni programmatore Android). Per fare ciò, assegna al thread principale soltanto le operazioni minime e indispensabili al funzionamento della UI, mentre tutte le altre operazioni (operazioni di lettura/scrittura, chiamate a API esterne, ecc.) le assegna a thread specifici.
Il Presenter, non essendo un’estensione di una classe di Android, e non avendo quindi alcun lifecycle, dipende dalla View. La sua stessa esistenza dipende dalla View. Il Presenter quindi può essere considerato come uno strumento, a disposizione di ogni View, per sottrarre a essa tutta la business logic, per alleggerirla e separare i compiti.
public class StarshipPresenter implements StarshipContract.Presenter { private StarshipsRepository starshipsRepository; private EngineManager engineManager; private final StarshipContract.View mView; public Mag5Presenter(StarshipContract.View mView) { this.mView = mView; } // ########################################################### // Override from contract // ########################################################### @Override public void start() { GetMag5TransportItemsList_CB cb = new GetMag5TransportItemsList_CB(); Disposable d = this.starshipsRepository.getStarships() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( result -> mView.showStarshipsList(result), error -> mView.showErrorMessage(R.string.error_fetching_item_mag5) ); addDisposable(d); } @Override public void launchStarship() { if (this.engineManager.isEngineReady()) this.starshipsRepository.launchStarship(); } ... }
LE INTERFACCE CONTRACT
Contract è un elemento del pattern MVP che serve principalmente per mantenere ordine nel progetto, e per garantire che venga rispettata una delle “regole” del pattern:
Ad ogni classe della View deve corrispondere una classe del Presenter, con una corrispondenza 1:1
L’idea è la seguente: per ogni classe della View deve esistere una e una sola classe del Presenter (e viceversa), e le due sono destinate a lavorare insieme, stipulando una sorta di contratto: come se fosse un team in cui il Presenter è la mente, colui che decide, la View è il braccio, colui che esegue, ma i due sono complementari e lavorano in squadra per implementare la stessa funzionalità.
I Contract sono interfacce che servono proprio per sancire questo legame tra ogni View e il proprio Presenter. Contengono a loro volta due interfacce, una per la View e una per il Presenter, che contengono a loro volta le firme dei metodi corrispondenti. In questo modo, osservando una classe Contract, è possibile vedere a colpo d’occhio cosa è stato implementato in una coppia di View/Presenter, come se fossero una cosa sola.
public interface StarshipContract { interface View { void showErrorMessage(int messageCode); void showStarshipsList(List starshipList); } interface Presenter { void start(); void launch(); } }
CONCLUSIONE
I pattern architetturali in android sono uno strumento fondamentale per rendere la vostra app Android ordinata, manutenibile e scalabile.
In questo articolo abbiamo esplorato con qualche esempio il pattern architetturale MVP (Model View Presenter). E non dimenticate di guardare le altre news del nostro blog o di consultare i nostri use cases per capire che tipo di lavori abbiamo eseguito in AIknow!
Hai bisogno di una app per digitalizzare un processo? Vuoi saperne di più?