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

Save the state

Save the state

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