Cómo manejar ciclos de vida con componentes optimizados para ciclos de vida   Parte de Android Jetpack.

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 incluye interfaces y clases que te permiten compilar componentes optimizados para ciclos de vida; es decir, componentes que pueden ajustar automáticamente su comportamiento en función del 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. Cuando administras demasiados componentes, se genera una cantidad considerable de código en los métodos del ciclo de vida, como onStart() y onStop(), lo que dificulta el mantenimiento.

Además, no hay garantía de que el componente vaya a comenzar antes de se detenga la actividad o el fragmento. Sobre todo, si necesitamos llevar a cabo una operación de larga duración, como algunas verificaciones de configuración en onStart(). Esto puede provocar una condición de carrera en la que el método onStop() termina antes que el método onStart(), lo que a su vez mantiene activo el componente más tiempo de lo 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 ofrece interfaces y clases que te permiten abordar estos problemas de manera resiliente y aislada.

Lifecycle

Lifecycle es una clase que mantiene 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 realizar un seguimiento del estado del ciclo de vida de su componente asociado:

Evento
Los eventos de ciclo de vida que se despachan desde el framework y la clase Lifecycle. Estos eventos se asignan a eventos de devolución de llamada en actividades y fragmentos.
Estado
Es el estado actual del componente al que le hace un seguimiento el objeto Lifecycle.
Diagrama de estados de ciclos de vida
Figura 1: Estados y eventos que componen el ciclo de vida de la actividad de Android

Considera los estados como los nodos de un gráfico y los eventos como los bordes de estos nodos.

Una clase puede supervisar el estado del ciclo de vida del componente mediante la implementación de DefaultLifecycleObserver y la anulación de los métodos correspondientes, como onCreate y onStart, entre otros. Luego, puedes agregar un observador. Para ello, llama al método addObserver() de la clase Lifecycle y pasa 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 analiza 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 la clase debe implementar. Si, en cambio, quieres administrar el ciclo de vida de todo el proceso de una aplicación, consulta ProcessLifecycleOwner.

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

Los componentes que implementan DefaultLifecycleObserver funcionan sin inconvenientes con los que implementan LifecycleOwner, ya que un propietario puede brindar un ciclo de vida, al que un observador puede registrarse para mirar.

Para el ejemplo de seguimiento de ubicación, podemos hacer que la clase MyLocationListener implemente DefaultLifecycleObserver y, luego, lo inicialice con el Lifecycle de la actividad en el método onCreate(). Esta acción permite que la clase MyLocationListener sea autosuficiente, lo que significa que la lógica para reaccionar ante los cambios en el estado del ciclo de vida se declara en MyLocationListener, no en 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 habitual consiste en evitar invocar ciertas devoluciones de llamada si el Lifecycle no está en buen estado en ese momento. Por ejemplo, si la devolución de llamada ejecuta una transacción de fragmento una vez guardado el estado de la actividad, generará un error, de manera que nunca deberí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 cuentan con la interfaz LifecycleOwner.

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

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 deberían intentar adquirir sus propios datos. En su lugar, usa un ViewModel y observa un objeto LiveData a fin de que los cambios se reflejen en las vistas.
  • Intenta escribir IU basadas en datos, donde la responsabilidad del controlador de IU sea actualizar las vistas a medida que los datos cambien o notificar las acciones del usuario al ViewModel.
  • Coloca la lógica de datos en la clase ViewModel. ViewModel funcionará como el conector entre el controlador de IU y el resto de la app. Sin embargo, ten en cuenta que recuperar datos (por ejemplo, de una red) no es responsabilidad de ViewModel. En cambio, ViewModel debería realizar una llamada al componente apropiado para recuperar los datos y, luego, proporcionar los resultados al controlador de IU.
  • Usa la vinculación de datos para mantener una interfaz limpia entre tus vistas y el controlador de IU. Esto permite que las vistas sean más declarativas y disminuir el código de actualización que debes 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 de presentador para manejar los cambios 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 el ViewModel. Si el ViewModel dura más tiempo que la actividad (en el caso de los cambios de configuración), tu actividad tendrá fugas y el recolector de elementos no utilizados no la desechará correctamente.
  • Usa corrutinas de Kotlin para administrar tareas de larga duración y otras operaciones que se pueden ejecutar de manera 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 los ciclos de vida a fin de habilitar actualizaciones de ubicación detalladas mientras tu app está visible y cambia a actualizaciones aproximadas 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 a fin de iniciar el almacenamiento de videos en búfer tan pronto como sea posible, pero posterga la reproducción hasta que la app se haya iniciado por completo. También puedes usar componentes optimizados para ciclos de vida a fin de terminar el almacenamiento en búfer cuando se destruya tu app.
  • Iniciar y detener la conectividad de red. Usa componentes optimizados para ciclos de vida a fin de habilitar la actualización en vivo (transmisión) de datos de redes mientras una app está en primer plano y también para detenerla automáticamente cuando 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 el estado de Fragment o AppCompatActivity se guarda a través de onSaveInstanceState(), su IU se considera inmutable hasta que se llama a ON_START. Si intentas modificar la IU una vez guardado el estado, es posible que provoques inconsistencias en el estado de navegación de tu aplicación; por este motivo, FragmentManager arroja una excepción si la app ejecuta una FragmentTransaction después de que se guardó el estado. Consulta commit() para obtener información.

LiveData evita este caso límite absteniéndose de llamar a su observador si su Lifecycle asociado no está, como mínimo, 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 una brecha donde no se permiten los cambios de estado de IU, pero el Lifecycle todavía no se trasladó al estado CREATED.

Para evitar este problema, la clase Lifecycle en la versión beta2 y las anteriores identifica el estado como CREATED sin despachar el evento, de manera que cualquier código que verifique el estado actual obtendrá el valor real, aunque el evento no se despache hasta que el sistema llame a onStop().

Lamentablemente, esta solución tiene dos problemas principales:

  • En el nivel de API 23 e inferiores, el sistema Android 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 genera un posible intervalo prolongado en el que el observador todavía cree que el ciclo de vida está activo aunque no sea posible modificar el estado de su IU.
  • Cualquier clase que quiera exponer un comportamiento similar a la clase LiveData debe implementar la solución proporcionada por la versión beta 2 de Lifecycle y versiones anteriores.

Recursos adicionales

Puedes obtener más información sobre la administración de los ciclos de vida con componentes optimizados para ciclos de vida en los siguientes recursos adicionales.

Ejemplos

  • Sunflower, una app de ejemplo que demuestra las prácticas recomendadas para usar los componentes de la arquitectura

Codelabs

Blogs