ActivityLifecycleCallbacks — слепое пятно в публичном API
С детства я люблю читать инструкции. Я вырос, но меня до сих пор удивляет то, как взрослые люди безалаберно относятся к инструкциям: многие из них считают, что все знают, и при этом пользуются одной-двумя функциями, в то время как их намного больше! Кто из вас пользовался функцией поддержания температуры в микроволновке? А она есть почти в каждой.
Однажды я решил почитать документацию к различным классам Android framework. Пробежался по основным классам: View, Activity, Fragment, Application, — и меня очень заинтересовал метод Application.registerActivityLifecycleCallbacks() и интерфейс ActivityLifecycleCallbacks. Из примеров его использования в интернете не нашлось ничего лучше, чем логирование жизненного цикла Activity. Тогда я начал сам экспериментировать с ним, и теперь мы в Яндекс.Деньгах активно используем его при решении целого спектра задач, связанных с воздействием на объекты Activity снаружи.
Посмотрите на этот интерфейс, вот как он выглядел, когда появился в API 14:
Начиная с API 29 в него добавили несколько новых методов
Возможно, этому интерфейсу уделяют так мало внимания, потому что он появился только в Android 4.0 ICS. А зря, ведь он позволяет нативно делать очень интересную вещь: воздействовать на все объекты Activity снаружи. Но об этом позже, а сначала внимательнее посмотрим на методы.
Каждый метод отображает аналогичный метод жизненного цикла Activity и вызывается в тот момент, когда метод срабатывает на какой-либо Activity в приложении. То есть если приложение запускается с MainActivity, то первым мы получим вызов ActivityLifecycleCallback.onActivityCreated(MainActivity, null).
Отлично, но как это работает? Тут никакой магии: Activity сами сообщают о том, в каком они состоянии. Вот кусочек кода из Activity.onCreate():
Это выглядит так, как если бы мы сами сделали BaseActivity. Только коллеги из Android сделали это за нас, еще и обязали всех этим пользоваться. И это очень даже хорошо!
В API 29 эти методы работают почти так же, но их Pre- и Post-копии честно вызываются до и после конкретных методов. Вероятно, теперь этим управляет ActivityManager, но это только мои догадки, потому что я не углублялся в исходники достаточно, чтобы это выяснить.
Как заставить ActivityLifecycleCallbacks работать?
Как и все callback, сначала их надо зарегистрировать. Мы регистрируем все ActivityLifecycleCallbacks в Application.onCreate(), таким образом получаем информацию обо всех Activity и возможность ими управлять.
Небольшое отступление: начиная с API 29 ActivityLifecycleCallbacks можно зарегистрировать еще и изнутри Activity. Это будет локальный callback, который работает только для этого Activity.
Вот и все. Но это вы можете найти, просто введя название ActivityLifecycleCallbacks в строку поисковика. Там будет много примеров про логирование жизненного цикла Activity, но разве это интересно? У Activity много публичных методов (около 400), и все это можно использовать для того, чтобы делать много интересных и полезных вещей.
Что с этим можно сделать?
А что вы хотите? Хотите динамически менять тему во всех Activity в приложении? Пожалуйста: метод setTheme() — публичный, а значит, его можно вызывать из ActivityLifecycleCallback:
Повторяйте этот трюк ТОЛЬКО дома
Какие-то Activity из подключенных библиотек могут использовать свои кастомные темы. Поэтому проверьте пакет или любой другой признак, по которому можно определить, что тему этой Activity можно безопасно менять. Например, проверяем пакет так (по-котлиновски =)):
Пример не работает? Возможно, вы забыли зарегистрировать ThemeCallback в Application или Application в AndroidManifest.
Хотите еще интересный пример? Можно показывать диалоги на любой Activity в приложении.
Повторяйте этот трюк ТОЛЬКО дома
Конечно же, не стоит показывать диалог на каждом экране — наши пользователи не будут нас любить за такое. Но иногда может быть полезно показать что-то такое на каких-то конкретных экранах.
А вот еще кейс: что, если нам надо запустить Activity Тут все просто: Activity.startActivity() — и погнали. Но что делать, если нам надо дождаться результата после вызова Activity.startActivityForResult()? У меня есть один рецепт:
В примере мы просто закидываем Fragment, который запускает Activity и получает результат, а потом делегирует его обработку нам. Будьте осторожны: тут мы проверяем, что наша Activity является AppCompatActivity, что может привести бесконечному циклу. Используйте другие условия.
Усложним примеры. До этого момента мы использовали только те методы, которые уже есть в Activity. Как насчет того, чтобы добавить свои? Допустим, мы хотим отправлять аналитику об открытии экрана. При этом у наших экранов свои имена. Как решить эту задачу? Очень просто. Создадим интерфейс Screen, который сможет отдавать имя экрана:
Теперь имплементируем его в нужных Activity:
После этого натравим на такие Activity специальные ActivityLifecycleCallback’и:
Видите? Мы просто проверяем интерфейс и, если он реализован, отправляем аналитику.
Повторим для закрепления. Что делать, если надо прокидывать еще и какие-то параметры? Расширим интерфейс:
Имплементируем:
Отправляем:
Но это все еще легко. Все это было только ради того, чтобы подвести вас к по-настоящему интересной теме: нативное внедрение зависимостей. Да, у нас есть Dagger, Koin, Guice, Kodein и прочее. Но на небольших проектах они избыточны. Но у меня есть решение… Угадайте какое?
Допустим, у нас есть некоторый инструмент, вроде такого:
Закроем его интерфейсом, как взрослые программисты:
А теперь немного уличной магии от ActivityLifecycleCallbacks: мы создадим интерфейс для внедрения этой зависимости, реализуем его в нужных Activity, а с помощью ActivityLifecycleCallbacks найдем его и внедрим реализацию CoolToolImpl.
Не забудьте зарегистрировать InjectingLifecycleCallbacks в вашем Application, запускайте — и все работает.
И не забудьте протестировать:
Но на больших проектах такой подход будет плохо масштабироваться, поэтому я не собираюсь отбирать ни у кого DI-фреймворки. Куда лучше объединить усилия и достигнуть синергии. Покажу на примере Dagger2. Если у вас в проекте есть какая-то базовая Activity, которая делает что-то вроде AndroidInjection.inject(this), то пора ее выкинуть. Вместо этого сделаем следующее:
- по инструкции внедряем DispatchingAndroidInjector в Application;
- создаем ActivityLifecycleCallbacks, который вызывает DispatchingAndroidInjector.maybeInject() на каждой Activity;
- регистрируем ActivityLifecycleCallbacks в Application.
И такого же эффекта можно добиться с другими DI-фреймворками. Попробуйте и напишите в комментариях, что получилось.
Подведем итоги
ActivityLifecycleCallbacks — это недооцененный, мощный инструмент. Попробуйте какой-нибудь из этих примеров, и пусть они помогут вам в ваших проектах так же, как помогают Яндекс.Деньгам делать наши приложения лучше.
Originally published at https://medium.com/