Pull to refresh
ЮMoney
Всё о разработке сервисов онлайн-платежей

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

Reading time9 min
Views5.4K

Привет! Сегодня я продолжаю рассказывать про инструменты, которые почему-то обделили вниманием. В своей предыдущей статье я написал про возможности 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()
            )
        }
}

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

Tags:
Hubs:
Total votes 11: ↑11 and ↓0+11
Comments0

Articles

Information

Website
jobs.yoomoney.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия
Representative
yooteam