Cómo controlar ciclos de vida con componentes optimizados para ciclos de vida (vistas)

Conceptos y la implementación de Jetpack Compose

Los componentes optimizados para ciclos de vida realizan acciones como respuesta a un cambio en el estado del ciclo de vida de otro componente, como actividades o fragmentos. Estos componentes te ayudan a crear un código mejor organizado, y a menudo más liviano, que resulta más fácil de mantener.

Un patrón común consiste en implementar las acciones de los componentes dependientes en los métodos del ciclo de vida de actividades y fragmentos. Sin embargo, este patrón genera una organización deficiente del código y la proliferación de errores. Al usar componentes optimizados para ciclos de vida, puedes sacar el código de los componentes dependientes que se encuentra en los métodos del ciclo de vida y colocarlo en los propios componentes.

El paquete androidx.lifecycle proporciona clases e interfaces que te permiten compilar componentes optimizados para ciclos de vida, que son componentes que pueden ajustar automáticamente su comportamiento según el estado actual del ciclo de vida de una actividad o un fragmento.

La mayoría de los componentes de la app definidos en el framework de Android están vinculados con ciclos de vida. El código del framework o el sistema operativo que se ejecuta en tu proceso se encarga del manejo de los ciclos de vida. Estos son fundamentales para el funcionamiento de Android y tu app debe respetarlos; de lo contrario, se podrían provocar pérdidas de memoria o fallas en la app.

Imagina que tenemos una actividad que muestra la ubicación del dispositivo en la pantalla. Una implementación usual podría ser similar a la siguiente:

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
    }
}

Aunque este ejemplo parece correcto, en la app real, tendrás demasiadas llamadas que administren la IU y otros componentes como respuesta al estado actual del ciclo de vida. La administración de varios componentes coloca una cantidad considerable de código en los métodos del ciclo de vida, como onStart() y onStop, lo que dificulta su mantenimiento.

Además, no hay garantía de que el componente vaya a comenzar antes de se detenga la actividad o el fragmento. Esto es especialmente cierto si necesitamos realizar una operación de larga duración, como una verificación de configuración en onStart. Esto puede causar una condición de carrera en la que el método onStop() finaliza antes que onStart, lo que mantiene el componente activo durante más tiempo del necesario.

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();
    }
}

El paquete androidx.lifecycle proporciona clases e interfaces que te ayudan a abordar estos problemas de forma resiliente y aislada.

Ciclo de vida

Lifecycle es una clase que contiene la información sobre el estado del ciclo de vida de un componente (como una actividad o un fragmento) y permite que otros objetos observen este estado.

Lifecycle usa dos enumeraciones principales para hacer un seguimiento del estado del ciclo de vida de su componente asociado:

Evento

Los eventos de ciclo de vida que se envían desde el framework y la clase Lifecycle Estos eventos se asignan a los eventos de devolución de llamada en actividades y fragmentos.

Estado

Es el estado actual del componente del que hace un seguimiento el objeto Lifecycle.

Figura 1. Estados y eventos del ciclo de vida de la actividad de Android

Piensa en los estados como nodos de un grafo y en los eventos como los bordes entre estos nodos.

Una clase puede supervisar el estado del ciclo de vida del componente implementando DefaultLifecycleObserver y anulando los métodos correspondientes, como onCreate, onStart, etc. Luego, puedes agregar un observador llamando al método addObserver() de la clase Lifecycle y pasando una instancia de tu observador, como se muestra en el siguiente ejemplo:

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());

En el ejemplo anterior, el objeto myLifecycleOwner implementa la interfaz LifecycleOwner, que se explica en la siguiente sección.

LifecycleOwner

LifecycleOwner es una interfaz de método único que indica que la clase tiene un Lifecycle. Tiene un método, getLifecycle, que debe implementar la clase. Si, en cambio, intentas administrar el ciclo de vida de todo un proceso de aplicación, consulta ProcessLifecycleOwner.

Esta interfaz abstrae la propiedad de un Lifecycle de clases individuales, como Fragment y AppCompatActivity, y permite escribir componentes que funcionen con ellas. Cualquier clase de aplicación personalizada puede implementar la interfaz LifecycleOwner.

Los componentes que implementan DefaultLifecycleObserver funcionan sin problemas con los componentes que implementan LifecycleOwner, ya que un propietario puede proporcionar un ciclo de vida que un observador puede registrar para mirar.

En el ejemplo de seguimiento de ubicación, podemos hacer que la clase MyLocationListener implemente DefaultLifecycleObserver y, luego, inicializarla con el Lifecycle de la actividad en el método onCreate(). Esto permite que la clase MyLocationListener sea autosuficiente, lo que significa que la lógica para reaccionar a los cambios en el estado del ciclo de vida se declara en MyLocationListener en lugar de la actividad. El hecho de que los componentes individuales almacenen su propia lógica permite que la lógica de las actividades y los fragmentos sea más fácil de administrar.

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 de uso común es evitar invocar ciertas devoluciones de llamada si el Lifecycle no está en un buen estado en este momento. Por ejemplo, si la devolución de llamada ejecuta una transacción de fragmento después de que se guarda el estado de la actividad, se activará una falla, por lo que nunca querríamos invocar esa devolución de llamada.

Para facilitar este caso de uso, la clase Lifecycle permite que otros objetos consulten el estado actual.

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
    }
}

Mediante esta implementación, nuestra clase LocationListener está completamente optimizada para los ciclos de vida. Si necesitamos usar el LocationListener de otra actividad o fragmento, solo es necesario inicializarlo. La propia clase administra todas las operaciones de configuración y desconexión.

Si una biblioteca proporciona clases que necesitan funcionar con el ciclo de vida de Android, recomendamos el uso de componentes optimizados para ciclos de vida. Los clientes de tu biblioteca pueden integrar fácilmente esos componentes sin la administración manual del ciclo de vida del cliente.

Cómo implementar un LifecycleOwner personalizado

Los fragmentos y las actividades de la biblioteca de compatibilidad 26.1.0 y versiones posteriores ya implementan la interfaz LifecycleOwner.

Si tienes una clase personalizada que te gustaría convertir en un LifecycleOwner, puedes usar la clase LifecycleRegistry, pero debes reenviar eventos a esa clase, como se muestra en el siguiente ejemplo de código:

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;
    }
}

Prácticas recomendadas para componentes optimizados para ciclos de vida

  • Mantén tus controladores de IU (actividades y fragmentos) tan simples como sea posible. No deben intentar adquirir sus propios datos; en su lugar, deben usar un ViewModel para hacerlo y observar un objeto LiveData para reflejar los cambios en las vistas.
  • Intenta escribir IU basadas en datos en las que la responsabilidad del controlador de IU sea actualizar las vistas a medida que cambian los datos o notificar las acciones del usuario al ViewModel.
  • Coloca la lógica de datos en tu clase ViewModel. ViewModel debe servir como conector entre el controlador de IU y el resto de la app. Sin embargo, ten cuidado, ya que no es responsabilidad de ViewModel recuperar datos (por ejemplo, de una red). En su lugar, ViewModel debería llamar al componente adecuado para recuperar los datos y, luego, proporcionar el resultado al controlador de IU.
  • Usa la vinculación de datos para mantener una interfaz limpia entre tus vistas y el controlador de IU. Esto te permite hacer que tus vistas sean más declarativas y minimizar el código de actualización que necesitas escribir en tus actividades y fragmentos. Si prefieres hacerlo en el lenguaje de programación Java, usa una biblioteca como Butter Knife para evitar el código estándar y tener una mejor abstracción.
  • Si tu IU es compleja, considera crear una clase presenter para controlar las modificaciones de la IU. Esta puede ser una tarea compleja, pero te permitirá probar tus componentes de IU con mayor facilidad.
  • Evita hacer referencia a un contexto View o Activity en tu ViewModel. Si el ViewModel sobrevive a la actividad (en caso de cambios de configuración), se produce una fuga de la actividad y el recolector de elementos no utilizados no la elimina correctamente.
  • Usa corrutinas de Kotlin para administrar tareas de larga duración y otras operaciones que se pueden ejecutar de forma asíncrona.

Casos de uso de componentes optimizados para ciclos de vida

Los componentes optimizados para los ciclos de vida pueden facilitar la administración de los ciclos de vida en varios casos. Algunos ejemplos son los siguientes:

  • Alternar entre actualizaciones de ubicación aproximadas o detalladas. Usa componentes optimizados para ciclos de vida para habilitar actualizaciones de ubicación detalladas mientras la app de ubicación esté visible y cambia a actualizaciones generales cuando la app esté en segundo plano. LiveData es un componente optimizado para ciclos de vida que permite que tu app actualice automáticamente la IU cuando usas cambios de ubicación.
  • Iniciar y detener el almacenamiento de videos en búfer. Usa componentes optimizados para ciclos de vida para iniciar el almacenamiento en búfer de video lo antes posible, pero posponer la reproducción hasta que la app se inicie por completo. También puedes usar componentes optimizados para ciclos de vida para finalizar el almacenamiento en búfer cuando se destruye tu app.
  • Iniciar y detener la conectividad de red. Usa componentes optimizados para ciclos de vida para habilitar las novedades en tiempo real (transmisión) de los datos de red mientras una app está en primer plano y, también, para pausarla automáticamente cuando la app pasa a segundo plano.
  • Detener y reanudar elementos de diseño animados. Usa componentes optimizados para ciclos de vida para detener los elementos de diseños animados cuando la app esté en segundo plano y reanudarlos una vez que la app esté en primer plano.

Cómo administrar eventos de detención

Cuando un Lifecycle pertenece a un AppCompatActivity o un Fragment, el estado de Lifecycle cambia a CREATED y se despacha el evento ON_STOP cuando se llama al AppCompatActivity o al onSaveInstanceState() de Fragment.

Cuando se guarda el estado de un Fragment o un AppCompatActivity con onSaveInstanceState, se considera que su IU es inmutable hasta que se llama a ON_START. Intentar modificar la IU después de que se guarda el estado puede causar incoherencias en el estado de navegación de tu aplicación, por lo que FragmentManager arroja una excepción si la app ejecuta un FragmentTransaction después de que se guarda el estado. Consulta commit() para obtener más detalles.

LiveData evita este caso límite de inmediato, ya que no llama a su observador si el Lifecycle asociado del observador no es, al menos, STARTED. En segundo plano, llama a isAtLeast() antes de decidir invocar a su observador.

Lamentablemente, se llama al método onStop() de AppCompatActivity después de onSaveInstanceState, lo que deja un espacio en el que no se permiten los cambios de estado de la IU, pero el Lifecycle aún no se movió al estado CREATED.

Para evitar este problema, la clase Lifecycle de la versión beta2 y anteriores marca el estado como CREATED sin enviar el evento, de modo que cualquier código que verifique el estado actual obtenga el valor real, aunque el evento no se envíe hasta que el sistema llame a onStop().

Lamentablemente, esta solución tiene dos problemas principales:

  • En la API de nivel 23 y de niveles inferiores, el sistema Android, en realidad, guarda el estado de una actividad incluso si está parcialmente cubierta por otra actividad. En otras palabras, el sistema Android llama a onSaveInstanceState(), pero no necesariamente a onStop. Esto crea un intervalo potencialmente largo en el que el observador aún cree que el ciclo de vida está activo, aunque no se pueda modificar su estado de IU.
  • Cualquier clase que quiera exponer un comportamiento similar al de la clase LiveData debe implementar la solución alternativa que proporciona la versión beta 2 y anteriores de Lifecycle.

Recursos adicionales

Para obtener más información sobre cómo controlar los ciclos de vida con componentes optimizados para ciclos de vida, consulta los siguientes recursos adicionales.

Codelabs

Blogs