Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Managing android process instance state.

Managing android process instance state.

Android development challenges are mainly managing the lifecycle of SDK components.
One of them is persisting the state of UI when configuration changes happen. ViewModel + LiveData component solves this.
However, It stills a problem on how to recover the state after a system kill process of our app.

In this talk, you will see how to manage this and why you should care.
Given some insights into how lifecycles work, we are going to compare some homemade ways of solutions against the saved state module for ViewModel from Jetpack.
And finally, how to integrate this in a painless way using Dagger.

C0e29dade6ec33081689ac135a269b45?s=128

Cristian Garrido

November 16, 2020
Tweet

Transcript

  1. With architecture components Managing process instance state Cristian Garrido

  2. Agenda • Lifecycle basics • Understanding onSaveInstanceState • Basic implementation

    with MVVM • Introducing Saved State module for ViewModel • Abstracting factory injection
  3. Lifecycle basics

  4. • https://github.com/xxv/android-lifecycle • https://github.com/JoseAlcerreca/android-lifecycles

  5. • Configuration changes • Process Lifecycle

  6. Configuration changes • Screen rotation • Langage changes • Window

    size changes • Etc…
  7. Screen rotation Activity.onCreate state=null Fragment.onAttach Fragment.onCreated state=null Activity.onStart Fragment.onViewCreated state=null

    Fragment.onActivityCreated state=null Fragment.onStart Activity.onResume Fragment.onResume
  8. Screen rotation Activity.onPause Fragment.onPause Activity.onStop Fragment.onStop Activity.onSaveInstanceState state=Bundle[…] Fragment.onSaveInstanceState state=Bundle[…]

    Activity.onDestroyed Fragment.onViewDestroyed Fragment.onDestroy Fragment.onDetach
  9. Screen rotation Activity.onCreated state=Bundle[…] Fragment.onAttach Fragment.onCreate state=Bundle[…] Activity.onStart Fragment.onViewCreated state=Bundle[…]

    Fragment.onActivityCreated state=Bundle[…] Fragment.onStart Activity.onResume Fragment.onResume
  10. Process Lifecycle • Foreground activity • Visible activity • Background

    activity • Empty Process
  11. Process Kill Activity.onCreate state=null Fragment.onAttach Fragment.onCreated state=null Activity.onStart Fragment.onViewCreated state=null

    Fragment.onActivityCreated state=null Fragment.onStart Activity.onResume Fragment.onResume
  12. Process Kill Activity.onPause Fragment.onPause Activity.onStop Fragment.onStop Activity.onSaveInstanceState state=Bundle[…] Fragment.onSaveInstanceState state=Bundle[…]

  13. Process Kill Activity.onPause Fragment.onPause Activity.onStop Fragment.onStop Activity.onSaveInstanceState state=Bundle[…] Fragment.onSaveInstanceState state=Bundle[…]

  14. Process Kill Activity.onCreated state=Bundle[…] Fragment.onAttach Fragment.onCreate state=Bundle[…] Activity.onStart Fragment.onViewCreated state=Bundle[…]

    Fragment.onActivityCreated state=Bundle[…] Fragment.onStart Activity.onResume Fragment.onResume
  15. Activities and singletons

  16. How do we add info to the instance state?

  17. override fun onCreate(savedInstanceState: Bundle?) override fun onSaveInstanceState(outState: Bundle) override fun

    onRestoreInstanceState(savedInstanceState: Bundle) Activity
  18. Fragment override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) override fun onActivityCreated(savedInstanceState: Bundle?) override fun onViewStateRestored(savedInstanceState: Bundle?) override fun onSaveInstanceState(outState: Bundle)
  19. Fragment.viewOwnerLifecycle override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) override fun onActivityCreated(savedInstanceState: Bundle?) override fun onViewStateRestored(savedInstanceState: Bundle?) override fun onSaveInstanceState(outState: Bundle)
  20. Fragment override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) override fun onActivityCreated(savedInstanceState: Bundle?) override fun onViewStateRestored(savedInstanceState: Bundle?) override fun onSaveInstanceState(outState: Bundle)
  21. Fragment override fun onActivityCreated(savedInstanceState: Bundle?) override fun onViewStateRestored(savedInstanceState: Bundle?) override

    fun onSaveInstanceState(outState: Bundle)
  22. Fragment override fun onActivityCreated(savedInstanceState: Bundle?) override fun onViewStateRestored(savedInstanceState: Bundle?) override

    fun onSaveInstanceState(outState: Bundle)
  23. Fragment // From FragmentManager.moveToState if (newState > Fragment.CREATED) { fragmentStateManager.createView(mContainer);

    fragmentStateManager.activityCreated(); fragmentStateManager.restoreViewState(); }
  24. onSaveInstanceState, what does it actually do for us ?

  25. onSaveInstanceState • Save view hierarchy “recursively” • State is unique

    for each UI components • Stable behaviour since API 28 (Pie)
  26. onSaveInstanceState • Save view hierarchy “recursively” • State is unique

    for each UI components • Stable behaviour since API 28 (Pie) • Since Pie: After onStop() • Previous Pie: Before onStop() no guarantees about it will occur before or after onPause().
  27. onSaveInstanceState outState=Bundle[{ android:viewHierarchyState=Bundle[{ android:views={ 16908290=android.view.AbsSavedState$1@456a31, 2131165223=androidx.appcompat.widget.Toolbar$SavedState@6cf5516, 2131165225=android.view.AbsSavedState$1@456a31, 2131165231=android.view.AbsSavedState$1@456a31, 2131165257=android.view.AbsSavedState$1@456a31, 2131165262=android.view.AbsSavedState$1@456a31

    } }], android:lastAutofillId=1073741824, android:fragments=android.app.FragmentManagerState@7381e97 }]
  28. Activity.intent & Fragment.arguments • Not managed by onSaveInstanceState • Can

    survive configuration changes and process kill. • But only if these were started with.
  29. Managing UI state: divide and conquer

  30. Managing UI state: divide and conquer

  31. Managing UI state: divide and conquer • ViewModel + LiveData

    • onSaveInstanceState() • Local persistance
  32. None
  33. None
  34. None
  35. None
  36. None
  37. https://developer.android.com/topic/libraries/architecture/saving-states#options_for_preserving_ui_state

  38. How to test a process kill

  39. Testing • No SDK or library to make automated test

    • Unit testing only inputs and outputs • Manual testing
  40. Don’t keep activities option This option make automatically destroy activities

    that passe to a background state. Nevertheless, The process tied to your application still alive.
  41. Killing your app through Logcat Try it out: • Send

    application in background with HOME button • click Terminate application button on Logcat tab in Android Studio • then re-launch the app from the launcher. Note: In Android Studio 4.0, the Terminate button issues am force-stop, and so we need to use am kill variant.
  42. Killing your app with ADB Try it out: • Send

    application in background with HOME button • Execute: $ adb shell am kill <your package name> • Then re-launch the app from the launcher.
  43. Killing your with Venom Send a exitProcess(0) in a new

    activity. Making the old activity stack regenerate by itself. https://www.reddit.com/r/androiddev/comments/g2cdid/
  44. Playground time!

  45. Target • Make a Fragment UI state restorable after a

    kill process.
  46. Planning • Step 1: Create a basic ViewModel implementation •

    Step 2: Make a implementation to survive the process kill
  47. • Step 1: Create a basic implementation

  48. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .replace(R.id.container, MainFragment.newInstance()) .commitNow() } } }
  49. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .replace(R.id.container, MainFragment.newInstance()) .commitNow() } } }
  50. class MainFragment : Fragment() { override fun onActivityCreated(savedInstanceState: Bundle?) {

    super.onActivityCreated(savedInstanceState) viewModel.songResults.observeNotNull(viewLifecycleOwner) { bindSongs(it) } } override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) searchButton.setOnClickListener { viewModel.search(searchField.text.toString()) } } private fun bindSongs(songs: List<Song>) = … }
  51. class MainFragment : Fragment() { override fun onActivityCreated(savedInstanceState: Bundle?) {

    super.onActivityCreated(savedInstanceState) viewModel.songResults.observeNotNull(viewLifecycleOwner) { bindSongs(it) } } override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) searchButton.setOnClickListener { viewModel.search(searchField.text.toString()) } } private fun bindSongs(songs: List<Song>) = … }
  52. class MainViewModel( private val repository: SongRepository ) : ViewModel() {

    private val _songResults = MutableLiveData<List<Song>>() val songResults: LiveData<List<Song>> get() = _songResults fun search(name: String) { _songResults.value = repository.findSongs(name) } }
  53. class MainViewModel( private val repository: SongRepository ) : ViewModel() {

    private val _songResults = MutableLiveData<List<Song>>() val songResults: LiveData<List<Song>> get() = _songResults fun search(name: String) { _songResults.value = repository.findSongs(name) } }
  54. class MainViewModel( private val repository: SongRepository ) : ViewModel() {

    private val _songResults = MutableLiveData<List<Song>>() val songResults: LiveData<List<Song>> get() = _songResults fun search(name: String) { _songResults.value = repository.findSongs(name) } }
  55. It survives configuration changes

  56. It does not survives process kill , yet…

  57. We need to persist this using the instance state.

  58. • Step 2: Make a implementation to survive the process

    kill
  59. class MainFragment : Fragment() { ... override fun onActivityCreated(savedInstanceState: Bundle?)

    override fun onSaveInstanceState(outState: Bundle) ... }
  60. class MainFragment : Fragment() { ... override fun onActivityCreated(savedInstanceState: Bundle?)

    { super.onActivityCreated(savedInstanceState) savedInstanceState?.let { bindSongs(it.getParcelableArrayList<Song>("awesome")) } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) val results = viewModel.songResults.value outState.putParcelableArrayList("awesome", results)) } ... }
  61. class MainFragment : Fragment() { ... override fun onActivityCreated(savedInstanceState: Bundle?)

    { super.onActivityCreated(savedInstanceState) savedInstanceState?.let { bindSongs(it.getParcelableArrayList<Song>("awesome")) } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) val results = viewModel.songResults.value outState.putParcelableArrayList("awesome", results)) } ... }
  62. class MainFragment : Fragment() { ... override fun onActivityCreated(savedInstanceState: Bundle?)

    { super.onActivityCreated(savedInstanceState) savedInstanceState?.let { bindSongs(it.getParcelableArrayList<Song>("awesome")) } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) val results = viewModel.songResults.value outState.putParcelableArrayList("awesome", results)) } ... }
  63. Two reasons to do not do this • TransactionTooLargeException •

    ViewModel is not aware of the restoration
  64. class MainFragment : Fragment() { ... override fun onActivityCreated(savedInstanceState: Bundle?)

    { super.onActivityCreated(savedInstanceState) viewModel.onActivityCreated(savedInstanceState) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) viewModel.onSaveInstanceState(outState) } ... } class MainFragment : Fragment() { ... override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.onActivityCreated(savedInstanceState) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) viewModel.onSaveInstanceState(outState) } ... }
  65. class MainViewModel(...) : ViewModel() { fun onSaveInstanceState(outState: Bundle) { outState.putParcelableArrayList("awesome",songResults.value)

    } fun onViewStateRestored(savedInstanceState: Bundle?) { _songResults.value = state?.getParcelableArrayList("awesome") } }
  66. class MainViewModel(...) : ViewModel() { private val instanceState: Bundle =

    Bundle() fun onSaveInstanceState(outState: Bundle) { outState.putAll(instanceState) } fun onActivityCreated(savedInstanceState: Bundle) { instanceState.putAll(savedInstanceState) instanceState.getString("name") ?.let(::search) } fun search(name: String) { instanceState.putString("name", name) _songResults.value = repository.getSongsByName(name) } }
  67. class MainViewModel(...) : ViewModel() { private val instanceState: Bundle =

    Bundle() fun onSaveInstanceState(outState: Bundle) { outState.putAll(instanceState) } fun onActivityCreated(savedInstanceState: Bundle) { instanceState.putAll(savedInstanceState) instanceState.getString("name") ?.let(::search) } fun search(name: String) { instanceState.putString("name", name) _songResults.value = repository.getSongsByName(name) } }
  68. class MainViewModel(...) : ViewModel() { private val instanceState: Bundle =

    Bundle() fun onSaveInstanceState(outState: Bundle) { outState.putAll(instanceState) } fun onActivityCreated(savedInstanceState: Bundle) { instanceState.putAll(savedInstanceState) instanceState.getString("name") ?.let(::search) } fun search(name: String) { instanceState.putString("name", name) _songResults.value = repository.getSongsByName(name) } }
  69. class MainViewModel(...) : ViewModel() { private val instanceState: Bundle =

    Bundle() fun onSaveInstanceState(outState: Bundle) { outState.putAll(instanceState) } fun onActivityCreated(savedInstanceState: Bundle) { instanceState.putAll(savedInstanceState) instanceState.getString("name") ?.let(::search) } fun search(name: String) { instanceState.putString("name", name) _songResults.value = repository.getSongsByName(name) } }
  70. class MainViewModel(...) : ViewModel() { private val instanceState: Bundle =

    Bundle() fun onSaveInstanceState(outState: Bundle) { outState.putAll(instanceState) } fun onActivityCreated(savedInstanceState: Bundle) { instanceState.putAll(savedInstanceState) instanceState.getString("name") ?.let(::search) } fun search(name: String) { instanceState.putString("name", name) _songResults.value = repository.getSongsByName(name) } }
  71. class MainViewModel(private val instanceState: Bundle, …) : ViewModel() { init

    { instanceState.getString("name") ?.let(::search) } }
  72. class MainViewModel(private val instanceState: Bundle, …) : ViewModel() { init

    { instanceState.getString("name") ?.let(::search) } fun onSaveInstanceState(outState: Bundle) { outState.putAll(instanceState) } fun search(name: String) { instanceState.putString("name", name) _songResults.value = repository.getSongsByName(name) } }
  73. It survives the process kill!!

  74. Introducing Saved State module for ViewModel

  75. None
  76. public class ComponentActivity extends androidx.core.app.ComponentActivity implements LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, OnBackPressedDispatcherOwner

    public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner AppCompat components
  77. AppCompat components public class ComponentActivity extends androidx.core.app.ComponentActivity implements LifecycleOwner, ViewModelStoreOwner,

    SavedStateRegistryOwner, OnBackPressedDispatcherOwner public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner
  78. ComponentActivity / Fragment @Override protected void onCreate(@Nullable Bundle savedInstanceState) {

    … mSavedStateRegistryController.performRestore(savedInstanceState); } protected void onSaveInstanceState(@NonNull Bundle outState) { … mSavedStateRegistryController.performSave(outState); }
  79. class MainViewModel(private val instanceState: Bundle, …) : ViewModel() { init

    { instanceState.getString("name") ?.let(::search) } fun onSaveInstanceState(outState: Bundle) { outState.putAll(instanceState) } fun search(name: String) { instanceState.putString("name", name) _songResults.value = repository.getSongsByName(name) } }
  80. class MainViewModel(private val instanceState: Bundle, …) : ViewModel() { init

    { instanceState.getString("name") ?.let(::search) } fun onSaveInstanceState(outState: Bundle) { outState.putAll(instanceState) } fun search(name: String) { instanceState.putString("name", name) _songResults.value = repository.getSongsByName(name) } }
  81. class MainViewModel(private val handle: SavedStateHandle, …) : ViewModel() { init

    { handle.get<String>("name") ?.let(::search) } fun onSaveInstanceState(outState: Bundle) { outState.putAll(instanceState) } fun search(name: String) { handle.set("name", name) _songResults.value = repository.getSongsByName(name) } }
  82. class MainViewModel(private val handle: SavedStateHandle, …) : ViewModel() { init

    { handle.get<String>("name") ?.let(::search) } fun onSaveInstanceState(outState: Bundle) { outState.putAll(instanceState) } fun search(name: String) { handle.set("name", name) _songResults.value = repository.getSongsByName(name) } }
  83. class MainViewModel(private val handle: SavedStateHandle,…) : ViewModel() { init {

    handle.get<String>("name") ?.let(::search) } fun search(name: String) { handle.set("name", name) _songResults.value = repository.getSongsByName(name) } }
  84. class MainViewModel(private val handle: SavedStateHandle,…) : ViewModel() { init {

    handle.get<String>("name") ?.let(::search) } fun search(name: String) { handle.set("name", name) _songResults.value = repository.getSongsByName(name) } }
  85. class MainViewModel(private val handle: SavedStateHandle,…) : ViewModel() { private var

    name by savedState<String>("name", handle) init { name?.let(::search) } fun search(name: String) { this.name = name _songResults.value = repository.getSongsByName(name) } }
  86. class MainViewModelFactory(private val instanceState: Bundle?,…) : ViewModelProvider.Factory { override fun

    <T : ViewModel?> create(modelClass: Class<T>): T { return MainViewModel(instanceState ?: Bundle(),…) as T } }
  87. class MainViewModelFactory(private val instanceState: Bundle?,…) : ViewModelProvider.Factory { override fun

    <T : ViewModel?> create(modelClass: Class<T>): T { return MainViewModel(instanceState ?: Bundle(),…) as T } }
  88. class MainViewModelFactory(private val owner: SavedStateRegistryOwner,…) : AbstractSavedStateViewModelFactory(owner, null) { override

    fun <T : ViewModel?> create( key: String, modelClass: Class<T>, handle: SavedStateHandle ): T { return MainViewModel(handle,…) as T } }
  89. class MainViewModelFactory(private val owner: SavedStateRegistryOwner,…) : AbstractSavedStateViewModelFactory(owner, null) { override

    fun <T : ViewModel?> create( key: String, modelClass: Class<T>, handle: SavedStateHandle ): T { return MainViewModel(handle,…) as T } }
  90. class MainViewModelFactory(private val owner: SavedStateRegistryOwner,…) : AbstractSavedStateViewModelFactory(owner, null) { override

    fun <T : ViewModel?> create( key: String, modelClass: Class<T>, handle: SavedStateHandle ): T { return MainViewModel(handle,…) as T } }
  91. @Module interface MainActivityModule { @ContributesAndroidInjector fun injectorForMainActivity(): MainActivity @ContributesAndroidInjector(modules =

    [MainFragmentModule::class]) fun injectorForMainFragment(): MainFragment } @Module interface MainFragmentModule { @Binds fun bindSavedStateRegistryOwner( fragment: MainFragment ) : SavedStateRegistryOwner }
  92. @Module interface MainActivityModule { @ContributesAndroidInjector fun injectorForMainActivity(): MainActivity @ContributesAndroidInjector(modules =

    [MainFragmentModule::class]) fun injectorForMainFragment(): MainFragment } @Module interface MainFragmentModule { @Binds fun bindSavedStateRegistryOwner( fragment: MainFragment ) : SavedStateRegistryOwner }
  93. @Module interface MainActivityModule { @ContributesAndroidInjector fun injectorForMainActivity(): MainActivity @ContributesAndroidInjector(modules =

    [MainFragmentModule::class]) fun injectorForMainFragment(): MainFragment } @Module interface MainFragmentModule { @Binds fun bindSavedStateRegistryOwner( fragment: MainFragment ) : SavedStateRegistryOwner }
  94. Recap • UI component has none knowledge how the instance

    state is saved
  95. Recap • UI component has none knowledge how the instance

    state is saved • The instance state is only a map or a variable on the ViewModel
  96. Recap • UI component has none knowledge how the instance

    state is saved • The instance state is only a map on the ViewModel • A lot of code was removed out of fragment and vm • Code for the factory still being needed
  97. Code for the factory still being needed

  98. @AssistedInject

  99. class MainViewModel(private val handle: SavedStateHandle,…) : ViewModel() { …. class

    Factory( private val owner: SavedStateRegistryOwner, defaultArgs: Bundle?, … ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) { override fun <T : ViewModel?> create( key: String, modelClass: Class<T>, handle: SavedStateHandle ): T { return MainViewModel(handle,…) as T } } } }
  100. class MainViewModel @AssistedInject constructor( private val repository: SongRepository, @Assisted private

    val handle: SavedStateHandle ) : ViewModel() { … @AssistedInject.Factory interface AssistedFactory: ViewModelAssistedFactory<MainViewModel> }
  101. class MainViewModel @AssistedInject constructor( private val repository: SongRepository, @Assisted private

    val handle: SavedStateHandle ) : ViewModel() { … @AssistedInject.Factory interface AssistedFactory: ViewModelAssistedFactory<MainViewModel> }
  102. class SavedStateViewModelFactory<VM, AssistedFactory> @Inject constructor( owner: SavedStateRegistryOwner, defaultArgs: Bundle?, private

    val factory: AssistedFactory ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) where VM: ViewModel, AssistedFactory: ViewModelAssistedFactory<VM> { override fun <VM : ViewModel?> create( key: String, modelClass: Class<VM>, handle: SavedStateHandle ): VM = factory.create(handle) as VM }
  103. class SavedStateViewModelFactory<VM, AssistedFactory> @Inject constructor( owner: SavedStateRegistryOwner, defaultArgs: Bundle?, private

    val factory: AssistedFactory ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) where VM: ViewModel, AssistedFactory: ViewModelAssistedFactory<VM> { override fun <VM : ViewModel?> create( key: String, modelClass: Class<VM>, handle: SavedStateHandle ): VM = factory.create(handle) as VM }
  104. class MainFragment : DaggerFragment() { @Inject lateinit var factory :

    ViewModelFactory<MainViewModel, MainViewModel.AssistedFactory> private val viewModel: MainViewModel by viewModels { factory } }
  105. ViewModelFactory<MainViewModel, MainViewModel.AssistedFactory>

  106. class MainViewModel @AssistedInject constructor( private val repository: SongRepository, @Assisted private

    val handle: SavedStateHandle ) : ViewModel() { … @AssistedInject.Factory interface AssistedFactory: ViewModelAssistedFactory<MainViewModel> }
  107. class MainViewModel @ViewModelInject constructor( private val repository: SongRepository, @Assisted private

    val handle: SavedStateHandle ) : ViewModel()
  108. @AndroidEntryPoint class MainFragment : Fragment() { private val viewModel: MainViewModel

    by viewModels() }
  109. Questions? Follow me on twitter @crgarridos Cristian Garrido Code: https://git.io/Je8CL

  110. Thanks!

  111. None