ActivityLifecycleCallbacks — слепое пятно в публичном API



    С детства я люблю читать инструкции. Я вырос, но меня до сих пор удивляет то, как взрослые люди безалаберно относятся к инструкциям: многие из них считают, что все знают, и при этом пользуются одной-двумя функциями, в то время как их намного больше! Кто из вас пользовался функцией поддержания температуры в микроволновке? А она есть почти в каждой.

    Однажды я решил почитать документацию к различным классам Android framework. Пробежался по основным классам: View, Activity, Fragment, Application, — и меня очень заинтересовал метод Application.registerActivityLifecycleCallbacks() и интерфейс ActivityLifecycleCallbacks. Из примеров его использования в интернете не нашлось ничего лучше, чем логирование жизненного цикла Activity. Тогда я начал сам экспериментировать с ним, и теперь мы в Яндекс.Деньгах активно используем его при решении целого спектра задач, связанных с воздействием на объекты Activity снаружи.

    Что такое ActivityLifecycleCallbacks?


    Посмотрите на этот интерфейс, вот как он выглядел, когда появился в 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);
    }

    Начиная с API 29 в него добавили несколько новых методов
    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) { }
    }


    Возможно, этому интерфейсу уделяют так мало внимания, потому что он появился только в Android 4.0 ICS. А зря, ведь он позволяет нативно делать очень интересную вещь: воздействовать на все объекты Activity снаружи. Но об этом позже, а сначала внимательнее посмотрим на методы.

    Каждый метод отображает аналогичный метод жизненного цикла Activity и вызывается в тот момент, когда метод срабатывает на какой-либо Activity в приложении. То есть если приложение запускается с MainActivity, то первым мы получим вызов ActivityLifecycleCallback.onActivityCreated(MainActivity, null).

    Отлично, но как это работает? Тут никакой магии: Activity сами сообщают о том, в каком они состоянии. Вот кусочек кода из Activity.onCreate():

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

    Это выглядит так, как если бы мы сами сделали BaseActivity. Только коллеги из Android сделали это за нас, еще и обязали всех этим пользоваться. И это очень даже хорошо!

    В API 29 эти методы работают почти так же, но их Pre- и Post-копии честно вызываются до и после конкретных методов. Вероятно, теперь этим управляет ActivityManager, но это только мои догадки, потому что я не углублялся в исходники достаточно, чтобы это выяснить.

    Как заставить ActivityLifecycleCallbacks работать?


    Как и все callback, сначала их надо зарегистрировать. Мы регистрируем все ActivityLifecycleCallbacks в Application.onCreate(), таким образом получаем информацию обо всех Activity и возможность ими управлять.

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

    Небольшое отступление: начиная с API 29 ActivityLifecycleCallbacks можно зарегистрировать еще и изнутри Activity. Это будет локальный callback, который работает только для этого Activity.

    Вот и все. Но это вы можете найти, просто введя название ActivityLifecycleCallbacks в строку поисковика. Там будет много примеров про логирование жизненного цикла Activity, но разве это интересно? У Activity много публичных методов (около 400), и все это можно использовать для того, чтобы делать много интересных и полезных вещей.

    Что с этим можно сделать?


    А что вы хотите? Хотите динамически менять тему во всех Activity в приложении? Пожалуйста: метод setTheme() — публичный, а значит, его можно вызывать из ActivityLifecycleCallback:

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

    Повторяйте этот трюк ТОЛЬКО дома
    Какие-то Activity из подключенных библиотек могут использовать свои кастомные темы. Поэтому проверьте пакет или любой другой признак, по которому можно определить, что тему этой Activity можно безопасно менять. Например, проверяем пакет так (по-котлиновски =)):

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

    Пример не работает? Возможно, вы забыли зарегистрировать ThemeCallback в Application или Application в AndroidManifest.

    Хотите еще интересный пример? Можно показывать диалоги на любой Activity в приложении.

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

    Повторяйте этот трюк ТОЛЬКО дома
    Конечно же, не стоит показывать диалог на каждом экране — наши пользователи не будут нас любить за такое. Но иногда может быть полезно показать что-то такое на каких-то конкретных экранах.

    А вот еще кейс: что, если нам надо запустить Activity Тут все просто: Activity.startActivity() — и погнали. Но что делать, если нам надо дождаться результата после вызова Activity.startActivityForResult()? У меня есть один рецепт:

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

    В примере мы просто закидываем Fragment, который запускает Activity и получает результат, а потом делегирует его обработку нам. Будьте осторожны: тут мы проверяем, что наша Activity является AppCompatActivity, что может привести бесконечному циклу. Используйте другие условия.

    Усложним примеры. До этого момента мы использовали только те методы, которые уже есть в Activity. Как насчет того, чтобы добавить свои? Допустим, мы хотим отправлять аналитику об открытии экрана. При этом у наших экранов свои имена. Как решить эту задачу? Очень просто. Создадим интерфейс Screen, который сможет отдавать имя экрана:

    interface Screen {
        val screenName: String
    }

    Теперь имплементируем его в нужных Activity:

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

    После этого натравим на такие Activity специальные ActivityLifecycleCallback’и:

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

    Видите? Мы просто проверяем интерфейс и, если он реализован, отправляем аналитику.

    Повторим для закрепления. Что делать, если надо прокидывать еще и какие-то параметры? Расширим интерфейс:

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

    Имплементируем:

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

    Отправляем:

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

    Но это все еще легко. Все это было только ради того, чтобы подвести вас к по-настоящему интересной теме: нативное внедрение зависимостей. Да, у нас есть Dagger, Koin, Guice, Kodein и прочее. Но на небольших проектах они избыточны. Но у меня есть решение… Угадайте какое?

    Допустим, у нас есть некоторый инструмент, вроде такого:

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

    Закроем его интерфейсом, как взрослые программисты:

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

    А теперь немного уличной магии от ActivityLifecycleCallbacks: мы создадим интерфейс для внедрения этой зависимости, реализуем его в нужных Activity, а с помощью ActivityLifecycleCallbacks найдем его и внедрим реализацию 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()
        }
    }

    Не забудьте зарегистрировать InjectingLifecycleCallbacks в вашем Application, запускайте — и все работает.

    И не забудьте протестировать:

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

    Но на больших проектах такой подход будет плохо масштабироваться, поэтому я не собираюсь отбирать ни у кого DI-фреймворки. Куда лучше объединить усилия и достигнуть синергии. Покажу на примере Dagger2. Если у вас в проекте есть какая-то базовая Activity, которая делает что-то вроде AndroidInjection.inject(this), то пора ее выкинуть. Вместо этого сделаем следующее: 
    1. по инструкции внедряем DispatchingAndroidInjector в Application;
    2. создаем ActivityLifecycleCallbacks, который вызывает DispatchingAndroidInjector.maybeInject() на каждой Activity;
    3. регистрируем ActivityLifecycleCallbacks в Application.


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

    И такого же эффекта можно добиться с другими DI-фреймворками. Попробуйте и напишите в комментариях, что получилось.

    Подведем итоги


    ActivityLifecycleCallbacks — это недооцененный, мощный инструмент. Попробуйте какой-нибудь из этих примеров, и пусть они помогут вам в ваших проектах так же, как помогают Яндекс.Деньгам делать наши приложения лучше.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Уже используете ActivityLifecycleCallbacks в своем проекте?

    • 4,0%Да, только для логирования2
    • 36,0%Да, и не только для логирования18
    • 60,0%Нет, не используем30
    Яндекс.Деньги
    Как мы делаем Деньги

    Комментарии 9

      0
      Добрый день. Лучше расскажите про ваш опыт использования этой возможности в Яндекс.Деньгах. Потому что приведенные примеры, в некотором роде, относятся к категории «вредных советов», вы наверное их для того и выделили отдельной строкой "Повторяйте этот трюк ТОЛЬКО дома". Сейчас реально хороший пример только с инициализацией dagger2.
        0

        Добрый день.
        На самом деле эта статья построена на примерах из нашего приложения, хотя они и переписаны с целью привлечь внимание к конкретным вариантам использования. В прошлом году мы провели серию крупных рефакторингов, связанных с отказом от базовых Activity и некоторых singleton'ов. Часть кода, которая связана с жизненным циклом Activity как раз была заменена на решения на основе ActivityLifecycleCallbacks.
        Если кратко по каждому, то:


        • мы пока не используем решение с setTheme(), но планируем его внедрение;
        • для ряда задач мы используем решение с диалогом, но фильтрация организована так, чтобы диалоги отображались на конкретных экранах;
        • отправка событий экранов у нас построена именно так, как в статье;
        • внедрение зависимостей используется в обоих вариантах в разных приложениях.
          +1
          Вы не против если я вас еще поспрашиваю? Всегда хочется использовать чужой опыт, чем изобретать собственные велосипеды (:

          • А почему вы вообще планируете использовать setTheme() в таком контексте? Ведь существует стандартный механизм переопределения тем. Это для тех случаев когда один и тот же диалог на разных экранах имеет разное отображение?
          • Я так понял что «решение с диалогом» и «отправка событий экранов» раньше была в базовых классах, а теперь это все живет в ActivityLifecycleCallbacks. С какими вы столкнулись проблемами что решили применить такой подход или какие он дал вам преимущества по сравнению логикой основанной на базовых классах?
            0
            А почему вы вообще планируете использовать setTheme() в таком контексте? Ведь существует стандартный механизм переопределения тем. Это для тех случаев когда один и тот же диалог на разных экранах имеет разное отображение?

            Мы даем пользователям возможность выбирать тему, поэтому стандартный механизм нам не подходит. По стандартному механизму задается базовая тема, а этот механизм меняет ее на тот вариант, который выбрал пользователь.


            Я так понял что «решение с диалогом» и «отправка событий экранов» раньше была в базовых классах, а теперь это все живет в ActivityLifecycleCallbacks. С какими вы столкнулись проблемами что решили применить такой подход или какие он дал вам преимущества по сравнению логикой основанной на базовых классах?

            Да, раньше это было в базовых классах. Мы перешли на многомодульное приложение и не хотели вытаскивать базовую Activity в корневой модуль. К тому же так не приходится думать о соблюдении соглашения «все Activity должны наследоваться от базовой», мы можем просто добавлять общий функционал во все приложение сразу, не переписывая код.

              0
              Спасибо большое! И последний вопрос: У вас в приложении можно выбрать отдельно тему для всего Activity и отдельно темы для диалогов? Просто в теме Activity можно создать пользовательский атрибут для темы диалога, в диалоге сослаться на него и дальше все переопределять стандартными методами. На первый взгляд — отличное решение или есть какие-то «подводные камни»?
                0
                У вас в приложении можно выбрать отдельно тему для всего Activity и отдельно темы для диалогов?

                Нет, тема выбирается для всего приложения целиком, но она может содержать информацию для стилизации диалогов.


                Просто в теме Activity можно создать пользовательский атрибут для темы диалога, в диалоге сослаться на него и дальше все переопределять стандартными методами. На первый взгляд — отличное решение или есть какие-то «подводные камни»?

                Да, можно попробовать. Можно даже просто переопределить в теме аттрибуты android:dialogTheme и dialogTheme, если хотите повлиять на все диалоги, которые запущены на Вашем Activity.

        0
        Полагаю, что уже относительно ненужная штука из-за single activity подхода.
          0
          У SingleActivity подхода можно использовать FragmentManager.FragmentLifecycleCallbacks()
          Тогда можно использовать такую штуку
          supportFragmentManager.registerFragmentLifecycleCallbacks(
              InjectingFragmentLifecycleCallback(), true
          )
          

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое