Раскладываем на части 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()             )         } }

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

ссылка на оригинал статьи https://habr.com/ru/company/yamoney/blog/492272/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *