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