ActivityLifecycleCallbacks – a blind spot in the public API

Since childhood, I like to read instructions. I grew up, but it still amazes me how adults are careless about instructions: many of them think that everyone knows, and at the same time use one or two functions, while there are a lot more of them! How many of you used to keep the temperature in the microwave? And she is in almost everyone.

Once I decided to read the documentation for the various classes of the Android framework. I ran through the main classes: View, Activity, Fragment, Application, and I was very interested in the Application.registerActivityLifecycleCallbacks () method and the ActivityLifecycleCallbacks interface. Of the examples of its use on the Internet, nothing was better than logging the Activity life cycle. Then I began to experiment with it myself, and now Yandex.Money actively uses it to solve a whole range of tasks related to the impact of Acltivity objects from the outside.

What are ActivityLifecycleCallbacks?

Look at this interface, here is what it looked like when it appeared in API 14:

public interface ActivityLifecycleCallbacks {
 void onActivityCreated (Activity activity, Bundle savedInstanceState);
void onActivityStarted (Activity activity);
void onActivityResumed (Activity activity);
void onActivityPaused (Activity activity);
 void onActivityStopped (Activity activity);
void onActivitySaveInstanceState (Activity activity, Bundle outState);
void onActivityDestroyed (Activity activity);
}

Starting with API 29, several new methods have been added to it:

public interface ActivityLifecycleCallbacks {
default void onActivityPreCreated (
        @NonNull Activity activity,
        @Nullable Bundle savedInstanceState) {}
void onActivityCreated (
        @NonNull Activity activity,
        @Nullable Bundle savedInstanceState);
 default void onActivityPostCreated (
        @NonNull Activity activity,
        @Nullable Bundle savedInstanceState) {}
 default void onActivityPreStarted (@NonNull Activity activity) {}
 void onActivityStarted (@NonNull Activity activity);
 default void onActivityPostStarted (@NonNull Activity activity) {}
 default void onActivityPreResumed (@NonNull Activity activity) {}
 void onActivityResumed (@NonNull Activity activity);
 default void onActivityPostResumed (@NonNull Activity activity) {}
 default void onActivityPrePaused (@NonNull Activity activity) {}
 void onActivityPaused (@NonNull Activity activity);
 default void onActivityPostPaused (@NonNull Activity activity) {}
 default void onActivityPreStopped (@NonNull Activity activity) {}
 void onActivityStopped (@NonNull Activity activity);
 default void onActivityPostStopped (@NonNull Activity activity) {}
 default void onActivityPreSaveInstanceState (
        @NonNull Activity activity,
        @NonNull Bundle outState) {}
 void onActivitySaveInstanceState (
        @NonNull Activity activity,
        @NonNull Bundle outState);
 default void onActivityPostSaveInstanceState (
        @NonNull Activity activity,
        @NonNull Bundle outState) {}
 default void onActivityPreDestroyed (@NonNull Activity activity) {}
 void onActivityDestroyed (@NonNull Activity activity);
 default void onActivityPostDestroyed (@NonNull Activity activity) {}
}

It is possible that so little attention is paid to this interface, because it appeared only in Android 4.0 ICS. But in vain, because it allows you to natively do a very interesting thing: to affect all Activity objects outside. But more on that later, and first take a closer look at the methods.

Each method displays a similar method of the Activity lifecycle and is called at the moment when the method is triggered on any Activity in the application. That is, if the application starts with MainActivity, then the first we will receive an ActivityLifecycleCallback.onActivityCreated (MainActivity, null) call.

Great, but how does it work? There is no magic here: Activity themselves report on what condition they are in. Here is a piece of code from Activity.onCreate ():

        mFragments.restoreAllState (p, mLastNonConfigurationInstances! = null
? mLastNonConfigurationInstances.fragments: null);
}
mFragments.dispatchCreate ();
getApplication (). dispatchActivityCreated (this, savedInstanceState);
if (mVoiceInteractor! = null) {

It looks as if we ourselves did BaseActivity. Only colleagues from Android did this for us, and also obliged everyone to use it. And this is very good!

In API 29, these methods work almost the same, but their Pre and Post copies are honestly called before and after specific methods. It’s probably now controlled by the ActivityManager, but these are just my guesses, because I didn’t go deep into the sources enough to find out.

How to make ActivityLifecycleCallbacks work?

Like all callbacks, you must first register them. We register all ActivityLifecycleCallbacks in Application.onCreate (), so we get information about all Activity and the ability to manage them.

class MyApplication: Application () {
 override fun onCreate () {
        super.onCreate ()
        registerActivityLifecycleCallbacks (MyCallbacks ())
    }
}

A small digression: starting with API 29, ActivityLifecycleCallbacks can also be registered from within the Activity. This will be a local callback that only works for this Activity.

That's all. But you can find this by simply entering the name ActivityLifecycleCallbacks in the search box. There will be many examples of logging the Activity lifecycle, but is that interesting? Activity has many public methods (about 400), and all this can be used to do many interesting and useful things.

What can be done with this?

And what do you want? Want to dynamically change the theme in all Activity in the application? Please: the setTheme () method is public, which means you can call it from an ActivityLifecycleCallback:

class ThemeCallback (
    @StyleRes val myTheme: Int
): ActivityLifecycleCallbacks {
    override fun onActivityCreated (
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        activity? .setTheme (myTheme)
    }
}

Repeat this trick ONLY at home

Some Activities from the connected libraries can use their custom themes. Therefore, check the package or any other sign by which it can be determined that the theme of this Activity can be safely changed. For example, we check the package like this (in Kotlinovsky =)):

class ThemeCallback (
    @StyleRes val myTheme: Int
): ActivityLifecycleCallbacks {
    override fun onActivityCreated (
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        val myPackage = "my.cool.application"
        activity
            ? .takeIf {it.javaClass.name.startsWith (myPackage)}
            ? .setTheme (myTheme)
    }
}

Does the example not work? You may have forgotten to register ThemeCallback in Application or Application in AndroidManifest.

Want another interesting example? You can show dialogs on any Activity in the application.

class DialogCallback (val dialogFragment: DialogFragment): Application.ActivityLifecycleCallbacks {
    override fun onActivityCreated (
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        if (savedInstanceState == null) {
            val tag = dialogFragment.javaClass.name
            (activity as? AppCompatActivity)
                ? .supportFragmentManager
                ? .also {fragmentManager ->
                    if (fragmentManager.findFragmentByTag (tag) == null) {
                        dialogFragment.show (fragmentManager, tag)
                    }
                }
        }
    }
}

Repeat this trick ONLY at home

Of course, you should not show a dialogue on every screen – our users will not love us for this. But sometimes it can be useful to show something like this on some specific screens.

And here's another case: what if we need to run an Activity Everything is simple: Activity.startActivity () – and we’ve driven it. But what if we need to wait for the result after calling Activity.startActivityForResult ()? I have one recipe:

class StartingActivityCallback: Application.ActivityLifecycleCallbacks {
    override fun onActivityCreated (
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        if (savedInstanceState == null) {
            (activity as? AppCompatActivity)
                ? .supportFragmentManager
                ? .also {fragmentManager ->
                    val startingFragment = findOrCreateFragment (fragmentManager)

                    startingFragment.listener = {resultCode, data ->
                        // handle response here
                    }

                    // start Activity inside StartingFragment
                }
        }
    }

    private fun findOrCreateFragment (
        fragmentManager: FragmentManager
    ): StartingFragment {
        val tag = StartingFragment :: class.java.name
        return fragmentManager
            .findFragmentByTag (tag) as StartingFragment?
                ?: StartingFragment (). Apply {
                    fragmentManager
                        .beginTransaction ()
                        .add (this, tag)
                        .commit ()
                }
    }
}

In the example, we simply drop the Fragment, which starts the Activity and gets the result, and then delegates its processing to us. Be careful: here we check that our Activity is AppCompatActivity, which can lead to an infinite loop. Use other conditions.

Let's complicate the examples. Until that moment, we used only those methods that are already in the Activity. How about adding your own? Suppose we want to send analytics about opening a screen. At the same time, our screens have their own names. How to solve this problem? Very simple. Create a Screen interface that can give the screen name:

interface Screen {
    val screenName: String
}

Now we implement it in the desired Activity:

class NamedActivity: Activity (), Screen {
    override val screenName: String = "First screen"
}

After that we set up special ActivityLifecycleCallback’s for such Activity:

class AnalyticsActivityCallback (
    val sendAnalytics: (String) -> Unit
): Application.ActivityLifecycleCallbacks {
    override fun onActivityCreated (
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        if (savedInstanceState == null) {
            (activity as? Screen) ?. screenName? .let (sendAnalytics)
        }
    }
}

See? We just test the interface and, if implemented, send analytics.

Repeat for fixing. What to do if you need to throw some more parameters? Extend the interface:

interface ScreenWithParameters: Screen {
    val parameters: Map
}

We implement:

class NamedActivity: Activity (), ScreenWithParameters {
    override val screenName: String = "First screen"
    override val parameters: Map = mapOf ("key" to "value")
}

We ship:

class AnalyticsActivityCallback (
    val sendAnalytics: (String, Map?) -> Unit
): Application.ActivityLifecycleCallbacks {
    override fun onActivityCreated (
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        if (savedInstanceState == null) {
            (activity as? Screen) ?. screenName? .let {name ->
                sendAnalytics (
                    name,
                    (activity as? ScreenWithParameters) ?. parameters
                )
            }
        }
    }
}

But it is still easy. All this was just to bring you to a really interesting topic: native dependency injection. Yes, we have Yandex.Money. We have Dagger, Koin, Guice, Kodein and more. But on small projects, they are redundant. But I have a solution … Guess which one?

Let's say we have some tool like this:

class CoolToolImpl {
    val extraInfo = "i am dependency"
}

Close it with the interface, like adult programmers:

interface CoolTool {
    val extraInfo: String
}

class CoolToolImpl: CoolTool {
    override val extraInfo = "i am dependency"
}

And now a little street magic from ActivityLifecycleCallbacks: we will create an interface for implementing this dependency, implement it in the necessary Activities, and using ActivityLifecycleCallbacks we will find it and implement the CoolToolImpl implementation.

interface RequireCoolTool {
    var coolTool: CoolTool
}

class CoolToolActivity: Activity (), RequireCoolTool {
    override lateinit var coolTool: CoolTool
}

class InjectingLifecycleCallbacks: ActivityLifecycleCallbacks {
    override fun onActivityCreated (
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        (activity as? RequireCoolTool) ?. coolTool = CoolToolImpl ()
    }
}

Remember to register InjectingLifecycleCallbacks in your Application, run it and it works.

And don't forget to test:

@RunWith (AndroidJUnit4 :: class)
class DIActivityTest {
    @Test
    fun `should access extraInfo when created` () {
        // prepare
        val mockTool: CoolTool = mock ()
        val application = getApplicationContext()
        application.registerActivityLifecycleCallbacks (
            object: Application.ActivityLifecycleCallbacks {
                override fun onActivityCreated (
                    activity: Activity,
                    savedInstanceState: Bundle?
                ) {
                    (activity as? RequireCoolTool) ?. coolTool = mockTool
                }
            })

        // invoke
        launch(Intent (application, DIActivity :: class.java))

        // assert
        verify (mockTool) .extraInfo
    }
}

But on large projects, this approach will not scale well, so I'm not going to take away any DI frameworks from anyone. Where it is better to combine efforts and achieve synergy. I will show you the example of Dagger2. If you have some basic Activity in the project that does something like AndroidInjection.inject (this), then it's time to throw it away. Instead, do the following:

  1. according to the instructions, we implement DispatchingAndroidInjector in Application;
  2. create an ActivityLifecycleCallbacks that calls DispatchingAndroidInjector.maybeInject () on each Activity;
  3. register ActivityLifecycleCallbacks in Application.
class MyApplication: Application () {
    @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector

    override fun onCreate () {
        super.onCreate ()
        DaggerYourApplicationComponent.create (). Inject (this);
        registerActivityLifecycleCallbacks (
            InjectingLifecycleCallbacks (
                dispatchingAndroidInjector
            )
        )
    }
}

class InjectingLifecycleCallbacks (
    val dispatchingAndroidInjector: DispatchingAndroidInjector
): ActivityLifecycleCallbacks {
    override fun onActivityCreated (
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
dispatchingAndroidInjector.maybeInject (activity)
    }
}

And the same effect can be achieved with other DI frameworks. Try and write in the comments what happened.

To summarize

ActivityLifecycleCallbacks is an underrated, powerful tool. Try one of these examples, and let them help you in your projects in the same way that Yandex.Money helps make our applications better.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *