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

Save the state

Save the state

Avatar for Keishin Yokomaku

Keishin Yokomaku

May 26, 2023
Tweet

More Decks by Keishin Yokomaku

Other Decks in Technology

Transcript

  1. Save the state About me ▸ Keishin Yokomaku - @KeithYokoma

    ▸ Giftmall, Inc. ▸ Android App Engineer ▸ https://keithyokoma.dev/ 2 Shibuya.apk #42
  2. Save the state References ▸ Best practices for saving UI

    state on Android 
 https://youtu.be/V-s4z7B_Gnc ▸ Advanced state and side effects in Jetpack Compose 
 https://youtu.be/TbxCz5AljQk 4 Shibuya.apk #42
  3. Save the state Quick run-through: Activity lifecycle 5 Shibuya.apk #42

    source: https://developer.android.com/guide/components/activities/activity-lifecycle
  4. Save the state Quick run-through: Activity lifecycle 6 Shibuya.apk #42

    source: https://developer.android.com/guide/components/activities/activity-lifecycle
  5. Save the state Quick run-through: Activity lifecycle ▸ Rebirthing activities

    ▸ System creates a new activity instance ▸ When this happens ▸ On low memory ▸ Con fi guration changes ▸ Don’t keep activities 7 Shibuya.apk #42 source: https://developer.android.com/guide/components/activities/activity-lifecycle
  6. Save the state Quick run-through: Activity lifecycle 9 Shibuya.apk #42

    source: https://giphy.com/gifs/6qqnulwmAJozb51SCe
  7. Save the state Not saving state: views class SampleActivity :

    ComponentActivity(R.layout.activity_sample) { var count: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val counterText = findViewById<TextView>(R.id.counter) updateCounterText(counterText) findViewById<Button>(R.id.counter_button).setOnClickListener { count++ updateCounterText(counterText) } } private fun updateCounterText(counterText: TextView) { counterText.text = "Count: $count" } } 10 Shibuya.apk #42
  8. Save the state Not saving state: views class SampleActivity :

    ComponentActivity(R.layout.activity_sample) { var count: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val counterText = findViewById<TextView>(R.id.counter) updateCounterText(counterText) findViewById<Button>(R.id.counter_button).setOnClickListener { count++ updateCounterText(counterText) } } private fun updateCounterText(counterText: TextView) { counterText.text = "Count: $count" } } 11 count is 0 when activity instance is created Shibuya.apk #42
  9. Save the state Saving state: views class ViewActivity : ComponentActivity(R.layout.activity_view)

    { var count: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) savedInstanceState?.getInt("count")?.let { count = it } // … } override fun onSaveInstanceState(outState: Bundle) { outState.putInt("count", count) super.onSaveInstanceState(outState) } } 13 Shibuya.apk #42
  10. Save the state Saving state: views class ViewActivity : ComponentActivity(R.layout.activity_view)

    { var count: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) savedInstanceState?.getInt("count")?.let { count = it } // … } override fun onSaveInstanceState(outState: Bundle) { outState.putInt("count", count) super.onSaveInstanceState(outState) } } 14 Save the value for the new instance Shibuya.apk #42
  11. Save the state Saving state: views class ViewActivity : ComponentActivity(R.layout.activity_view)

    { var count: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) savedInstanceState?.getInt("count")?.let { count = it } // … } override fun onSaveInstanceState(outState: Bundle) { outState.putInt("count", count) super.onSaveInstanceState(outState) } } 15 Restore the saved value if present Shibuya.apk #42
  12. Save the state Not saving state: compose @Composable fun SampleScreen()

    { var count: Int by remember { mutableStateOf(0) } Column { Text(text = "Count: $count") Button(onClick = { count++ }) { Text(text = "Increase") } } } 17 Shibuya.apk #42
  13. Save the state Not saving state: compose @Composable fun SampleScreen()

    { var count: Int by remember { mutableStateOf(0) } Column { Text(text = "Count: $count") Button(onClick = { count++ }) { Text(text = "Increase") } } } 18 Shibuya.apk #42 count is not saved and restored at activity destruction
  14. Save the state Saving state: compose @Composable fun SampleScreen() {

    var count: Int by rememberSaveable { mutableStateOf(0) } Column { Text(text = "Count: $count") Button(onClick = { count++ }) { Text(text = "Increase") } } } 19 Shibuya.apk #42
  15. Save the state Saving state: compose @Composable fun SampleScreen() {

    var count: Int by rememberSaveable { mutableStateOf(0) } Column { Text(text = "Count: $count") Button(onClick = { count++ }) { Text(text = "Increase") } } } 20 Shibuya.apk #42 count is saved and restored
  16. Save the state remember vs rememberSaveable ▸ remember ▸ Remember

    any type of the value ▸ rememberSaveable ▸ Remember Bundle-supported type of the value ▸ Add more support with Parcelable framework or Saver interface 21 Shibuya.apk #42
  17. Save the state Save custom data objects // views class

    ViewActivity : ComponentActivity(R.layout.activity_view) { var count: MyValueObject = MyValueObject(0) override fun onSaveInstanceState(outState: Bundle) { outState.putParcelable("count", count) super.onSaveInstanceState(outState) } } // composables @Composable fun SampleScreen() { var count: MyValueObject by rememberSaveable { mutableStateOf(MyValueObject(0)) } } 22 Shibuya.apk #42
  18. Save the state Save custom data objects // views class

    ViewActivity : ComponentActivity(R.layout.activity_view) { var count: MyValueObject = MyValueObject(0) override fun onSaveInstanceState(outState: Bundle) { outState.putParcelable("count", count) super.onSaveInstanceState(outState) } } // composables @Composable fun SampleScreen() { var count: MyValueObject by rememberSaveable { mutableStateOf(MyValueObject(0)) } } 23 Shibuya.apk #42 MyValueObject should be Parcelable! Compilation error 💥 Requires Saver for MyValueObject or MyValueObject should be Parcelable! Runtime error 💥
  19. Save the state Save custom data objects: Parcelables // apply

    Parcelize plugin in build.gradle @Parcelize data class MyValueObject( val count: Int, ) : Parcelable 24 Shibuya.apk #42
  20. Save the state Save custom data objects: Savers @Composable fun

    SampleScreen() { var count: MyValueObject by rememberSaveable(stateSaver = MyValueObjectSaver) { mutableStateOf(MyValueObject(0)) } // … } val MyValueObjectSaver = Saver<MyValueObject, Int>( save = { obj -> obj.count }, restore = { count -> MyValueObject(count = count) }, ) 25 Shibuya.apk #42
  21. Save the state Save custom data objects: Savers @Composable fun

    SampleScreen() { var count: MyValueObject by rememberSaveable(stateSaver = MyValueObjectSaver) { mutableStateOf(MyValueObject(0)) } // … } val MyValueObjectSaver = Saver<MyValueObject, Int>( save = { obj -> obj.count }, restore = { count -> MyValueObject(count = count) }, ) 26 Shibuya.apk #42 Convert type into Bundle-supported one on save
  22. Save the state Save custom data objects: Savers @Composable fun

    SampleScreen() { var count: MyValueObject by rememberSaveable(stateSaver = MyValueObjectSaver) { mutableStateOf(MyValueObject(0)) } // … } val MyValueObjectSaver = Saver<MyValueObject, Int>( save = { obj -> obj.count }, restore = { count -> MyValueObject(count = count) }, ) 27 Shibuya.apk #42 Convert saved value to
  23. Save the state Save custom data objects: Savers @Composable fun

    SampleScreen() { var count: MyValueObject by rememberSaveable(stateSaver = MyValueObjectSaver) { mutableStateOf(MyValueObject(0)) } // … } val MyValueObjectSaver = Saver<MyValueObject, Int>( save = { obj -> obj.count }, restore = { count -> MyValueObject(count = count) }, ) 28 Shibuya.apk #42 Use stateSaver to save/restore
  24. Save the state ViewModel as a StateHolder class MyViewModel( private

    val countRepository: CountRepository, ) : ViewModel() { private val countMutation = MutableStateFlow(0) val count: StateFlow<Int> = countMutation.asStateFlow() } class MyFragment : Fragment() { private val viewModel: MyViewModel by viewModels() } 29 Shibuya.apk #42
  25. Save the state ViewModel as a StateHolder class MyViewModel( private

    val countRepository: CountRepository, ) : ViewModel() { private val countMutation = MutableStateFlow(0) val count: StateFlow<Int> = countMutation.asStateFlow() } class MyFragment : Fragment() { private val viewModel: MyViewModel by viewModels() } 30 Shibuya.apk #42 ViewModel instance is kept during con fi guration changes
  26. Save the state ViewModel as a StateHolder class MyViewModel( private

    val countRepository: CountRepository, ) : ViewModel() { private val countMutation = MutableStateFlow(0) val count: StateFlow<Int> = countMutation.asStateFlow() } class MyFragment : Fragment() { private val viewModel: MyViewModel by viewModels() } 31 Shibuya.apk #42 ViewModel instance is lost when Activity is destroyed for low memory…
  27. Save the state ViewModel + SavedState Handle class MyViewModel( private

    val countRepository: CountRepository, private val savedStateHandle: SavedStateHandle, ) : ViewModel() { private val countMutation = MutableStateFlow( savedStateHandle["count"] ?: 0 ) val count: StateFlow<Int> = countMutation.asStateFlow() fun saveState() { savedStateHandle["count"] = countMutation.value } } class MyFragment : Fragment() { private val viewModel: MyViewModel by viewModels() override fun onSaveInstanceState(outState: Bundle) { viewModel.saveState() } } 32 Shibuya.apk #42
  28. Save the state ViewModel + SavedState Handle class MyViewModel( private

    val countRepository: CountRepository, private val savedStateHandle: SavedStateHandle, ) : ViewModel() { private val countMutation = MutableStateFlow( savedStateHandle["count"] ?: 0 ) val count: StateFlow<Int> = countMutation.asStateFlow() fun saveState() { savedStateHandle["count"] = countMutation.value } } class MyFragment : Fragment() { private val viewModel: MyViewModel by viewModels() override fun onSaveInstanceState(outState: Bundle) { viewModel.saveState() } } 33 Shibuya.apk #42 Use SavedStateHandle to save and restore values
  29. Save the state Save custom data objects: Savers for reusable

    UI element states class MyValueState( private val countRepository: CountRepository, initial: Int, ) { var count: MutableState<Int> = mutableStateOf(initial) private set } 34 Shibuya.apk #42
  30. Save the state Save custom data objects: Savers for reusable

    UI element states @Composable fun rememberMyValueState( countRepository: CountRepository, initialCount: Int, ) { return rememberSaveable( inputs = arrayOf(countRepository, initialCount), saver = MyValueState.saver(countRepository), ) { MyValueState(countRepository, initialCount) } } 35 Shibuya.apk #42
  31. Save the state Save custom data objects: Savers for reusable

    UI element states class MyValueState( private val countRepository: CountRepository, initial: Int, ) { companion object { fun saver( repository: CountRepository, ): Saver<MyValueState, Int> = Saver( save = { state -> state.count }, restore = { value -> MyValueState(repository, value) }, ) } } 36 Shibuya.apk #42
  32. Save the state Save custom data objects: Savers for reusable

    view element states class MyValueState( private val countRepository: CountRepository, initial: Int, ) { var count: Int = initial } 37 Shibuya.apk #42
  33. Save the state Save custom data objects: Savers for reusable

    view element states class MyValueState( private val countRepository: CountRepository, initial: Int, ) : SavedStateRegistry.SavedStateProvider { var count: Int = initial override fun saveState(): Bundle = bundleOf( "count" to count ) } 38 Shibuya.apk #42
  34. Save the state Save custom data objects: Savers for reusable

    view element states class MyValueState( private val countRepository: CountRepository, initial: Int, registryOwner: SavedStateRegistryOwner, ) : SavedStateRegistry.SavedStateProvider { var count: Int = initial init { registryOwner.lifecycle.addObserver( LifecycleEventObserver { _, event -> // TODO: recover the state from saved state registry } ) } } 39 Shibuya.apk #42
  35. Save the state Save custom data objects: Savers for reusable

    view element states LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val reg = registryOwner.savedStateRegistry if (reg.getSavedStateProvider("provider") == null) { reg.registerSavedStateProvider("provider", this) } val state = reg.consumeRestoredStateForKey("provider") count = state?.getInt("count") ?: initial } } 40 Shibuya.apk #42
  36. Save the state Save custom data objects: Savers for reusable

    view element states class MyValueState( private val countRepository: CountRepository, initial: Int, registryOwner: SavedStateRegistryOwner, ) : SavedStateRegistry.SavedStateProvider class MyFragment : Fragment() { private var state: MyValueState = MyValueState( countRepository = // … initial = 0, registryOwner = this, ) } 41 Shibuya.apk #42
  37. Save the state Save custom data objects: Savers for reusable

    view element states class MyValueState( private val countRepository: CountRepository, initial: Int, registryOwner: SavedStateRegistryOwner, ) : SavedStateRegistry.SavedStateProvider class MyFragment : Fragment() { private var state: MyValueState = MyValueState( countRepository = // … initial = 0, registryOwner = this, ) } 42 Shibuya.apk #42
  38. Save the state Advanced use case ▸ Control rememberSaveable value’s

    lifecycle ▸ Saveable values are disposed on exit composition by default ▸ We can extend the lifecycle ▸ as long as the navigation destination is in the back stack ▸ similar to navGraphViewModels ▸ see: https://youtu.be/V-s4z7B_Gnc?t=874 43 Shibuya.apk #42
  39. Save the state Stop stopping ▸ Con fi guration changes

    ▸ Fixing screen orientation doesn’t help ▸ Can’t stop device folding, theme / locale changes, etc… ▸ Stopping con fi g changes ▸ It’s on you, not the system; More code required to handle this ▸ Can’t stop activity recreation for some con fi guration changes 44 ✗ android:screenOrientation="portrait" Shibuya.apk #42 ? android:configChanges="orientation"
  40. Save the state Stop stopping ▸ Don’t keep activities ▸

    Developer option to always dispose activity instance ▸ Activity disposal can happen without enabling this option ▸ ViewModel will lost its data without SavedStateHandle 45 Shibuya.apk #42
  41. Save the state Caveats 1: SavedStateHandle limitations ▸ no more

    than 1MB data size in 1 process ▸ Same as Bundle! ▸ otherwise TransactionTooLargeException 💥 ▸ Avoid saving HUGE objects ▸ Bitmap obviously :) ▸ List containing lots of elements 46 Shibuya.apk #42
  42. Save the state Caveats 1: SavedStateHandle limitations ▸ How to

    make value object properties transient ▸ Kotlin Parcelize: @IgnoredOnParcel ▸ Make sure to set the data after recreation! 47 Shibuya.apk #42
  43. Save the state // Kotlin 1.6.20+ @Parcelize data class SampleValue(

    val name: String = "", @IgnoredOnParcel val list: List<String> = emptyList(), ) : Parcelable 48 Shibuya.apk #42 Caveats 1: SavedStateHandle limitations
  44. Save the state Caveats 2: WebView ▸ Keeps reloading the

    webpage on showing the WebView ▸ <input> form values are gone after recreation ▸ Accompanist WebView can save scroll position (0.31.1-alpha) ▸ What if we need to implement a image fi le chooser for <input>…? ▸ Android 11+: no worries with stock Photo Picker, it’s transparent activity! ▸ Android 10 and below: no clue… 49 Shibuya.apk #42
  45. Save the state Enable automatic installation of the backported photo

    picker <!-- Trigger Google Play services to install the backported photo picker module. —> <!-- on Android 4.4 to Android 10 --> <service android:name="com.google.android.gms.metadata.ModuleDependencies" android:enabled="false" android:exported="false" tools:ignore="MissingClass" > <intent-filter> <action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" /> </intent-filter> <meta-data android:name="photopicker_activity:0:required" android:value="" /> </service> 50