Привет! Сегодня я продолжаю рассказывать про инструменты, которые почему-то обделили вниманием. В своей предыдущей статье я написал про возможности ActivityLifecycleCallbacks и как их можно применять не только для логирования жизненного цикла. Но кроме Activity есть еще и Fragment, и нам хотелось получить для них подобное поведение.
Не долго думая, я открыл поиск по классам в AndroidStudio (Cmd/Ctrl + O) и ввел туда FragmentLifecycleCallbacks. И каково же было мое удивление, когда поиск показал мне FragmentManager.FragmentLifecycleCallbacks. Самые нетерпеливые читатели писали про это в комментариях, поэтому вот продолжение всей этой истории. Скорее под кат!

Что это такое
Интерфейс наподобие ActivityLifecycleCallbacks, только для Fragment.
/** * Callback interface for listening to fragment state changes that happen * within a given FragmentManager. */ public abstract static class FragmentLifecycleCallbacks { /** * Called right before the fragment's {@link Fragment#onAttach(Context)} method is called. * This is a good time to inject any required dependencies or perform other configuration * for the fragment before any of the fragment's lifecycle methods are invoked. * * @param fm Host FragmentManager * @param f Fragment changing state * @param context Context that the Fragment is being attached to */ public void onFragmentPreAttached( @NonNull FragmentManager fm, @NonNull Fragment f, @NonNull Context context) {} /** * Called after the fragment has been attached to its host. Its host will have had * `onAttachFragment` called before this call happens. * * @param fm Host FragmentManager * @param f Fragment changing state * @param context Context that the Fragment was attached to */ public void onFragmentAttached( @NonNull FragmentManager fm, @NonNull Fragment f, @NonNull Context context) {} /** * Called right before the fragment's {@link Fragment#onCreate(Bundle)} method is called. * This is a good time to inject any required dependencies or perform other configuration * for the fragment. * * @param fm Host FragmentManager * @param f Fragment changing state * @param savedInstanceState Saved instance bundle from a previous instance */ public void onFragmentPreCreated( @NonNull FragmentManager fm, @NonNull Fragment f, @Nullable Bundle savedInstanceState) {} /** * Called after the fragment has returned from the FragmentManager's call to * {@link Fragment#onCreate(Bundle)}. This will only happen once for any given * fragment instance, though the fragment may be attached and detached multiple times. * * @param fm Host FragmentManager * @param f Fragment changing state * @param savedInstanceState Saved instance bundle from a previous instance */ public void onFragmentCreated( @NonNull FragmentManager fm, @NonNull Fragment f, @Nullable Bundle savedInstanceState) {} /** * Called after the fragment has returned from the FragmentManager's call to * {@link Fragment#onActivityCreated(Bundle)}. This will only happen once for any given * fragment instance, though the fragment may be attached and detached multiple times. * * @param fm Host FragmentManager * @param f Fragment changing state * @param savedInstanceState Saved instance bundle from a previous instance */ public void onFragmentActivityCreated( @NonNull FragmentManager fm, @NonNull Fragment f, @Nullable Bundle savedInstanceState) {} /** * Called after the fragment has returned a non-null view from the FragmentManager's * request to {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}. * * @param fm Host FragmentManager * @param f Fragment that created and owns the view * @param v View returned by the fragment * @param savedInstanceState Saved instance bundle from a previous instance */ public void onFragmentViewCreated( @NonNull FragmentManager fm, @NonNull Fragment f, @NonNull View v, @Nullable Bundle savedInstanceState) {} /** * Called after the fragment has returned from the FragmentManager's call to * {@link Fragment#onStart()}. * * @param fm Host FragmentManager * @param f Fragment changing state */ public void onFragmentStarted( @NonNull FragmentManager fm, @NonNull Fragment f) {} /** * Called after the fragment has returned from the FragmentManager's call to * {@link Fragment#onResume()}. * * @param fm Host FragmentManager * @param f Fragment changing state */ public void onFragmentResumed( @NonNull FragmentManager fm, @NonNull Fragment f) {} /** * Called after the fragment has returned from the FragmentManager's call to * {@link Fragment#onPause()}. * * @param fm Host FragmentManager * @param f Fragment changing state */ public void onFragmentPaused( @NonNull FragmentManager fm, @NonNull Fragment f) {} /** * Called after the fragment has returned from the FragmentManager's call to * {@link Fragment#onStop()}. * * @param fm Host FragmentManager * @param f Fragment changing state */ public void onFragmentStopped( @NonNull FragmentManager fm, @NonNull Fragment f) {} /** * Called after the fragment has returned from the FragmentManager's call to * {@link Fragment#onSaveInstanceState(Bundle)}. * * @param fm Host FragmentManager * @param f Fragment changing state * @param outState Saved state bundle for the fragment */ public void onFragmentSaveInstanceState( @NonNull FragmentManager fm, @NonNull Fragment f, @NonNull Bundle outState) {} /** * Called after the fragment has returned from the FragmentManager's call to * {@link Fragment#onDestroyView()}. * * @param fm Host FragmentManager * @param f Fragment changing state */ public void onFragmentViewDestroyed( @NonNull FragmentManager fm, @NonNull Fragment f) {} /** * Called after the fragment has returned from the FragmentManager's call to * {@link Fragment#onDestroy()}. * * @param fm Host FragmentManager * @param f Fragment changing state */ public void onFragmentDestroyed( @NonNull FragmentManager fm, @NonNull Fragment f) {} /** * Called after the fragment has returned from the FragmentManager's call to * {@link Fragment#onDetach()}. * * @param fm Host FragmentManager * @param f Fragment changing state */ public void onFragmentDetached( @NonNull FragmentManager fm, @NonNull Fragment f) {} }
В отличие от ActivityLifecycleCallbacks он управляется не самим Fragment, а FragmentManager, что дает ряд преимуществ. Например, у этого интерфейса методы с приставкой Pre-, которые вызываются до соответствующих методов Fragment. А методы без приставки вызываются после того, как сработают эти же методы Fragment.
К тому же FragmentLifecycleCallbacks — это абстрактный класс, а не интерфейс. Думаю, что это для того, чтобы у методов была реализация по умолчанию.
Но перейдем к интересному — как это запустить.
Как зарегистрировать
Чтобы заставить FragmentLifecycleCallbacks работать, его нужно зарегистрировать на FragmentManager. Для этого надо вызвать FragmentManager.registerFragmentLifecycleCallback(), передав в него два параметра: сам callback и флаг — recursive. Флаг показывает, нужно ли применить этот callback только к этому FragmentManager или его надо рекурсивно прокидывать во все childFragmentManager’ы, этого FragmentManager’а и дальше по иерархии.

FragmentLifecycleCallback стоит регистрировать до Activity.onCreate(), иначе мы можем получить не все события, например, при восстановлении состояния.
class FlcExampleActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { supportFragmentManager .registerFragmentLifecycleCallbacks( ExampleFragmentLifecycleCallback(), true ) super.onCreate(savedInstanceState) } } class ExampleFragmentLifecycleCallback : FragmentManager.FragmentLifecycleCallbacks()
Выглядит не очень красиво, и в некоторых ситуациях потребует заводить что-то вроде базовой Activity. Но если ты уже прочитал мою статью про ActivityLifecycleCallbacks, то знаешь, на что базовые Activity отлично заменяются =).
class ActivityFragmentLifecycleCallbacks : Application.ActivityLifecycleCallbacks, FragmentManager.FragmentLifecycleCallbacks() { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { (activity as? FragmentActivity) ?.supportFragmentManager ?.registerFragmentLifecycleCallbacks(this, true) } }
И тут мы получаем потрясающую синергию callback’ов. Благодаря этому решению мы теперь можем дотянуться почти до любого объекта Activity и Fragment, создаваемых в системе. И теперь, когда мы видим все как на ладони, можно заставить всю эту систему работать на нас.
Примеры использования
Сразу про dependency injection: да, теперь можно распространять зависимости по всему приложению, даже если у вас Single Activity Application. Помнишь пример из предыдущей статьи, про RequireCoolTool? То же самое можно сделать для всех Activity и Fragment в приложении. И ты уже догадался как, да? Но я все-равно покажу пример.
interface CoolTool { val extraInfo: String } class CoolToolImpl : CoolTool { override val extraInfo = "i am dependency" } interface RequireCoolTool { var coolTool: CoolTool } class InjectingLifecycleCallbacks : Application.ActivityLifecycleCallbacks, FragmentManager.FragmentLifecycleCallbacks() { private val coolToolImpl = CoolToolImpl() override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { (activity as? RequireCoolTool)?.coolTool = coolToolImpl (activity as? FragmentActivity) ?.supportFragmentManager ?.registerFragmentLifecycleCallbacks(this, true) } override fun onFragmentPreCreated( fm: FragmentManager, f: Fragment, savedInstanceState: Bundle? ) { (f as? RequireCoolTool)?.coolTool = coolToolImpl } } class DIActivity : AppCompatActivity(), RequireCoolTool { override lateinit var coolTool: CoolTool override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(LinearLayout { orientation = LinearLayout.VERTICAL FrameLayout { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f) Text( """ DI example activity CoolTool.extraInfo="${coolTool.extraInfo}" """.trimIndent() ) } FrameLayout { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f) id = R.id.container } }) supportFragmentManager.findFragmentById(R.id.container) ?: run { supportFragmentManager .beginTransaction() .add(R.id.container, DIFragment()) .commit() } } } class DIFragment : Fragment(), RequireCoolTool { override lateinit var coolTool: CoolTool override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = inflater.context.FrameLayout { setBackgroundColor(Color.LTGRAY) Text( """ DI example fragment CoolTool.extraInfo="${coolTool.extraInfo}" """.trimIndent() ) } }
И конечно же с Dagger’ом все тоже идеально работает.
interface DaggerTool { val extraInfo: String } class DaggerToolImpl : DaggerTool { override val extraInfo = "i am dependency" } class DaggerInjectingLifecycleCallbacks( val dispatchingAndroidInjector: DispatchingAndroidInjector<Any> ) : Application.ActivityLifecycleCallbacks, FragmentManager.FragmentLifecycleCallbacks() { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { dispatchingAndroidInjector.maybeInject(activity) (activity as? FragmentActivity) ?.supportFragmentManager ?.registerFragmentLifecycleCallbacks(this, true) } override fun onFragmentPreCreated( fm: FragmentManager, f: Fragment, savedInstanceState: Bundle? ) { dispatchingAndroidInjector.maybeInject(f) } } class DaggerActivity : AppCompatActivity() { @Inject lateinit var daggerTool: DaggerTool override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(LinearLayout { orientation = LinearLayout.VERTICAL FrameLayout { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f) Text( """ Dagger example activity CoolTool.extraInfo="${daggerTool.extraInfo}" """.trimIndent() ) } FrameLayout { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f) id = R.id.container } }) supportFragmentManager.findFragmentById(R.id.container) ?: run { supportFragmentManager .beginTransaction() .add(R.id.container, DIFragment()) .commit() } } } class DaggerFragment : Fragment() { @Inject lateinit var daggerTool: DaggerTool override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = inflater.context.FrameLayout { Text( """ Dagger example fragment DaggerTool.extraInfo="${daggerTool.extraInfo}" """.trimIndent() ) } } @Module class DaggerModule { @Provides fun provideDaggerTool(): DaggerTool { return DaggerToolImpl() } } @Module abstract class DaggerAndroidModule { @ContributesAndroidInjector(modules = [DaggerModule::class]) abstract fun contributeDaggerActivity(): DaggerActivity @ContributesAndroidInjector(modules = [DaggerModule::class]) abstract fun contributeDaggerFragment(): DaggerFragment }
Я думаю, что ты вполне справишься с другими DI-фреймворками, но если не получится, то давай обсудим это в комментариях.
Конечно, можно делать все то же самое, что и с Activity, например, отправлять аналитику.
interface Screen { val screenName: String } interface ScreenWithParameters : Screen { val parameters: Map<String, String> } class AnalyticsCallback( val sendAnalytics: (String, Map<String, String>?) -> Unit ) : Application.ActivityLifecycleCallbacks, FragmentManager.FragmentLifecycleCallbacks() { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { if (savedInstanceState == null) { (activity as? Screen)?.screenName?.let { sendAnalytics( it, (activity as? ScreenWithParameters)?.parameters ) } } } } class AnalyticsActivity : AppCompatActivity(), ScreenWithParameters { override val screenName: String = "First screen" override val parameters: Map<String, String> = mapOf("key" to "value") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(LinearLayout { orientation = android.widget.LinearLayout.VERTICAL FrameLayout { layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f) Text( """ Analytics example see output in Logcat by "Analytics" tag """.trimIndent() ) } FrameLayout { layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f) id = R.id.container } }) with(supportFragmentManager) { findFragmentById(R.id.container) ?: commit { add(R.id.container, AnalyticsFragment()) } } } } class AnalyticsFragment : Fragment(), ScreenWithParameters { override val screenName: String = "Fragment screen" override val parameters: Map<String, String> = mapOf("key" to "value") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = inflater.context.FrameLayout { setBackgroundColor(Color.LTGRAY) Text( """ Analytics example see output in Logcat by "Analytics" tag """.trimIndent() ) } }
А какие варианты использования знаешь ты?
ссылка на оригинал статьи https://habr.com/ru/company/yamoney/blog/492272/
Добавить комментарий