Slide 1

Slide 1 text

Save the state Keishin Yokomaku (@KeithYokoma) / Giftmall, Inc. Shibuya.apk #42

Slide 2

Slide 2 text

Save the state About me ▸ Keishin Yokomaku - @KeithYokoma ▸ Giftmall, Inc. ▸ Android App Engineer ▸ https://keithyokoma.dev/ 2 Shibuya.apk #42

Slide 3

Slide 3 text

Shibuya.apk #42 Save the state

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Save the state Quick run-through: Activity lifecycle 8 Shibuya.apk #42 source: https://gifer.com/en/2Dn8

Slide 9

Slide 9 text

Save the state Quick run-through: Activity lifecycle 9 Shibuya.apk #42 source: https://giphy.com/gifs/6qqnulwmAJozb51SCe

Slide 10

Slide 10 text

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(R.id.counter) updateCounterText(counterText) findViewById(R.id.counter_button).setOnClickListener { count++ updateCounterText(counterText) } } private fun updateCounterText(counterText: TextView) { counterText.text = "Count: $count" } } 10 Shibuya.apk #42

Slide 11

Slide 11 text

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(R.id.counter) updateCounterText(counterText) findViewById(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

Slide 12

Slide 12 text

Save the state Not saving state: views 12 Shibuya.apk #42

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Save the state Saving state: views 16 Shibuya.apk #42

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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 💥

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Save the state ViewModel as a StateHolder class MyViewModel( private val countRepository: CountRepository, ) : ViewModel() { private val countMutation = MutableStateFlow(0) val count: StateFlow = 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…

Slide 32

Slide 32 text

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 = 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

Slide 33

Slide 33 text

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 = 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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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 = Saver( save = { state -> state.count }, restore = { value -> MyValueState(repository, value) }, ) } } 36 Shibuya.apk #42

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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"

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Save the state // Kotlin 1.6.20+ @Parcelize data class SampleValue( val name: String = "", @IgnoredOnParcel val list: List = emptyList(), ) : Parcelable 48 Shibuya.apk #42 Caveats 1: SavedStateHandle limitations

Slide 49

Slide 49 text

Save the state Caveats 2: WebView ▸ Keeps reloading the webpage on showing the WebView ▸ 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 …? ▸ Android 11+: no worries with stock Photo Picker, it’s transparent activity! ▸ Android 10 and below: no clue… 49 Shibuya.apk #42

Slide 50

Slide 50 text

Save the state Enable automatic installation of the backported photo picker 50

Slide 51

Slide 51 text

Save the state Keishin Yokomaku (@KeithYokoma) / Giftmall, Inc. Shibuya.apk #42