Gestione dei cicli di vita con componenti consapevoli del ciclo di vita   Componente di Android Jetpack.

I componenti consapevoli 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 frammenti. Questi componenti ti aiutano a produrre codice più organizzato e spesso più leggero, che è più facile da gestire.

Un pattern comune è implementare le azioni dei componenti dipendenti nei metodi di ciclo di vita di attività e frammenti. Tuttavia, questo modello porta a una cattiva organizzazione del codice e alla proliferazione di errori. Utilizzando i componenti sensibili al ciclo di vita, puoi spostare il codice dei componenti dipendenti dai metodi del ciclo di vita nei componenti stessi.

Il pacchetto androidx.lifecycle fornisce classi e interfacce che ti consentono di creare componenti consapevoli del ciclo di vita, ovvero componenti che possono regolare automaticamente il loro comportamento in base allo stato corrente del ciclo di vita di un'attività o di un frammento.

La maggior parte dei componenti dell'app definiti nel framework Android ha associati dei cicli di vita. I cicli di vita sono gestiti dal sistema operativo o dal codice del framework in esecuzione nel processo. Sono fondamentali per il funzionamento di Android e la tua applicazione deve rispettarle. In caso contrario, potrebbero verificarsi perdite di memoria o persino arresti anomali dell'applicazione.

Immaginiamo 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 corrente 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 li rende difficili da gestire.

Inoltre, non è garantito che il componente venga avviato prima dell'interruzione dell'attività o del frammento. Ciò è particolarmente vero se dobbiamo eseguire un'operazione di lunga durata, ad esempio un controllo di configurazione in onStart(). Ciò può causare una condizione di gara in cui il metodo onStop() termina prima di 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.

Ciclo di vita

Lifecycle è una classe che contiene le informazioni sullo stato del ciclo di vita di un componente (ad esempio un'attività o un frammento) 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:

Evento
Gli eventi di ciclo di vita inviati dal framework e dalla classe Lifecycle. Questi eventi vengono mappati agli eventi di callback in attività e frammenti.
Stato
Lo stato attuale del componente monitorato dall'oggetto Lifecycle.
Diagramma degli stati del ciclo di vita
Figura 1. Stati ed eventi che costituiscono il ciclo di vita dell'attività Android

Pensa agli stati come ai nodi di un grafico e agli eventi come alle frecce tra questi nodi.

Una classe può monitorare lo stato del ciclo di vita del componente implementando DefaultLifecycleObserver e sostituendo i metodi corrispondenti, come onCreate, onStart e così via. 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'interfaccia LifecycleOwner, illustrata nella sezione seguente.

LifecycleOwner

LifecycleOwner è un'interfaccia con un singolo metodo 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 di applicazione, consulta ProcessLifecycleOwner.

Questa interfaccia esegue l'astrazione della proprietà di un Lifecycle dalle singole classi, come Fragment e AppCompatActivity, e consente di scrivere componenti che funzionano con queste. 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 che un osservatore può registrare per osservarlo.

Per l'esempio di monitoraggio della posizione, possiamo fare in modo che la classe MyLocationListener implementi DefaultLifecycleObserver e poi la inizializzia con Lifecycle dell'attività 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 è dichiarata in MyLocationListener anziché nell'attività. La memorizzazione della logica dei singoli componenti semplifica la gestione della logica delle attività e dei frammenti.

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 è evitare di richiamare determinati callback se il valore Lifecycle non è in buono stato. Ad esempio, se il callback esegue una transazione del frammento dopo la salvataggio dello stato dell'attività, attiverebbe un arresto anomalo, quindi non vogliamo mai invocare questo callback.

Per semplificare questo caso d'uso, la classe Lifecycle consente ad altri oggetti di eseguire query sullo stato corrente.

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 da un altro frammento, dobbiamo solo inizializzalo. Tutte le operazioni di configurazione e smantellamento sono gestite dal corso stesso.

Se una libreria fornisce classi che devono funzionare con il ciclo di vita di Android, consigliamo di utilizzare componenti consapevoli del ciclo di vita. I client della libreria possono integrare facilmente questi componenti senza la gestione manuale del ciclo di vita lato client.

Implementazione di un LifecycleOwner personalizzato

I frammenti e le attività nella libreria di supporto 26.1.0 e versioni successive implementano già l'interfaccia LifecycleOwner.

Se hai una classe personalizzata di cui vuoi creare un LifecycleOwner, puoi utilizzare la classe LifecycleRegistry, ma devi inoltrare gli eventi a quella 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 consapevoli del ciclo di vita

  • Mantieni i controller dell'interfaccia utente (attività e frammenti) il più semplici possibile. Non devono tentare di acquisire i propri dati, ma devono utilizzare un ViewModel per farlo e osservare un oggetto LiveData per riflettere le modifiche nelle visualizzazioni.
  • Prova a scrivere UI basate sui dati in cui è responsabilità del controller dell'interfaccia utente aggiornare le visualizzazioni man mano che i dati cambiano o notificare le azioni dell'utente al ViewModel.
  • Inserisci la logica dei dati nella classe ViewModel. ViewModel deve fungere da connettore tra il controller dell'interfaccia utente e il resto dell'app. Fai attenzione, però: non è compito di ViewModel recuperare i dati (ad esempio da una rete). Invece, ViewModel deve chiamare il componente appropriato per recuperare i dati, quindi fornire nuovamente il risultato al controller dell'interfaccia utente.
  • Utilizza il Data Binding per mantenere un'interfaccia chiara tra le visualizzazioni e il controller dell'interfaccia utente. In questo modo puoi trovare le visualizzazioni più dichiarative e ridurre al minimo il codice di aggiornamento da scrivere nelle attività e nei frammenti. 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 la tua UI è complessa, valuta la possibilità di creare una classe presentatore per gestire le modifiche dell'interfaccia utente. Questa potrebbe essere un'attività laboriosa, ma può facilitare il test dei componenti dell'interfaccia utente.
  • Evita di fare riferimento a un contesto View o Activity nel ViewModel. Se ViewModel sopravvive all'attività (in caso di modifiche alla configurazione), la tua attività viene persa e non viene smaltita correttamente dal garbage collector.
  • Utilizza le coroutine Kotlin per gestire attività a lunga esecuzione e altre operazioni che possono essere eseguite in modo asincrono.

Casi d'uso per i componenti consapevoli del ciclo di vita

I componenti consapevoli del ciclo di vita possono semplificare notevolmente la gestione dei cicli di vita in una serie di casi. Ecco alcuni esempi:

  • Passare da aggiornamenti della posizione approssimativi a aggiornamenti granulari e viceversa. Utilizza componenti attenti al ciclo di vita per attivare aggiornamenti della posizione granulari mentre la sua app di geolocalizzazione è visibile e passa agli aggiornamenti meno granulari 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 dei video. Utilizza componenti consapevoli del ciclo di vita per avviare il buffering dei video il prima possibile, ma rimanda la riproduzione fino all'avvio completo dell'app. Puoi anche utilizzare componenti attenti al ciclo di vita per interrompere il buffering quando l'app viene distrutta.
  • Avvio e arresto della connettività di rete. Utilizza componenti consapevoli del ciclo di vita per attivare l'aggiornamento in tempo reale (streaming) dei dati di rete mentre un'app è in primo piano e anche per mettere in pausa automaticamente l'app quando passa in background.
  • Mettere in pausa e riprendere gli elementi drawable animati. Utilizza componenti consapevoli del ciclo di vita per gestire la messa in pausa degli elementi drawable animati quando l'app è in background e riprendere gli elementi drawable dopo che l'app è in primo piano.

Gestione degli eventi di interruzione

Quando un Lifecycle appartiene a un AppCompatActivity o a un Fragment, lo stato di Lifecycle diventa CREATED e l'evento ON_STOP viene inviato quando viene chiamato onSaveInstanceState() di AppCompatActivity o Fragment.

Quando lo stato di un Fragment o di un AppCompatActivity viene salvato tramite onSaveInstanceState(), la sua UI viene considerata immutabile fino a quando non viene invocato ON_START. Il tentativo di modificare l'interfaccia utente dopo il salvataggio dello stato potrebbe causare 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, visita la pagina commit().

LiveData impedisce questo caso limite in modo predefinito evitando di chiamare il suo osservatore se il valore Lifecycle associato all'osservatore non è almeno STARTED. Dietro le quinte, chiama isAtLeast() prima di decidere di invocare il suo osservatore.

Purtroppo, il metodo onStop() di AppCompatActivity viene chiamato dopo onSaveInstanceState(), il che lascia un vuoto in cui le modifiche dello stato dell'interfaccia utente non sono consentite, ma il valore Lifecycle non è ancora stato spostato nello stato CREATED.

Per evitare questo problema, la classe Lifecycle nella versione beta2 e versioni precedenti contrassegna lo stato come CREATED senza inviare l'evento in modo che qualsiasi codice che controlli lo stato corrente riceva il valore reale anche se l'evento non viene inviato finché onStop() non viene chiamato dal sistema.

Purtroppo, questa soluzione presenta due problemi principali:

  • A partire dal livello API 23 e versioni 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 chiama necessariamente onStop(). Ciò 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 voglia esporre un comportamento simile a quello della classe LiveData deve implementare la soluzione alternativa fornita dalla Lifecycle versione beta 2 e precedenti.

Risorse aggiuntive

Per scoprire di più sulla gestione dei cicli di vita con componenti consapevoli del ciclo di vita, consulta le seguenti risorse aggiuntive.

Campioni

  • Sunflower, un'app di dimostrazione che mostra le best practice con i componenti dell'architettura

Codelab

Blog