In almost all Android apps, you might set up many things in onStart() or onCreate(), then tear them all down in onStop() or onDestroy(). For example, you might have animations, music, sensors, or timers that you need to both set up and tear down, and start and stop. If you forget one, that leads to bugs and headaches.

The lifecycle library, which is part of Android Jetpack, simplifies this task. The library is especially useful in cases where you have to track many moving parts, some of which are at different lifecycle states. The library flips around the way lifecycles work: Usually the activity or fragment tells a component (Usually the data layer) what to do when a lifecycle callback occurs. But when you use the lifecycle library, the component itself watches for lifecycle changes, then does what’s needed when those changes happen.

There are three main parts of the lifecycle library:

  • Lifecycle owners, which are the components that have (and “own”) a lifecycle. Activity and Fragment are lifecycle owners. Lifecycle owners implement the LifecycleOwner interface.

Your MainActivity class is already a lifecycle owner through object-oriented inheritance. Notice MainActivity subclasses from AppCompatActivity, which in turn subclasses from FragmentActivity. Since the FragmentActivity superclass implements LifecycleOwner, there’s nothing more you need to do to make your activity lifecycle-aware

  • The Lifecycle class, which holds the actual state of a lifecycle owner and triggers events when lifecycle changes happen.

  • Lifecycle observers, which observe the lifecycle state and perform tasks when the lifecycle changes. Lifecycle observers implement the LifecycleObserver interface.

Consider below example to understand this.

class Timer(lifecycle : Lifecycle) : LifecycleObserver {
    init {
        lifecycle.addObserver(this)
    }

    // The number of seconds counted since the timer started
    var secondsCount = 0

    /**
     * [Handler] is a class meant to process a queue of messages (known as [android.os.Message]s)
     * or actions (known as [Runnable]s)
     */
    private var handler = Handler(Looper.getMainLooper())
    private lateinit var runnable: Runnable


    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun startTimer() {
        // Create the runnable action, which prints out a log and increments the seconds counter
        runnable = Runnable {
            secondsCount++
            Timber.i("Timer is at : $secondsCount")
            // postDelayed re-adds the action to the queue of actions the Handler is cycling
            // through. The delayMillis param tells the handler to run the runnable in
            // 1 second (1000ms)
            handler.postDelayed(runnable, 1000)
        }

        // This is what initially starts the timer
        handler.postDelayed(runnable, 1000)

        // Note that the Thread the handler runs on is determined by a class called Looper.
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun stopTimer() {
        // Removes all pending posts of runnable from the handler''s queue, effectively stopping the
        // timer
        handler.removeCallbacks(runnable)
    }
}

In our example, we are just printing time in logs. This timer class is our observer & it will start/stop printing time as and when the activity state will change. This happens through annotations.

For e.g. @OnLifecycleEvent(Lifecycle.Event.ON_STOP) This tells the system that stop timer should be called when Activities onStop is called.

For this to work, in your Activity, when you’re creating the timer object, just pass the lifecycle

timer = Timer(lifecycle)

That’s it. This will work seamlessly now. To check how callbacks are being triggered, put logs in all Activity methods like onCreate, onStart, onStop, etc

There’s one more thing you should always keep in check. What if you press the home button and android os kills your app in the background. In such cases you loose the state of your app and the timer starts from the beginning.

Let’s learn how to simulate app shutdown

adb shell am kill com.exmaple.android

This command tells any connected devices or emulators to send a STOP message to terminate the process with the com.exmaple.android package name, but only if the app is in the background. Because your app was in the background, nothing shows on the device or emulator screen to indicate that your process has been stopped. In Android Studio, click the Run tab to see the onStop() method called. Click the Logcat tab to see that the onDestroy() callback was never run—your activity simply ended.

However, in all cases, onSaveInstanceState will be called after onStop. So implement it to save timer state.

override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)

   Timber.i("onSaveInstanceState Called")
   outState.putInt(KEY_TIME_SECONDS, desertTimer.secondsCount)
}

and in onCreate

if (savedInstanceState != null) {
   timer.secondsCount = savedInstanceState.getInt(KEY_TIME_SECONDS)
}

Note: If the activity is being re-created, the onRestoreInstanceState() callback is called after onStart(), also with the bundle. Most of the time, you restore the activity state in onCreate(). But because onRestoreInstanceState() is called after onStart(), if you ever need to restore some state after onCreate() is called, you can use onRestoreInstanceState().

Added advantage of doing this is, it also saves your timer state during all configuration changes as well. When a configuration change occurs, Android invokes all the activity lifecycle’s shutdown callbacks. Then Android restarts the activity from scratch, running all the lifecycle startup callbacks.

A configuration change happens when the state of the device changes so radically that the easiest way for the system to resolve the change is to completely shut down and rebuild the activity. For example, if the user changes the device language, the whole layout might need to change to accommodate different text directions. If the user plugs the device into a dock or adds a physical keyboard, the app layout may need to take advantage of a different display size or layout. And if the device orientation changes, for example if the device is rotated from portrait to landscape or back the other way, then the layout may need to change to fit the new orientation.

You use also use the ViewModel class to store and manage UI-related data in a lifecycle-conscious way. The ViewModel class allows data to survive device-configuration changes such as screen rotations and changes to keyboard availability. We will pick this up next.

Thanks for reading!