Activity and View Model Lifecycles Demo App

vtsen

Vincent Tsen

Posted on October 14, 2022

Activity and View Model Lifecycles Demo App

Simple app to demonstrate Android lifecycle in Activity and View Model using debug logging

The app has 2 screens (First Screen and Second Screen) implemented using simple compose navigation.

Navigating between screens has no impact on the activity life cycles. However, it may impact View Model lifecycle depend on where you instantiate the ViewModel.

Let's first look at activity lifecycle first.

Implement DefaultLifecycleObserver

This is the official activity lifecycle diagram, indicating when these lifecycle event callbacks are called. For example, before activity goes into CREATED state, onCreate() event callback is called. This applies to the rest of the lifecycle states.

Please note that onCreate() and the rest are lifecycle events and not lifecycle states.

In order to demonstrate the activity lifecycle, you need to implement DefaultLifecycleObserver interface. Then, you override all the functions and print out the different lifecycle states.

class MyLifeCycleObserver(private val name: String) : DefaultLifecycleObserver {

    private val tag = "LifeCycleDebug"

    override fun onCreate(owner: LifecycleOwner) {
        Log.d(tag, "$name: onCreate()")
    }

    override fun onStart(owner: LifecycleOwner) {
        Log.d(tag, "$name: onStart()")
    }

    override fun onResume(owner: LifecycleOwner) {
        Log.d(tag, "$name: onResume()")
    }

    override fun onPause(owner: LifecycleOwner) {
        Log.d(tag, "$name: onPause()")
    }

    override fun onStop(owner: LifecycleOwner) {
        Log.d(tag, "$name: onStop()")
    }

    override fun onDestroy(owner: LifecycleOwner) {
        Log.d(tag, "$name: onDestroy()")
    }
}
Enter fullscreen mode Exit fullscreen mode

In activity, you register this lifecycle observer in onCreate() function.

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        val observer = MyLifeCycleObserver(MainActivity::class.simpleName!!)
        lifecycle.addObserver(observer)

        /*...*/
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

For some reasons, the onRestart() callback is not available in DefaultLifecycleObserver. So you need to also override the onRestart() in your activity.

class MainActivity : ComponentActivity() {
    /*...*/

    override fun onRestart() {
        super.onRestart()

        Log.d(
            "LifeCycleDebug",
            "${MainActivity::class.simpleName!!}: onRestart()")
    }
}
Enter fullscreen mode Exit fullscreen mode

Simulate Activity Loses Focus

In order to demonstrate the current activity lifecycle is paused (loses focus), the current activity needs to go into background, but still visible. To do that, you start a second activity with transparent background.

First, you create this Loses Focus button to start the second activity.

val context = LocalContext.current
/*...*/
DefaultButton(
    text = "Loses Focus",
    onClick = {
        context.startActivity(Intent(context, SecondActivity::class.java))
    }
)
Enter fullscreen mode Exit fullscreen mode

In AndroidManifest.xml, add the second activity with android:theme="@android:style/Theme.Translucent":

<activity
    android:name="com.example.understandlifecyclesdemo.ui.SecondActivity"
    android:exported="true"
    android:theme="@android:style/Theme.Translucent">
</activity>
Enter fullscreen mode Exit fullscreen mode

When the button is clicked, the current activity loses focus. Thus, it goes into PAUSED state.

Activity Lifecycle Summary

Try to play around with different scenarios and investigate the output from Logcat.

Here is the summary of all the different scenarios.

Scenario Activity Lifecycle Event Callbacks
Starts up onCreate() → onStart() → onResume()
Navigate to different screens No transition
Starts second transparent activity onPause()
Press back button (from the transparent activity) onResume()
Rotate screen onPause() → onStop() → onDestroy() → onCreate() → onStart() → onResume()
Press home button onPause() → onStop()
Press square button and select the app onRestart() → onStart() → onResume()
Shut down (press back button) onPause() → onStop() → onDestroy()
Simulate process death (press home button, kill the process manually) onPause() → onStop()

Please note that in process death, onDestroy() event callback is not fired.

To simulate process death, you first need to press the home button to move the activity into background. After that, you kill the process manually. The easiest way to kill the process is using the stop button in Logcat.

Activity_and_ViewModel_Lifecycle_Demo_App_01.png

[Updated - Oct 15, 2022]: The above method does NOT work anymore on Android Studio Dolphin version. See the following article for more details.

More detailed descriptions on each lifecycle state below:

Activity Lifecycle State Description
CREATED Activity is created and NOT visible
STARTED Activity is visible at background
RESUMED Activity is visible at foreground
DESTROYED Activity is exited / shut down by user

Please note that there are no Paused, Stopped and Restarted lifecycle states which I find it a bit confusing. See diagram below.

ON_PAUSE event sends the lifecyle state to STARTED. ON_STOP event sends the lifecyle state to CREATED.

Lifecycle Event Lifecycle State Description
ON_PAUSE STARTED Activity is paused and visible, still in foreground
ON_STOP CREATED Activity is NOT visible, move from foreground to background
N/A RESTARTED Intermediate state between Stopped and Started stages

For complete documentation, you can refer to this official documentation here.

Now, let's look at the View Model lifecycle.

When ViewModel is Created and Destroyed?

There are only 2 lifecycle stages in View Model:

  • ViewModel is created. That is when ViewModel constructor is called.
  • ViewModel is destroyed. That is when ViewModel.onCleared() is called.
class MainViewModel(private val name: String) : ViewModel() {  

    private val tag = "LifeCycleDebug"  

    init {  
        Log.d(tag, "${name}ViewModel: onCreated()")  
    }  

    override fun onCleared() {  
        super.onCleared()  

        Log.d(tag, "${name}ViewModel: onCleared()")  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Since MainViewModel takes in constructor parameter (to differentiate the ViewModel instances), you need to create the MainViewModelFactory

class MainViewModelFactory(private val name: String)  
    : ViewModelProvider.NewInstanceFactory() {  

    override fun <T : ViewModel> create(modelClass: Class<T>): T {  

        if (modelClass.isAssignableFrom(MainViewModel::class.java))  
            return MainViewModel(name) as T  

    throw IllegalArgumentException("Unknown ViewModel class")  
    }  
}
Enter fullscreen mode Exit fullscreen mode

In order to demonstrate this View Model lifecycle, you create the ViewModel in 3 different places:

Create ViewModel in MainScreen()

@Composable  
private fun MainScreen() {  
    //view model store owner belongs to the activity
    val viewModel: MainViewModel = viewModel(
        factory = MainViewModelFactory("MainScreen"))  
    /*...*/
}
Enter fullscreen mode Exit fullscreen mode

Create ViewModel in FirstScreen()

@Composable  
fun FirstScreen(  
    navigateToSecondScreen: () -> Unit  
) {  
    //compose navigation creates a new view model store owner for each destination  
  val viewModel: MainViewModel = viewModel(  
        factory = MainViewModelFactory("FirstScreen"))
  /*...*/
}
Enter fullscreen mode Exit fullscreen mode

Create ViewModel in SecondScreen()

@Composable  
fun SecondScreen(  
    popBackStack: () -> Unit,  
) {  
    //compose navigation creates a new view model store owner for each destination  
  val viewModel: MainViewModel = viewModel(  
        factory = MainViewModelFactory("SecondScreen"))
}        
Enter fullscreen mode Exit fullscreen mode

View Model Lifecycle Summary

Let's investigate the Logcat and see what happens.

Scenario View Model Lifecycle State
Starts up After activity is resumed, main and first screen view models are created
Navigate to Second Screen Second screen view model is created
Pop back to first Screen Second screen view model is destroyed
Rotate the screen No impact to view model lifecycle
Press back and exit the app After activity is destroyed, main and first screen view models are destroyed

ViewModelStoreOwner determines the view model lifecycle. It is set to LocalViewModelStoreOwner.current depends on where you call the viewModel() composable function.

In MainScreen(), before the navigation graph is build, the ViewModelStoreOwner belongs to the activity. In FirstScreen() and SecondScreen(), compose navigation creates a new ViewModelStoreOwner for each screen destination. However, since First Screen is the root/start destination, its lifecycle very much the same as the Main Screen view model, which is tied to the activity lifecycle.

So, after the activity is resumed, both Main and First Screen view models are created. Second Screen view model is created when it is pushed to the stack. When it pops from stack, it is then destroyed.

When the app is shutdown, activity is destroyed. After that, Main and First Screen view models are destroyed. The diagram below summarizes what happens.

Activity_and_ViewModel_Lifecycle_Demo_App_02.png

Conclusion

Activity lifecycle is common and well documented. However, it is not completely clear on view model lifecycle, especially when it will be destroyed.

Before I ran the test on this simple app, I had an impression that when the composable screen is gone off-screen, its view model is destroyed. I thought the view model lifecycle is tied to composable screen.

Well, this is not the case. It is based on the back stack from the navigation. When the screen is added into the back stack, its view model is created. When it is removed from the back stack, its view model is destroyed.

Source Code

GitHub Repository: Demo_UnderstandLifecycles


Originally published at https://vtsen.hashnode.dev.

💖 💪 🙅 🚩
vtsen
Vincent Tsen

Posted on October 14, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related