Skip to main content

Android Jetpack – Handling Lifecycles Using Lifecycle Library

 

This tutorial is part of my Android Jetpack tutorials and source can be found here

If you’ve been around in Android for quite a while, you probably know that managing an app’s lifecycle is quite a headache! Have you ever had these moments yourself?

Should I put this code in onStart() or onResume()?

Should I put this code in onPause() or onStop()?

*Runs the app. Wait, why isn’t it called?!

*Moves code to another lifecycle callback

It’s sometimes unpredictable and confusing whether a piece of code should be inside this lifecycle callback or not.

Fortunately, after many years, Android heard our outcries and provided us with a solution – Lifecycle library. 🤩🎉

The android.arch.lifecycle package provides classes and interfaces that let you build lifecycle-aware components—which are components that can automatically adjust their behavior based on the current lifecycle state of an activity or fragment.

 

How Can It Help You?

Here are some of the functionalities that I think are quite useful from this library:

  • Ability to create objects that can observe the lifecycle of an Activity or Fragment and react accordingly.
  • Activities and Fragments are no longer responsible for initializing/destroying components based on it’s state. It’s now the responsibility of the object that you created to initialize/destroy itself based on the lifecycle that you’re object is observing.

 

Let’s Replicate A Problem

The problem that we’re going to replicate is just one of the many lifecycle problems that you will encounter.

1. Create a new project

2. Choose an Empty Activity

3. Create a new interface OnGetUserCallback

interface OnGetUserCallback {
    fun onGetUser(user: String)
}

Just imagine that user is a User object.

4. Create a new class DatabaseRepository.class

class DatabaseRepository(private val executor: Executor) {

    fun getUser(callback: OnGetUserCallback) {
        executor.execute {
            Thread.sleep(10000)
            callback.onGetUser("User Object")
        }
    }
}

We’re going to sleep the thread for 10 seconds to simulate a long operation.

5. Open your activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

6. Create a new fragment UserFragment.class

import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

class UserFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_user, container, false)
    }
}

7. Open your MainActivity.class

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.d("Lifecycle", "onCreate() called")
    }

    override fun onStart() {
        super.onStart()
        Log.d("Lifecycle", "onStart() called")
        val database = DatabaseRepository(Executors.newSingleThreadExecutor())
        database.getUser(object : OnGetUserCallback {
            override fun onGetUser(user: String) {
                Log.d("Lifecycle", "Callback called: $user")

                val userFragment = UserFragment()
                supportFragmentManager.beginTransaction()
                        .add(R.id.fragment_container, userFragment).commit()
            }
        })
    }

    override fun onResume() {
        super.onResume()
        Log.d("Lifecycle", "onResume() called")
    }

    override fun onPause() {
        super.onPause()
        Log.d("Lifecycle", "onPause() called")
    }

    override fun onStop() {
        super.onStop()
        Log.d("Lifecycle", "onStop() called")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("Lifecycle", "onDestroy() called")
    }
}

8. Open Developer Options in Settings of your device/emulator

9. Under Apps, turn ON “Don’t keep activities

10. Run the app and after 2-3 seconds, press the Home button to destroy the activity

Open your Logcat and you should see similar to this:

What did you notice from the logs?

onDestroy() called
Callback called: User Object

The callback in the onStart() method was called even though the Activity is already destroyed!

The app also crashes

Remove the “Lifecycle” keyword from your Logcat’s search bar and select Error from the dropdown and you should see a Stacktrace like this:

07-12 16:23:11.742 9414-9429/com.imakeanapp.lifecyclesample E/AndroidRuntime: FATAL EXCEPTION: pool-1-thread-1
Process: com.imakeanapp.lifecyclesample, PID: 9414
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:2053)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:2079)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:678)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:632)
at com.imakeanapp.lifecyclesample.MainActivity$onStart$1.onGetUser(MainActivity.kt:27)
at com.imakeanapp.lifecyclesample.DatabaseRepository$getUser$1.run(DatabaseRepository.kt:10)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:761)

When a Fragment or AppCompatActivity‘s state is saved via onSaveInstanceState(), it’s UI is considered immutable until ON_START is called. Trying to modify the UI after the state is saved is likely to cause inconsistencies in the navigation state of your application which is why FragmentManager throws an exception if the app runs aFragmentTransaction after state is saved.

Basically, onSaveInstanceState() is called before onDestroy(), the state is now saved and you “modified” the UI when the OnGetUserCallback was called in onStart().

This is just one of the many lifecycle problems that you will encounter as you work with Android.

 

How can we prevent this?

1. Open your DatabaseRepository.class

import android.arch.lifecycle.Lifecycle
import android.arch.lifecycle.LifecycleObserver
import android.util.Log
import java.util.concurrent.Executor

class DatabaseRepository(private val executor: Executor,
                         private val lifecycle: Lifecycle): LifecycleObserver {

    fun getUser(callback: OnGetUserCallback) {
        executor.execute {
            Thread.sleep(10000)
            if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
                callback.onGetUser("User Object")
            } else {
                Log.d("Lifecycle", "Lifecycle is not at least started")
            }
        }
    }
}

2. Open your MainActivity.class

class MainActivity : AppCompatActivity() {

    ...

    override fun onStart() {
        super.onStart()
        Log.d("Lifecycle", "onStart() called")
        // Fragments and Activities in Support Library 26.1.0 and later already implement
        // the LifecycleOwner interface.
        val database = DatabaseRepository(Executors.newSingleThreadExecutor(), lifecycle)
        ...
    }

    ...
}

3. Run the app and after 2-3 seconds, press the Home button to destroy the activity. You should see something like this and it no longer crashed! 🎉

You can finally get the idea that the responsibility of reacting to the lifecycle of an Activity or Fragment is now delegated to a component that observes the lifecycle.

It’s no longer the responsibility of the Activity or Fragment to initialize/destroy your objects. Which cleans up your Activity or Fragment from state-related logic and is now focused with UI logic.

 

Listening to Lifecycle Callbacks

class DatabaseRepository(private val executor: Executor,
                         private val lifecycle: Lifecycle): LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun start() {
        // Do some initialization
    }

    fun getUser(callback: OnGetUserCallback) {
        executor.execute {
            Thread.sleep(10000)
            if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
                callback.onGetUser("User Object")
            } else {
                Log.d("Lifecycle", "Lifecycle is not at least started")
            }
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun stop() {
        // Do some clean up, release resources, etc.
    }
}

Using the @OnLifeCycleEvent(…) annotation, you can listen to lifecycle callbacks of the component that you’re observing.

There are two enumerations to track the lifecycle status for a component:

Event

State

That’s it! Thank you so much for reaching this far! 😊

Checkout Android’s Lifecycle documentation for a more detailed and in-depth details.

Want to learn more about Android Jetpack?

Checkout all my tutorials related to Android Jetpack.

 

If you find this article helpful, you can subscribe below to be updated with new useful articles in the future.