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.

Cristian Garrido

November 16, 2020
Tweet

More Decks by Cristian Garrido

Other Decks in Programming

Transcript

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

    with MVVM • Introducing Saved State module for ViewModel • Abstracting factory injection
  2. 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)
  3. 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)
  4. 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)
  5. Fragment // From FragmentManager.moveToState if (newState > Fragment.CREATED) { fragmentStateManager.createView(mContainer);

    fragmentStateManager.activityCreated(); fragmentStateManager.restoreViewState(); }
  6. onSaveInstanceState • Save view hierarchy “recursively” • State is unique

    for each UI components • Stable behaviour since API 28 (Pie)
  7. 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().
  8. Activity.intent & Fragment.arguments • Not managed by onSaveInstanceState • Can

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

    • onSaveInstanceState() • Local persistance
  10. Testing • No SDK or library to make automated test

    • Unit testing only inputs and outputs • Manual testing
  11. 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.
  12. 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.
  13. 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.
  14. 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/
  15. Planning • Step 1: Create a basic ViewModel implementation •

    Step 2: Make a implementation to survive the process kill
  16. 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() } } }
  17. 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() } } }
  18. 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>) = … }
  19. 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>) = … }
  20. 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) } }
  21. 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) } }
  22. 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) } }
  23. 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)) } ... }
  24. 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)) } ... }
  25. 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)) } ... }
  26. Two reasons to do not do this • TransactionTooLargeException •

    ViewModel is not aware of the restoration
  27. 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) } ... }
  28. class MainViewModel(...) : ViewModel() { fun onSaveInstanceState(outState: Bundle) { outState.putParcelableArrayList("awesome",songResults.value)

    } fun onViewStateRestored(savedInstanceState: Bundle?) { _songResults.value = state?.getParcelableArrayList("awesome") } }
  29. 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) } }
  30. 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) } }
  31. 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) } }
  32. 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) } }
  33. 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) } }
  34. class MainViewModel(private val instanceState: Bundle, …) : ViewModel() { init

    { instanceState.getString("name") ?.let(::search) } }
  35. 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) } }
  36. public class ComponentActivity extends androidx.core.app.ComponentActivity implements LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, OnBackPressedDispatcherOwner

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

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

    … mSavedStateRegistryController.performRestore(savedInstanceState); } protected void onSaveInstanceState(@NonNull Bundle outState) { … mSavedStateRegistryController.performSave(outState); }
  39. 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) } }
  40. 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) } }
  41. 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) } }
  42. 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) } }
  43. 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) } }
  44. 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) } }
  45. 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) } }
  46. class MainViewModelFactory(private val instanceState: Bundle?,…) : ViewModelProvider.Factory { override fun

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

    <T : ViewModel?> create(modelClass: Class<T>): T { return MainViewModel(instanceState ?: Bundle(),…) as T } }
  48. 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 } }
  49. 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 } }
  50. 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 } }
  51. @Module interface MainActivityModule { @ContributesAndroidInjector fun injectorForMainActivity(): MainActivity @ContributesAndroidInjector(modules =

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

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

    [MainFragmentModule::class]) fun injectorForMainFragment(): MainFragment } @Module interface MainFragmentModule { @Binds fun bindSavedStateRegistryOwner( fragment: MainFragment ) : SavedStateRegistryOwner }
  54. 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
  55. 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
  56. 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 } } } }
  57. class MainViewModel @AssistedInject constructor( private val repository: SongRepository, @Assisted private

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

    val handle: SavedStateHandle ) : ViewModel() { … @AssistedInject.Factory interface AssistedFactory: ViewModelAssistedFactory<MainViewModel> }
  59. 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 }
  60. 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 }
  61. class MainFragment : DaggerFragment() { @Inject lateinit var factory :

    ViewModelFactory<MainViewModel, MainViewModel.AssistedFactory> private val viewModel: MainViewModel by viewModels { factory } }
  62. class MainViewModel @AssistedInject constructor( private val repository: SongRepository, @Assisted private

    val handle: SavedStateHandle ) : ViewModel() { … @AssistedInject.Factory interface AssistedFactory: ViewModelAssistedFactory<MainViewModel> }