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 en 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. Si usas 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 ofrece 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 marco de trabajo de Android están vinculados con ciclos de vida. El código del marco de trabajo o el sistema operativo que se ejecuta en tu proceso se encarga del control de los ciclos de vida. Estos son fundamentales para el funcionamiento de Android y tu aplicación debe respetarlos; de lo contrario, podrían producirse pérdidas de memoria o fallas en la aplicación.

Imagina que tenemos una actividad que muestra la ubicación del dispositivo en la pantalla. Una implementación habitual 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, 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 prolongada, como una verificación 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 resistente 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 tengan este estado.

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

Evento
Los eventos del ciclo de vida que se envían desde el marco y la clase Lifecycle. Estos eventos se asignan a eventos de devolución de llamada en actividades y fragmentos.
Estado
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 a los estados como nodos de un gráfico y a los eventos como los bordes de estos nodos.

Una clase puede supervisar el estado del ciclo de vida del componente agregando anotaciones a sus métodos. Luego, puedes agregar un observador. Para ello, realiza una llamada al método addObserver() de la clase Lifecycle y transfiere una instancia de tu observador, como se muestra en el siguiente ejemplo:

Kotlin

    class MyObserver : LifecycleObserver {

        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
        fun connectListener() {
            ...
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        fun disconnectListener() {
            ...
        }
    }

    myLifecycleOwner.getLifecycle().addObserver(MyObserver())
    

Java

    public class MyObserver implements LifecycleObserver {
        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
        public void connectListener() {
            ...
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        public void disconnectListener() {
            ...
        }
    }

    myLifecycleOwner.getLifecycle().addObserver(new MyObserver());
    

En el ejemplo que se muestra a continuación, el objeto myLifecycleOwner implementa la interfaz LifecycleOwner. Esto se explica en la sección siguiente.

LifecycleOwner

LifecycleOwner es una interfaz de método individual que indica que la clase tiene un Lifecycle. Tiene un método, getLifecycle(), que la clase debe implementar. Si, en cambio, intentas 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 ellos. Cualquier clase de aplicación personalizada puede implementar la interfaz LifecycleOwner.

Los componentes que implementan LifecycleObserver funcionan sin inconvenientes con los componentes 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 LifecycleObserver y, luego, iniciarlo 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á una falla, de manera que nunca debería invocarse esa devolución de llamada.

Para facilitar este caso práctico, 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
    ) {

        private var enabled = false

        @OnLifecycleEvent(Lifecycle.Event.ON_START)
        fun start() {
            if (enabled) {
                // connect
            }
        }

        fun enable() {
            enabled = true
            if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
                // connect if not connected
            }
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
        fun stop() {
            // disconnect if connected
        }
    }
    

Java

    class MyLocationListener implements LifecycleObserver {
        private boolean enabled = false;
        public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
           ...
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_START)
        void start() {
            if (enabled) {
               // connect
            }
        }

        public void enable() {
            enabled = true;
            if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
                // connect if not connected
            }
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
        void stop() {
            // 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 en la Biblioteca de compatibilidad 26.1.0 y en las versiones posteriores ya cuentan con la interfaz LifecycleOwner.

Si tienes una clase personalizada que te gustaría convertir en LifecycleOwner, puedes usar 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;
        }
    }
    

Recomendaciones de 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. Para eso, usa un ViewModel y observa un objeto LiveData para que los cambios se reflejen en las vistas.
  • Intenta escribir IU basadas en datos, donde la responsabilidad del controlador de la IU sea actualizar las vistas a medida que los datos cambian o notificar las acciones del usuario al ViewModel.
  • Coloca la lógica de datos en la clase ViewModel. ViewModel funcionará como conector entre el controlador de la IU y el resto de la app. Sin embargo, ten en cuenta que obtener datos (por ejemplo, de una red) no es responsabilidad de ViewModel. En cambio, ViewModel debería realizar una llamada al componente apropiado para obtener 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 de 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 prácticos de componentes optimizados para ciclos de vida

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

  • Alternar entre actualizaciones de ubicación aproximadas o detalladas. Usa componentes optimizados para 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 a fin de 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 una AppCompatActivity o un Fragment, el estado de Lifecycle cambia a CREATED y se despacha el evento ON_STOP cuando se llama a AppCompatActivity o a 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 app; por este motivo, FragmentManager muestra una excepción si la apliación ejecuta una FragmentTransaction después de que se guardó el estado. Para obtener más información, consulta commit().

LiveData evita este caso límite absteniéndose de llamar a su observador si el Lifecycle asociado del observador no está, como mínimo, iniciado (STARTED). Sin que se perciba, llama a isAtLeast() antes de decidir invocar a su observador.

Lamentablemente, el método AppCompatActivity de onStop() se invoca 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 en 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 invoque a onStop().

Lamentablemente, esta solución tiene dos problemas principales:

  • En la API nivel 23 y anteriores, el sistema Android, en realidad, guarda el estado de una actividad incluso si está cubierta de manera parcial por otra actividad. En otras palabras, el sistema Android hace una llamada a onSaveInstanceState(), pero no necesariamente a onStop(). Así, se 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 al de la clase LiveData debe implementar la solución proporcionada por la versión beta 2 o una anterior de Lifecycle.

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

Codelabs

Blogs