Slim down your Android components with LifecycleObserver
November 17, 2022
One of the main challenges in Android development is keeping your Android components smaller. We’ve all seen (or even worked on) apps where Activities, Fragment and Application classes are too big (aka God Activity). This post is a reminder that addressing this problem is both important and easy to do.
LifecycleObserver
LifecycleObserver
is available to you if you use any lifecycle Jetpack library (package starts with androidx.lifecycle.*
)
Let’s create an observer and set it up to follow a particular lifecycle. For instance, if you are concerned that your Activity
is getting too big, let’s observe your Activity
’s lifecycle in a separate class.
Multiple observers can be set up per
Activity
to keep things clean and organized.Some observers can be re-used by several Android components.
To create a lifecycle-aware custom observer and track a limited number of lifecycle events, implement a DefaultLifecycleObserver
which defines empty lifecycle callbacks and overwrite only those callbacks you are interested in.
For instance, we will track onStart
and onStop
lifecycle callbacks of our Activity
:
class MyLifecycleTracker : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { super.onStart(owner) println("onStart: $owner") } override fun onStop(owner: LifecycleOwner) { super.onStop(owner) println("onStop: $owner") }
Observing Activity
The new observer will register to observe the lifecycle of our MainActivity
by using lifecycle.addObserver()
Now you no longer need to override onStart
and onStop
of the activity to follow its lifecycle by our tracker.
class MainActivity : AppCompatActivity() { @Inject lateinit var myLifecycleTracker: MyLifecycleTracker override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) lifecycle.addObserver(myLifecycleTracker) } }
Now when your MainActivity
starts and stops you will see these log entries respectively:
onStart: io.valueof.lifecycleobserverdemo.MainActivity@e38449 onStop: io.valueof.lifecycleobserverdemo.MainActivity@e38449
Note that there is no need to explicitly remove the observer. According to Jose Alcerreca here: “that's the whole point of the new lifecycle-aware components, no need to unsubscribe/remove observers.”
override fun onDestroy() { // no need to do this lifecycle.removeObserver(myLifecycleTracker) super.onDestroy() }
Observing Fragment
You can observe your MainFragment
’s lifecycle in a similar way:
class MainFragment : Fragment(R.layout.fragment_main) { @Inject lateinit var myLifecycleTracker: MyLifecycleTracker override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(myLifecycleTracker) } }
And if you want to observe Fragment’s view lifecycle, add observer to the viewLifecycleOwner.lifecycle
instead of lifecycle
class MainFragment : Fragment(R.layout.fragment_main) { @Inject lateinit var myLifecycleTracker: MyLifecycleTracker override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(myLifecycleTracker) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { viewLifecycleOwner.lifecycle.addObserver(myLifecycleTracker) return super.onCreateView(inflater, container, savedInstanceState) } }
Note that you cannot start observing the view lifecycle in Fragment’s onCreate
since at that point the view lifecycle owner has not been created.
Running the app again, we see that our Activity
’s and Fragment
’s onStart
are being observed by MyLifecycleTracker
onStart: MainFragment{9820c36} (64773e67-d0ab-4818-9fd0-20732d59aa0a id=0x7f08007c) onStart: androidx.fragment.app.FragmentViewLifecycleOwner@5af828 onStart: io.valueof.lifecycleobserverdemo.MainActivity@e38449
And when the Activity
is stopped, we can track that too:
onStop: io.valueof.lifecycleobserverdemo.MainActivity@e38449 onStop: androidx.fragment.app.FragmentViewLifecycleOwner@5af828 onStop: MainFragment{9820c36} (64773e67-d0ab-4818-9fd0-20732d59aa0a id=0x7f08007c)
Observing Process (Application)
We can also hook into your Application
’s lifecycle to offload some of the processing it has to do especially when initializing the app.
class LifecycleObserverDemoApp : Application() { @Inject lateinit var myAppLifecycleTracker: MyAppLifecycleTracker override fun onCreate() { super.onCreate() ProcessLifecycleOwner .get() .lifecycle .addObserver(myAppLifecycleTracker) } }
Now when our Application with its Activity and Fragment starts, we get the following logs:
onStart: MainFragment{9820c36} (5735649a-585a-4612-88c5-890ccf327339 id=0x7f08007c) onStart: androidx.fragment.app.FragmentViewLifecycleOwner@5af828 onStart: androidx.lifecycle.ProcessLifecycleOwner@f501372 onStart: io.valueof.lifecycleobserverdemo.MainActivity@e38449
And when it stops (becomes invisible), we get the following logs:
onStop: io.valueof.lifecycleobserverdemo.MainActivity@e38449 onStop: androidx.fragment.app.FragmentViewLifecycleOwner@5af828 onStop: MainFragment{9820c36} (5735649a-585a-4612-88c5-890ccf327339 id=0x7f08007c) onStop: androidx.lifecycle.ProcessLifecycleOwner@f501372
Dagger and Hilt Scoping
One of the benefits of having our custom lifecycle-aware component is that it can be injected into a ViewModel
as well:
@HiltViewModel class MainViewModel @Inject constructor( private val myLifecycleTracker: MyLifecycleTracker ) : ViewModel()
Without annotating MyLifecycleTracker
with a Hilt scoping annotation, a new instance of it will be created every time our ViewModel is created. If you’d like to maintain the same instance of our tracker across its lifecycle owner Activity’s configuration changes, annotate it with @ActivityRetainedScoped
@ActivityRetainedScoped class MyLifecycleTracker @Inject constructor() : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { super.onStart(owner) println("onStart: $owner") } override fun onStop(owner: LifecycleOwner) { super.onStop(owner) println("onStop: $owner") } }
It is still a valid injectable dependency for our MainViewModel
since Activity
-scoped ViewModels also survive Activity configuration changes.
Now our Fragment
can observe the ViewModel
and the Fragment
’s lifecycle can be followed by MyLifecycleTracker
:
@AndroidEntryPoint class MainFragment : Fragment(R.layout.fragment_main) { @Inject lateinit var myLifecycleTracker: MyLifecycleTracker private val viewModel by viewModels<MainViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(myLifecycleTracker) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { viewLifecycleOwner.lifecycle.addObserver(myLifecycleTracker) return super.onCreateView(inflater, container, savedInstanceState) } }
Also, you may have noticed that I created a separate Tracker to observe Application
’s lifecycle:
@Singleton class MyAppLifecycleTracker @Inject constructor() : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { super.onStart(owner) println("onStart: $owner") } override fun onStop(owner: LifecycleOwner) { super.onStop(owner) println("onStop: $owner") } }
It is so that this Tracker can have a different Hilt scope (@Singleton
). It would not make sense to have the implicitly Singleton
-scoped Process/Application observed by a short-lived @ActivityRetainedScoped
component and Hilt would not let us start the app with an Dagger/IncompatibleScopedBinding
with error message:
error: [Dagger/IncompatiblyScopedBindings] io.valueof.lifecycleobserverdemo.LifecycleObserverDemoApp_HiltComponents.SingletonC scoped with @Singleton may not reference bindings with different scopes: public abstract static class SingletonC implements FragmentGetContextFix.FragmentGetContextFixEntryPoint, ^ @dagger.hilt.android.scopes.ActivityRetainedScoped class io.valueof.lifecycleobserverdemo.util.MyLifecycleTracker
Observing with LifecycleEventObserver
Our Tracker can be written to react to different lifecycle state changes in a different way as well by using LifecycleEventObserver
:
@ActivityRetainedScoped class MyLifecycleTracker @Inject constructor() : LifecycleEventObserver { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { when (event) { Lifecycle.Event.ON_CREATE -> println("onCreate event: $source") Lifecycle.Event.ON_START -> println("onStart event: $source") Lifecycle.Event.ON_RESUME -> println("onResume event: $source") Lifecycle.Event.ON_PAUSE -> println("onPause event: $source") Lifecycle.Event.ON_STOP -> println("onStop event: $source") Lifecycle.Event.ON_DESTROY -> println("onDestroy event: $source") Lifecycle.Event.ON_ANY -> println("onAny event: $source") } } }
This is useful if you want to make sure you handle all state changes.
Note that if you have a lifecycle-aware component that implements both DefaultLifecycleObserver
and LifecycleEventObserver
, the callbacks from DefaultLifecycleObserver
will be executed before those from LifecycleEventObserver
Hope this post was helpful and going forward your Android components are going to have fewer lines of code ;)
The source code for this post can be found at https://github.com/jshvarts/LifecycleObserverDemo