ActivityLifecycleCallbacks: un punto ciego en la API pública



Desde pequeño, me gusta leer las instrucciones. Crecí, pero todavía me sorprende cómo los adultos son descuidados con las instrucciones: muchos de ellos piensan que todos lo saben, y al mismo tiempo usan una o dos funciones, ¡mientras que hay muchas más! ¿Cuántos de ustedes solían mantener la temperatura en el microondas? Y ella está en casi todos.

Una vez decidí leer la documentación de las diversas clases del marco de Android. Revisé las clases principales: Vista, Actividad, Fragmento, Aplicación, y estaba muy interesado en el método Application.registerActivityLifecycleCallbacks () y la interfaz ActivityLifecycleCallbacks . De los ejemplos de su uso en Internet, nada fue mejor que registrar el ciclo de vida de la Actividad. Luego comencé a experimentar con mí mismo, y ahora en Yandex.Money lo usamos activamente para resolver una amplia gama de tareas relacionadas con el impacto de los objetos de actividad desde el exterior.

¿Qué son ActivityLifecycleCallbacks?


Mire esta interfaz, así es como se veía cuando apareció en API 14:

public interface ActivityLifecycleCallbacks {    void onActivityCreated(Activity activity, Bundle savedInstanceState);    void onActivityStarted(Activity activity);    void onActivityResumed(Activity activity);    void onActivityPaused(Activity activity);    void onActivityStopped(Activity activity);    void onActivitySaveInstanceState(Activity activity, Bundle outState);    void onActivityDestroyed(Activity activity); } 

Comenzando con API 29, se le han agregado varios métodos nuevos.
 public interface ActivityLifecycleCallbacks {   default void onActivityPreCreated( @NonNull Activity activity, @Nullable Bundle savedInstanceState) { }   void onActivityCreated( @NonNull Activity activity, @Nullable Bundle savedInstanceState);   default void onActivityPostCreated( @NonNull Activity activity, @Nullable Bundle savedInstanceState) { }   default void onActivityPreStarted(@NonNull Activity activity) { }   void onActivityStarted(@NonNull Activity activity);   default void onActivityPostStarted(@NonNull Activity activity) { }   default void onActivityPreResumed(@NonNull Activity activity) { }   void onActivityResumed(@NonNull Activity activity);   default void onActivityPostResumed(@NonNull Activity activity) { }   default void onActivityPrePaused(@NonNull Activity activity) { }   void onActivityPaused(@NonNull Activity activity);   default void onActivityPostPaused(@NonNull Activity activity) { }   default void onActivityPreStopped(@NonNull Activity activity) { }   void onActivityStopped(@NonNull Activity activity);   default void onActivityPostStopped(@NonNull Activity activity) { }   default void onActivityPreSaveInstanceState( @NonNull Activity activity, @NonNull Bundle outState) { }   void onActivitySaveInstanceState( @NonNull Activity activity, @NonNull Bundle outState);   default void onActivityPostSaveInstanceState( @NonNull Activity activity, @NonNull Bundle outState) { }   default void onActivityPreDestroyed(@NonNull Activity activity) { }   void onActivityDestroyed(@NonNull Activity activity);   default void onActivityPostDestroyed(@NonNull Activity activity) { } } 


Quizás esta interfaz recibe tan poca atención porque solo apareció en Android 4.0 ICS. Pero en vano, porque te permite hacer algo muy interesante de forma nativa: influir en todos los objetos de actividad desde el exterior. Pero más sobre eso más adelante, y primero eche un vistazo más de cerca a los métodos.

Cada método muestra un método similar del ciclo de vida de la Actividad y se llama en el momento en que el método se activa en cualquier Actividad en la aplicación. Es decir, si la aplicación se inicia con MainActivity, entonces el primero recibiremos una llamada a ActivityLifecycleCallback.onActivityCreated (MainActivity, null).

Genial, pero ¿cómo funciona? Aquí no hay magia: la actividad en sí misma informa sobre en qué estado se encuentran. Aquí hay un fragmento de código de Activity.onCreate ():

  mFragments.restoreAllState(p, mLastNonConfigurationInstances != null           ? mLastNonConfigurationInstances.fragments : null); } mFragments.dispatchCreate(); getApplication().dispatchActivityCreated(this, savedInstanceState); if (mVoiceInteractor != null) { 

Parece que nosotros mismos hicimos BaseActivity. Solo colegas de Android hicieron esto por nosotros, y también obligaron a todos a usarlo. ¡Y esto es muy bueno!

En API 29, estos métodos funcionan casi igual, pero sus copias Pre y Post se llaman honestamente antes y después de métodos específicos. Probablemente ahora esté controlado por el ActivityManager, pero esta es solo mi suposición, porque no ingresé a la fuente lo suficiente como para averiguarlo.

¿Cómo hacer que ActivityLifecycleCallbacks funcione?


Como todas las devoluciones de llamada, primero debe registrarlas. Registramos todos los ActivityLifecycleCallbacks en Application.onCreate (), por lo que obtenemos información sobre toda la Actividad y la capacidad de administrarlos.

 class MyApplication : Application() {   override fun onCreate() { super.onCreate() registerActivityLifecycleCallbacks(MyCallbacks()) } } 

Una pequeña digresión: a partir de API 29, ActivityLifecycleCallbacks también se puede registrar desde la Actividad. Esta será una devolución de llamada local que solo funciona para esta Actividad.

Eso es todo Pero puede encontrar esto simplemente ingresando el nombre ActivityLifecycleCallbacks en el cuadro de búsqueda. Habrá muchos ejemplos de registro del ciclo de vida de la Actividad, pero ¿es interesante? La actividad tiene muchos métodos públicos (alrededor de 400), y todo esto se puede utilizar para hacer muchas cosas interesantes y útiles.

¿Qué se puede hacer con esto?


Que quieres ¿Desea cambiar dinámicamente el tema en todas las actividades de la aplicación? Por favor: el método setTheme () es público, lo que significa que puede llamarlo desde un ActivityLifecycleCallback:

 class ThemeCallback( @StyleRes val myTheme: Int ) : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { activity.setTheme(myTheme) } } 

Repite este truco SOLO en casa
Algunas actividades de las bibliotecas conectadas pueden usar sus temas personalizados. Por lo tanto, verifique el paquete o cualquier otro síntoma por el cual se pueda determinar que el tema de esta Actividad se puede cambiar de manera segura. Por ejemplo, verificamos el paquete de esta manera (en Kotlinovsky =)):

 class ThemeCallback( @StyleRes val myTheme: Int ) : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { val myPackage = "my.cool.application" activity .takeIf { it.javaClass.name.startsWith(myPackage) } ?.setTheme(myTheme) } } 

¿El ejemplo no funciona? Es posible que haya olvidado registrar ThemeCallback en la Aplicación o Aplicación en AndroidManifest.

¿Quieres otro ejemplo interesante? Puede mostrar cuadros de diálogo en cualquier Actividad en la aplicación.

 class DialogCallback( val dialogFragment: DialogFragment ) : Application.ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { if (savedInstanceState == null) { val tag = dialogFragment.javaClass.name (activity as? AppCompatActivity) ?.supportFragmentManager ?.also { fragmentManager -> if (fragmentManager.findFragmentByTag(tag) == null) { dialogFragment.show(fragmentManager, tag) } } } } } 

Repite este truco SOLO en casa
Por supuesto, no debe mostrar un diálogo en cada pantalla; nuestros usuarios no nos amarán por esto. Pero a veces puede ser útil mostrar algo como esto en algunas pantallas específicas.

Y aquí hay otro caso: ¿qué pasa si necesitamos comenzar una actividad? Todo es simple aquí: Activity.startActivity (), y lo condujo. Pero, ¿qué sucede si necesitamos esperar el resultado después de llamar a Activity.startActivityForResult ()? Tengo una receta:

 class StartingActivityCallback : Application.ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { if (savedInstanceState == null) { (activity as? AppCompatActivity) ?.supportFragmentManager ?.also { fragmentManager -> val startingFragment = findOrCreateFragment(fragmentManager) startingFragment.listener = { resultCode, data -> // handle response here } // start Activity inside StartingFragment } } } private fun findOrCreateFragment( fragmentManager: FragmentManager ): StartingFragment { val tag = StartingFragment::class.java.name return fragmentManager .findFragmentByTag(tag) as StartingFragment? ?: StartingFragment().apply { fragmentManager .beginTransaction() .add(this, tag) .commit() } } } 

En el ejemplo, simplemente dejamos caer el Fragmento, que inicia la Actividad y obtiene el resultado, y luego nos delega su procesamiento. Tenga cuidado: aquí verificamos que nuestra Actividad es AppCompatActivity, lo que puede conducir a un bucle infinito. Use otras condiciones.

Vamos a complicar los ejemplos. Hasta ese momento, utilizamos solo aquellos métodos que ya están en la Actividad. ¿Qué hay de agregar el tuyo? Supongamos que queremos enviar análisis sobre cómo abrir una pantalla. Al mismo tiempo, nuestras pantallas tienen sus propios nombres. ¿Cómo resolver este problema? Muy simple Cree una interfaz de pantalla que pueda dar el nombre de pantalla:

 interface Screen { val screenName: String } 

Ahora lo implementamos en la Actividad deseada:

 class NamedActivity : Activity(), Screen { override val screenName: String = "First screen" } 

Después de eso, configuramos ActivityLifecycleCallbacks especiales para dicha Actividad:

 class AnalyticsActivityCallback( val sendAnalytics: (String) -> Unit ) : Application.ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { if (savedInstanceState == null) { (activity as? Screen)?.screenName?.let(sendAnalytics) } } } 

¿Ves? Simplemente verificamos la interfaz y, si se implementa, enviamos análisis.

Repita para la fijación. ¿Qué hacer si necesita lanzar algunos parámetros más? Extienda la interfaz:

 interface ScreenWithParameters : Screen { val parameters: Map<String, String> } 

Implementamos:

 class NamedActivity : Activity(), ScreenWithParameters { override val screenName: String = "First screen" override val parameters: Map<String, String> = mapOf("key" to "value") } 

Enviamos:

 class AnalyticsActivityCallback( val sendAnalytics: (String, Map<String, String>?) -> Unit ) : Application.ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { if (savedInstanceState == null) { (activity as? Screen)?.screenName?.let { name -> sendAnalytics( name, (activity as? ScreenWithParameters)?.parameters ) } } } } 

Pero aún es fácil. Todo esto fue solo para llevarlo a un tema realmente interesante: la inyección de dependencia nativa. Sí, tenemos Dagger, Koin, Guice, Kodein y más. Pero en proyectos pequeños, son redundantes. Pero tengo una solución ... ¿Adivina cuál?

Digamos que tenemos alguna herramienta como esta:

 class CoolToolImpl { val extraInfo = "i am dependency" } 

Ciérrelo con la interfaz, como programadores adultos:

 interface CoolTool { val extraInfo: String } class CoolToolImpl : CoolTool { override val extraInfo = "i am dependency" } 

Y ahora un poco de magia callejera de ActivityLifecycleCallbacks: crearemos una interfaz para implementar esta dependencia, la implementaremos en la Actividad deseada, y usando ActivityLifecycleCallbacks la encontraremos e implementaremos la implementación CoolToolImpl.

 interface RequireCoolTool { var coolTool: CoolTool } class CoolToolActivity : Activity(), RequireCoolTool { override lateinit var coolTool: CoolTool } class InjectingLifecycleCallbacks : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { (activity as? RequireCoolTool)?.coolTool = CoolToolImpl() } } 

Recuerde registrar InjectingLifecycleCallbacks en su aplicación, ejecútelo y funciona.

Y no olvides probar:

 @RunWith(AndroidJUnit4::class) class DIActivityTest { @Test fun `should access extraInfo when created`() { // prepare val mockTool: CoolTool = mock() val application = getApplicationContext<android.app.Application>() application.registerActivityLifecycleCallbacks( object : Application.ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { (activity as? RequireCoolTool)?.coolTool = mockTool } }) // invoke launch<DIActivity>(Intent(application, DIActivity::class.java)) // assert verify(mockTool).extraInfo } } 

Pero en proyectos grandes, este enfoque no se escalará bien, por lo que no voy a quitarle ningún marco DI a nadie. Donde es mejor combinar esfuerzos y lograr sinergia. Te mostraré el ejemplo de Dagger2. Si tiene alguna Actividad básica en el proyecto que hace algo como AndroidInjection.inject (this), entonces es hora de tirarla. En su lugar, haga lo siguiente:
  1. de acuerdo con las instrucciones, implementamos DispatchingAndroidInjector en la aplicación;
  2. cree un ActivityLifecycleCallbacks que llame a DispatchingAndroidInjector.maybeInject () en cada actividad;
  3. registre ActivityLifecycleCallbacks en la aplicación.


 class MyApplication : Application() { @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any> override fun onCreate() { super.onCreate() DaggerYourApplicationComponent.create().inject(this); registerActivityLifecycleCallbacks( InjectingLifecycleCallbacks( dispatchingAndroidInjector ) ) } } class InjectingLifecycleCallbacks( val dispatchingAndroidInjector: DispatchingAndroidInjector<Any> ) : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) {       dispatchingAndroidInjector.maybeInject(activity) } } 

Y se puede lograr el mismo efecto con otros marcos DI. Intenta escribir en los comentarios lo que pasó.

Para resumir


ActivityLifecycleCallbacks es una herramienta poderosa y subestimada. Pruebe uno de estos ejemplos y deje que lo ayuden en sus proyectos de la misma manera que Yandex.Money ayuda a mejorar nuestras aplicaciones.

Source: https://habr.com/ru/post/482476/


All Articles