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
Activityto 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@e38449And 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@f501372Dagger 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.MyLifecycleTrackerObserving 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