Guida per sviluppatori Android al modello di navigazione dei frammenti
Pubblicato: 2022-03-11Nel corso degli anni, ho visto diverse implementazioni di pattern di navigazione in Android. Alcune app utilizzavano solo attività, mentre altre attività mescolate con frammenti e/o con visualizzazioni personalizzate.
Una delle mie implementazioni preferite del pattern di navigazione si basa sulla filosofia "One-Activity-Multiple-Fragments", o semplicemente il Fragment Navigation Pattern, in cui ogni schermata dell'applicazione è un frammento a schermo intero e tutti o la maggior parte di questi frammenti sono contenuti in un'attività.
Questo approccio non solo semplifica il modo in cui viene implementata la navigazione, ma ha prestazioni molto migliori e di conseguenza offre una migliore esperienza utente.
In questo articolo esamineremo alcune implementazioni di modelli di navigazione comuni in Android, quindi introdurremo il modello di navigazione basato su frammenti, confrontandolo e contrastando con gli altri. Un'applicazione demo che implementa questo modello è stata caricata su GitHub.
Mondo delle attività
Una tipica applicazione Android che utilizza solo attività è organizzata in una struttura ad albero (più precisamente in un grafico orientato) in cui l'attività di root viene avviata dal launcher. Durante la navigazione nell'applicazione è presente un back stack di attività gestito dal sistema operativo.
Un semplice esempio è mostrato nel diagramma seguente:
L'attività A1 è il punto di ingresso nella nostra applicazione (ad esempio, rappresenta una schermata iniziale o un menu principale) e da essa l'utente può passare ad A2 o A3. Quando è necessario comunicare tra le attività, è possibile utilizzare startActivityForResult() o magari condividere tra di esse un oggetto di logica aziendale accessibile a livello globale.
Quando è necessario aggiungere una nuova attività, è necessario eseguire i seguenti passaggi:
- Definisci la nuova attività
- Registralo in AndroidManifest.xml
- Aprilo con startActivity() da un'altra attività
Naturalmente questo diagramma di navigazione è un approccio abbastanza semplicistico. Può diventare molto complesso quando devi manipolare il back stack o quando devi riutilizzare la stessa attività più volte, ad esempio quando vorresti far navigare l'utente attraverso alcune schermate di tutorial ma ogni schermata utilizza in realtà la stessa attività di un base.
Fortunatamente abbiamo strumenti per questo chiamati attività e alcune linee guida per una corretta navigazione nel back stack.
Poi, con il livello API 11 sono arrivati i frammenti...
Mondo dei frammenti
Android ha introdotto frammenti in Android 3.0 (livello API 11), principalmente per supportare progetti di interfaccia utente più dinamici e flessibili su schermi di grandi dimensioni, come i tablet. Poiché lo schermo di un tablet è molto più grande di quello di un telefono, c'è più spazio per combinare e scambiare i componenti dell'interfaccia utente. I frammenti consentono tali progetti senza la necessità di gestire modifiche complesse alla gerarchia delle viste. Dividendo il layout di un'attività in frammenti, puoi modificare l'aspetto dell'attività in fase di esecuzione e conservare tali modifiche in un back stack gestito dall'attività. – citato dalla guida API di Google per i frammenti.
Questo nuovo giocattolo ha consentito agli sviluppatori di creare un'interfaccia utente a più riquadri e di riutilizzare i componenti in altre attività. Alcuni sviluppatori lo adorano mentre altri no. È un dibattito popolare se usare o meno i frammenti, ma penso che tutti sarebbero d'accordo sul fatto che i frammenti hanno portato ulteriore complessità e gli sviluppatori hanno davvero bisogno di capirli per usarli correttamente.
Frammento da incubo a schermo intero su Android
Ho iniziato a vedere sempre più esempi in cui i frammenti non rappresentavano solo una parte dello schermo, ma in realtà l'intero schermo era un frammento contenuto in un'attività. Una volta ho persino visto un progetto in cui ogni attività aveva esattamente un frammento a schermo intero e nient'altro e l'unico motivo per cui queste attività esistevano era ospitare questi frammenti. Accanto all'ovvio difetto di progettazione, c'è un altro problema con questo approccio. Dai un'occhiata allo schema qui sotto:
Come può A1 comunicare con F1? Bene, A1 ha il controllo totale sulla F1 da quando ha creato la F1. A1 può passare un bundle, ad esempio, alla creazione di F1 o può invocare i suoi metodi pubblici. Come può F1 comunicare con A1? Bene, questo è più complicato, ma può essere risolto con un modello di richiamata/osservatore in cui A1 si iscrive a F1 e F1 notifica A1.
Ma come possono comunicare tra loro A1 e A2? Questo è già stato trattato, ad esempio tramite startActivityForResult() .
E ora arriva la vera domanda: come possono F1 e F2 comunicare tra loro? Anche in questo caso possiamo avere un componente di business logic che è disponibile a livello globale, quindi può essere utilizzato per passare i dati. Ma questo non porta sempre a un design elegante. E se F2 avesse bisogno di passare alcuni dati a F1 in un modo più diretto? Bene, con un modello di richiamata F2 può notificare A2, quindi A2 termina con un risultato e questo risultato viene catturato da A1 che notifica F1.
Questo approccio richiede molto codice standard e diventa rapidamente fonte di bug, dolore e rabbia.
E se potessimo sbarazzarci di tutte le attività e mantenerne solo una che conserva il resto dei frammenti?
Schema di navigazione del frammento
Nel corso degli anni ho iniziato a utilizzare il pattern "One-Activity-Multiple-Fragments" nella maggior parte delle mie applicazioni e lo uso ancora. Ci sono molte discussioni là fuori su questo approccio, per esempio qui e qui. Quello che mi è mancato però è un esempio concreto che posso vedere e testare io stesso.
Diamo un'occhiata al diagramma seguente:
Ora abbiamo solo un'attività contenitore e abbiamo più frammenti che hanno di nuovo una struttura ad albero. La navigazione tra di loro è gestita dal FragmentManager , ha il suo back stack.
Si noti che ora non abbiamo startActivityForResult() ma possiamo implementare un modello di callback/osservatore. Vediamo alcuni pro e contro di questo approccio:
Professionisti:
1. AndroidManifest.xml più pulito e manutenibile
Ora che abbiamo una sola attività, non abbiamo più bisogno di aggiornare il manifest ogni volta che aggiungiamo una nuova schermata. A differenza delle attività, non dobbiamo dichiarare frammenti.
Potrebbe sembrare una cosa da poco, ma per applicazioni più grandi che hanno più di 50 attività questo può migliorare significativamente la leggibilità del file AndroidManifest.xml .
Guarda il file manifest dell'applicazione di esempio che ha diverse schermate. Il file manifest rimane ancora super semplice.

<?xml version="1.0" encoding="utf-8"?> package="com.exarlabs.android.fragmentnavigationdemo.ui" > <application android:name= ".FragmentNavigationDemoApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.exarlabs.android.fragmentnavigationdemo.ui.MainActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:theme="@style/AppTheme.NoActionBar" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
2. Gestione centralizzata della navigazione
Nel mio esempio di codice, vedrai che utilizzo NavigationManager che nel mio caso viene iniettato in ogni frammento. Questo gestore può essere utilizzato come luogo centralizzato per la registrazione, la gestione del back stack e così via, in modo che i comportamenti di navigazione siano disaccoppiati dal resto della logica aziendale e non diffusi in implementazioni di schermate diverse.
Immaginiamo una situazione in cui vorremmo avviare una schermata in cui l'utente può selezionare alcuni elementi da un elenco di persone. Vorresti anche passare alcuni argomenti di filtro come età, occupazione e sesso.
In caso di attività, scriveresti:
Intent intent = new Intent(); intent.putExtra("age", 40); intent.putExtra("occupation", "developer"); intent.putExtra("gender", "female"); startActivityForResult(intent, 100);
Quindi devi definire onActivityResult da qualche parte sotto e gestire il risultato.
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); }
Il mio problema personale con questo approccio è che questi argomenti sono "extra" e non sono obbligatori, quindi devo assicurarmi che l'attività di ricezione gestisca tutti i diversi casi in cui manca un extra. Successivamente, quando viene effettuato un refactoring e l'extra "età" ad esempio non è più necessario, devo cercare ovunque nel codice in cui inizio questa attività e assicurarsi che tutti gli extra siano corretti.
Inoltre, non sarebbe meglio se il risultato (elenco di persone) arrivasse sotto forma di _Lista
In caso di navigazione basata su frammenti, tutto è più semplice. Tutto quello che devi fare è scrivere un metodo nel NavigationManager chiamato startPersonSelectorFragment() con gli argomenti necessari e con un'implementazione di callback.
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", new PersonSelectorFragment.OnPersonSelectedListener() { @Override public boolean onPersonsSelected(List<Person> selection) { [do something] return false; } });
O con RetroLambda
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", selection -> [do something]);
3. Migliori mezzi di comunicazione tra gli schermi
Tra le attività, possiamo condividere solo un Bundle che può contenere dati primitivi o serializzati. Ora con i frammenti possiamo implementare un modello di callback in cui, ad esempio, F1 può ascoltare F2 che passa oggetti arbitrari. Dai un'occhiata all'implementazione del callback degli esempi precedenti, che restituisce un _List
4. I frammenti di costruzione sono meno costosi delle attività di costruzione
Questo diventa ovvio quando si utilizza un cassetto che ha ad esempio 5 voci di menu e in ogni pagina il cassetto dovrebbe essere visualizzato di nuovo.
In caso di pura attività di navigazione, ogni pagina dovrebbe gonfiare e inizializzare il drawer, cosa ovviamente costosa.
Sul diagramma sottostante puoi vedere diversi frammenti di radice (FR*) che sono i frammenti a schermo intero a cui è possibile accedere direttamente dal cassetto, e inoltre il cassetto è accessibile solo quando questi frammenti sono visualizzati. Tutto ciò che è a destra della linea tratteggiata nel diagramma è presente come esempio di uno schema di navigazione arbitrario.
Poiché l'attività del contenitore contiene il cassetto, abbiamo solo un'istanza del cassetto, quindi ad ogni passaggio di navigazione in cui il cassetto dovrebbe essere visibile non è necessario gonfiarlo e inizializzarlo nuovamente. Non sei ancora convinto di come funzionano tutti questi elementi? Dai un'occhiata alla mia applicazione di esempio che dimostra l'utilizzo del cassetto.
contro
La mia più grande paura era sempre stata che se avessi usato un modello di navigazione basato su frammenti in un progetto, da qualche parte lungo la strada avrei incontrato un problema imprevisto che sarebbe stato difficile da risolvere a causa della maggiore complessità dei frammenti, delle librerie di terze parti e delle diverse versioni del sistema operativo. E se dovessi rifattorizzare tutto ciò che ho fatto finora?
In effetti, ho dovuto risolvere problemi con frammenti nidificati, librerie di terze parti che utilizzano anche frammenti come ShinobiControls, ViewPagers e FragmentStatePagerAdapters.
Devo ammettere che acquisire sufficiente esperienza con i frammenti per poter risolvere questi problemi è stato un processo piuttosto lungo. Ma in ogni caso il problema non era che la filosofia fosse cattiva, ma che non capivo abbastanza bene i frammenti. Forse se capisci i frammenti meglio di me non incontreresti nemmeno questi problemi.
L'unico neo che posso menzionare ora è che possiamo ancora incontrare problemi che non sarebbero banali da risolvere poiché non esiste una libreria matura che mostri tutti gli scenari complessi di un'applicazione complessa con navigazione basata su frammenti.
Conclusione
In questo articolo abbiamo visto un modo alternativo per implementare la navigazione in un'applicazione Android. L'abbiamo confrontata con la filosofia di navigazione tradizionale che utilizza le attività e abbiamo visto alcuni buoni motivi per cui è vantaggioso utilizzarla rispetto all'approccio tradizionale.
Se non l'hai già fatto, controlla l'applicazione demo caricata su GitHub implementando. Sentiti libero di biforcare o contribuire con esempi più belli che ne mostrerebbero meglio l'utilizzo.