Раскладываем на части FragmentLifecycleCallbacks

    Привет! Сегодня я продолжаю рассказывать про инструменты, которые почему-то обделили вниманием. В своей предыдущей статье я написал про возможности ActivityLifecycleCallbacks и как их можно применять не только для логирования жизненного цикла. Но кроме Activity есть еще и Fragment, и нам хотелось получить для них подобное поведение.


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



    Что это такое


    Интерфейс наподобие ActivityLifecycleCallbacks, только для Fragment.


    FragmentLifecycleCallbacks
    /**
     * 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 в приложении. И ты уже догадался как, да? Но я все-равно покажу пример.


    Dependency injection
    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’ом все тоже идеально работает.


    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, например, отправлять аналитику.


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

    А какие варианты использования знаешь ты?

    ЮMoney (Яндекс.Деньги)
    Всё о разработке сервисов онлайн-платежей

    Comments 0

    Only users with full accounts can post comments. Log in, please.