Concetti e implementazione di Jetpack Compose
I componenti che tengono conto del ciclo di vita eseguono azioni in risposta a una modifica dello stato del ciclo di vita di un altro componente, ad esempio attività e fragment. Questi componenti ti aiutano a produrre codice più organizzato e spesso più leggero, più facile da gestire.
Un pattern comune consiste nell'implementare le azioni dei componenti dipendenti nei metodi del ciclo di vita di attività e fragment. Tuttavia, questo pattern porta a una scarsa organizzazione del codice e alla proliferazione di errori. Utilizzando i componenti che tengono conto del ciclo di vita, puoi spostare il codice dei componenti dipendenti dai metodi del ciclo di vita ai componenti stessi.
Il pacchetto androidx.lifecycle fornisce classi e interfacce che consentono
di creare componenti che tengono conto del ciclo di vita, ovvero componenti che possono
regolare automaticamente il loro comportamento in base allo stato del ciclo di vita corrente di un'
attività o di un fragment.
La maggior parte dei componenti dell'app definiti in Android Framework hanno cicli di vita associati. I cicli di vita vengono gestiti dal sistema operativo o dal codice del framework in esecuzione nel tuo processo. Sono fondamentali per il funzionamento di Android e la tua applicazione deve rispettarli. In caso contrario, potrebbero verificarsi perdite di memoria o persino arresti anomali dell'applicazione.
Supponiamo di avere un'attività che mostra la posizione del dispositivo sullo schermo. Un'implementazione comune potrebbe essere la seguente:
Kotlin
internal class MyLocationListener(
private val context: Context,
private val callback: (Location) -> Unit
) {
fun start() {
// connect to system location service
}
fun stop() {
// disconnect from system location service
}
}
class MyActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener
override fun onCreate(...) {
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
}
public override fun onStart() {
super.onStart()
myLocationListener.start()
// manage other components that need to respond
// to the activity lifecycle
}
public override fun onStop() {
super.onStop()
myLocationListener.stop()
// manage other components that need to respond
// to the activity lifecycle
}
}
Java
class MyLocationListener {
public MyLocationListener(Context context, Callback callback) {
// ...
}
void start() {
// connect to system location service
}
void stop() {
// disconnect from system location service
}
}
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
@Override
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, (location) -> {
// update UI
});
}
@Override
public void onStart() {
super.onStart();
myLocationListener.start();
// manage other components that need to respond
// to the activity lifecycle
}
@Override
public void onStop() {
super.onStop();
myLocationListener.stop();
// manage other components that need to respond
// to the activity lifecycle
}
}
Anche se questo esempio sembra corretto, in un'app reale finisci per avere troppe chiamate che gestiscono l'interfaccia utente e altri componenti in risposta allo stato attuale del ciclo di vita. La gestione di più componenti inserisce una quantità considerevole di
codice nei metodi del ciclo di vita, come onStart() e onStop, il che
rende difficile la manutenzione.
Inoltre, non è garantito che il componente venga avviato prima dell'arresto dell'attività o del fragment. Ciò è particolarmente vero se dobbiamo eseguire un'operazione a
lunga esecuzione, ad esempio un controllo della configurazione in onStart. Ciò può causare una race condition in cui il onStop() metodo termina prima del onStart, mantenendo il componente attivo più a lungo del necessario.
Kotlin
class MyActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener
override fun onCreate(...) {
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
}
public override fun onStart() {
super.onStart()
Util.checkUserStatus { result ->
// what if this callback is invoked AFTER activity is stopped?
if (result) {
myLocationListener.start()
}
}
}
public override fun onStop() {
super.onStop()
myLocationListener.stop()
}
}
Java
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, location -> {
// update UI
});
}
@Override
public void onStart() {
super.onStart();
Util.checkUserStatus(result -> {
// what if this callback is invoked AFTER activity is stopped?
if (result) {
myLocationListener.start();
}
});
}
@Override
public void onStop() {
super.onStop();
myLocationListener.stop();
}
}
Il pacchetto androidx.lifecycle fornisce classi e interfacce che ti aiutano
ad affrontare questi problemi in modo resiliente e isolato.
Lifecycle
Lifecycle è una classe che contiene le informazioni sullo stato del ciclo di vita di un componente (ad esempio un'attività o un fragment) e consente ad altri oggetti di osservare questo stato.
Lifecycle utilizza due enumerazioni principali per monitorare lo stato del ciclo di vita del componente associato:
Event
Gli eventi del ciclo di vita inviati dal framework e dalla
Lifecycle classe. Questi eventi vengono mappati agli eventi di callback in attività e fragment.
State
Lo stato attuale del componente monitorato dall'oggetto Lifecycle.
Considera gli stati come nodi di un grafico e gli eventi come i bordi tra questi nodi.
Una classe può monitorare lo stato del ciclo di vita del componente implementando
DefaultLifecycleObserver e sostituendo i metodi corrispondenti, ad esempio
onCreate, onStart, ecc. Poi puoi aggiungere un osservatore chiamando il metodo
addObserver() della classe Lifecycle e passando un'
istanza dell'osservatore, come mostrato nell'esempio seguente:
Kotlin
class MyObserver : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
connect()
}
override fun onPause(owner: LifecycleOwner) {
disconnect()
}
}
myLifecycleOwner.getLifecycle().addObserver(MyObserver())
Java
public class MyObserver implements DefaultLifecycleObserver {
@Override
public void onResume(LifecycleOwner owner) {
connect()
}
@Override
public void onPause(LifecycleOwner owner) {
disconnect()
}
}
myLifecycleOwner.getLifecycle().addObserver(new MyObserver());
Nell'esempio precedente, l'oggetto myLifecycleOwner implementa l'
LifecycleOwner interfaccia, descritta nella sezione seguente.
LifecycleOwner
LifecycleOwner è un'interfaccia a metodo singolo che indica che la classe
ha un Lifecycle. Ha un metodo, getLifecycle, che deve essere
implementato dalla classe. Se invece stai cercando di gestire il ciclo di vita di un intero
processo dell'applicazione, consulta ProcessLifecycleOwner.
Questa interfaccia astrae la proprietà di un Lifecycle dalle singole
classi, come Fragment e AppCompatActivity, e consente di
scrivere componenti che funzionano con queste classi. Qualsiasi classe di applicazione personalizzata può
implementare l'interfaccia LifecycleOwner.
I componenti che implementano DefaultLifecycleObserver funzionano perfettamente con
i componenti che implementano LifecycleOwner perché un proprietario può fornire un
ciclo di vita a cui un osservatore può registrarsi per monitorarlo.
Per l'esempio di monitoraggio della posizione, possiamo fare in modo che la MyLocationListener classe
implementi DefaultLifecycleObserver e poi inizializzarla con il
suo Lifecycle nel metodo onCreate(). In questo modo, la classe MyLocationListener è autosufficiente, il che significa che la logica per reagire alle modifiche dello stato del ciclo di vita viene dichiarata in MyLocationListener anziché nell'attività. Se i singoli componenti memorizzano la propria logica, la logica di attività e fragment è più facile da gestire.
Kotlin
class MyActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener
override fun onCreate(...) {
myLocationListener = MyLocationListener(this, lifecycle) { location ->
// update UI
}
Util.checkUserStatus { result ->
if (result) {
myLocationListener.enable()
}
}
}
}
Java
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
// update UI
});
Util.checkUserStatus(result -> {
if (result) {
myLocationListener.enable();
}
});
}
}
Un caso d'uso comune consiste nell'evitare di richiamare determinati callback se Lifecycle
non è in uno stato ottimale al momento. Ad esempio, se il callback esegue una transazione di fragment dopo il salvataggio dello stato dell'attività, si verifica un arresto anomalo, quindi non vogliamo mai richiamare il callback.
Per semplificare questo caso d'uso, la Lifecycle classe consente ad altri oggetti di
eseguire query sullo stato attuale.
Kotlin
internal class MyLocationListener(
private val context: Context,
private val lifecycle: Lifecycle,
private val callback: (Location) -> Unit
): DefaultLifecycleObserver {
private var enabled = false
override fun onStart(owner: LifecycleOwner) {
if (enabled) {
// connect
}
}
fun enable() {
enabled = true
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
// connect if not connected
}
}
override fun onStop(owner: LifecycleOwner) {
// disconnect if connected
}
}
Java
class MyLocationListener implements DefaultLifecycleObserver {
private boolean enabled = false;
public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
...
}
@Override
public void onStart(LifecycleOwner owner) {
if (enabled) {
// connect
}
}
public void enable() {
enabled = true;
if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
// connect if not connected
}
}
@Override
public void onStop(LifecycleOwner owner) {
// disconnect if connected
}
}
Con questa implementazione, la nostra classe LocationListener è completamente consapevole del ciclo di vita. Se dobbiamo utilizzare LocationListener da un'altra attività o fragment, dobbiamo solo inizializzarlo. Tutte le operazioni di configurazione e pulizia vengono gestite dalla classe stessa.
Se una libreria fornisce classi che devono funzionare con il ciclo di vita di Android, ti consigliamo di utilizzare componenti che tengono conto del ciclo di vita. I client della libreria possono integrare facilmente questi componenti senza la gestione manuale del ciclo di vita sul lato client.
Implementare un LifecycleOwner personalizzato
I fragment e le attività nella Support Library 26.1.0 e versioni successive implementano già
l'interfaccia LifecycleOwner.
Se hai una classe personalizzata che vuoi rendere LifecycleOwner,
puoi utilizzare la classe LifecycleRegistry, ma devi inoltrare gli eventi
a questa classe, come mostrato nell'esempio di codice seguente:
Kotlin
class MyActivity : Activity(), LifecycleOwner {
private lateinit var lifecycleRegistry: LifecycleRegistry
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleRegistry = LifecycleRegistry(this)
lifecycleRegistry.markState(Lifecycle.State.CREATED)
}
public override fun onStart() {
super.onStart()
lifecycleRegistry.markState(Lifecycle.State.STARTED)
}
override fun getLifecycle(): Lifecycle {
return lifecycleRegistry
}
}
Java
public class MyActivity extends Activity implements LifecycleOwner {
private LifecycleRegistry lifecycleRegistry;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
lifecycleRegistry = new LifecycleRegistry(this);
lifecycleRegistry.markState(Lifecycle.State.CREATED);
}
@Override
public void onStart() {
super.onStart();
lifecycleRegistry.markState(Lifecycle.State.STARTED);
}
@NonNull
@Override
public Lifecycle getLifecycle() {
return lifecycleRegistry;
}
}
Best practice per i componenti che tengono conto del ciclo di vita
- Mantieni i controller dell'interfaccia utente (attività e fragment) il più snelli possibile.
Non devono tentare di acquisire i propri dati; utilizza invece un
ViewModelper farlo e osserva unLiveDataoggetto per riflettere le modifiche nelle visualizzazioni. - Prova a scrivere interfacce utente basate sui dati in cui la responsabilità del controller dell'interfaccia utente è
aggiornare le visualizzazioni quando i dati cambiano o inviare le azioni dell'utente a
ViewModel. - Inserisci la logica dei dati nella classe
ViewModel.ViewModeldeve fungere da connettore tra il controller dell'interfaccia utente e il resto dell' app. Fai attenzione, però, non è responsabilità diViewModel's recuperare i dati (ad esempio da una rete). Al contrario,ViewModeldeve chiamare il componente appropriato per recuperare i dati, quindi fornire il risultato al controller dell'interfaccia utente. - Utilizza Data Binding per mantenere un'interfaccia pulita tra le visualizzazioni e il controller dell'interfaccia utente. In questo modo puoi rendere le visualizzazioni più dichiarative e ridurre al minimo il codice di aggiornamento che devi scrivere nelle attività e nei fragment. Se preferisci farlo nel linguaggio di programmazione Java, utilizza una libreria come Butter Knife per evitare il codice boilerplate e avere un' astrazione migliore.
- Se l'interfaccia utente è complessa, valuta la possibilità di creare una classe di presentazione per gestire le modifiche dell'interfaccia utente. Potrebbe essere un'attività laboriosa, ma può semplificare il test dei componenti dell'interfaccia utente.
- Evita di fare riferimento a un
ViewoActivitycontesto inViewModel. SeViewModelsopravvive all'attività (in caso di modifiche alla configurazione), l'attività perde memoria e non viene eliminata correttamente dal Garbage Collector. - Utilizza le coroutine Kotlin per gestire le attività a lunga esecuzione e altre operazioni che possono essere eseguite in modo asincrono.
Casi d'uso per i componenti che tengono conto del ciclo di vita
I componenti che tengono conto del ciclo di vita possono semplificare notevolmente la gestione dei cicli di vita in una serie di casi. Ecco alcuni esempi:
- Passaggio da aggiornamenti della posizione grossolani a quelli precisi. Utilizza i componenti che tengono conto del ciclo di vita per attivare gli aggiornamenti della posizione precisi mentre l'app di localizzazione è visibile e passa agli aggiornamenti grossolani quando l'app è in background.
LiveData, un componente che tiene conto del ciclo di vita, consente all'app di aggiornare automaticamente l'interfaccia utente quando l'utente cambia posizione. - Arresto e avvio del buffering video. Utilizza i componenti che tengono conto del ciclo di vita per avviare il buffering video il prima possibile, ma rimanda la riproduzione fino all'avvio completo dell'app. Puoi anche utilizzare i componenti che tengono conto del ciclo di vita per terminare il buffering quando l'app viene eliminata.
- Avvio e arresto della connettività di rete. Utilizza i componenti che tengono conto del ciclo di vita per attivare l'aggiornamento in tempo reale (streaming) dei dati di rete mentre un'app è in primo piano e per mettere automaticamente in pausa quando l'app passa in background.
- Messa in pausa e ripresa dei drawable animati. Utilizza i componenti che tengono conto del ciclo di vita per gestire la messa in pausa dei drawable animati quando l'app è in background e riprendere i drawable dopo che l'app è in primo piano.
Gestire gli eventi di arresto
Quando un Lifecycle appartiene a un AppCompatActivity o
Fragment, lo stato di Lifecycle's cambia in CREATED e
l'evento ON_STOP viene inviato quando viene chiamato onSaveInstanceState() di AppCompatActivity o
Fragment's.
Quando lo stato di un Fragment o AppCompatActivity's viene salvato utilizzando
onSaveInstanceState, la sua interfaccia utente viene considerata immutabile fino a quando non viene chiamato
ON_START. Se provi a modificare l'interfaccia utente dopo il salvataggio dello stato, è
probabile che si verifichino incoerenze nello stato di navigazione dell'applicazione,
motivo per cui FragmentManager genera un'eccezione se l'app esegue un
FragmentTransaction dopo il salvataggio dello stato. Per maggiori dettagli, consulta commit() per
dettagli.
LiveData impedisce questo caso limite immediatamente evitando di chiamare il suo osservatore se Lifecycle associato all'osservatore non è almeno STARTED. Dietro le quinte, chiama isAtLeast() prima
di decidere di richiamare il suo osservatore.
Purtroppo, il metodo AppCompatActivity's onStop() viene chiamato
dopo onSaveInstanceState, il che lascia un intervallo in cui le modifiche dello stato dell'interfaccia utente
non sono consentite, ma Lifecycle non è ancora passato allo stato
CREATED.
Per evitare questo problema, la classe Lifecycle nella versione beta2 e precedenti
contrassegna lo stato come CREATED senza inviare l'evento in modo che qualsiasi codice
che controlla lo stato attuale ottenga il valore reale anche se l'evento non viene
inviato fino a quando il sistema non chiama onStop().
Purtroppo, questa soluzione presenta due problemi principali:
- A livello API 23 e precedenti, il sistema Android salva effettivamente lo stato di un'attività anche se è parzialmente coperta da un'altra attività. In altre
parole, il sistema Android chiama
onSaveInstanceState(), ma non necessariamenteonStop. In questo modo si crea un intervallo potenzialmente lungo in cui l'osservatore ritiene ancora che il ciclo di vita sia attivo anche se lo stato dell'interfaccia utente non può essere modificato. - Qualsiasi classe che vuole esporre un comportamento simile alla
LiveDataclasse deve implementare la soluzione alternativa fornita daLifecycleversionebeta 2e precedenti.
Risorse aggiuntive
Per saperne di più sulla gestione dei cicli di vita con i componenti che tengono conto del ciclo di vita, consulta le seguenti risorse aggiuntive.
Campioni
- Sunflower, un'app demo che mostra le best practice con i componenti dell'architettura