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
.
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 objetoLiveData
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 deViewModel
. 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
oActivity
en elViewModel
. Si elViewModel
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 aonStop()
. 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ónbeta 2
deLifecycle
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
- Muestra básica de los componentes de la arquitectura de Android
- Sunflower, una app de ejemplo que demuestra las prácticas recomendadas para usar los componentes de la arquitectura
Codelabs
Blogs
- Presentación de Android Sunflower (en inglés)
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Descripción general de LiveData
- Cómo usar las corrutinas de Kotlin con componentes optimizados para ciclos de vida
- Módulo de estado guardado para ViewModel