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

Previous
Previous

Compose by example: BoxWithConstraints

Next
Next

Show BottomBar conditionally in Compose