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
    Keishin Yokomaku (@KeithYokoma) / Giftmall, Inc.
    Shibuya.apk #42

    View Slide

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


    ▸ Giftmall, Inc.


    ▸ Android App Engineer


    ▸ https://keithyokoma.dev/
    2
    Shibuya.apk #42

    View Slide

  3. Shibuya.apk #42
    Save the state

    View Slide

  4. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  10. 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

    View Slide

  11. 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

    View Slide

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

    View Slide

  13. 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

    View Slide

  14. 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

    View Slide

  15. 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

    View Slide

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

    View Slide

  17. 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

    View Slide

  18. 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

    View Slide

  19. 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

    View Slide

  20. 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

    View Slide

  21. 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

    View Slide

  22. 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

    View Slide

  23. 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 💥

    View Slide

  24. 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

    View Slide

  25. 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

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide

  30. 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

    View Slide

  31. 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…

    View Slide

  32. 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

    View Slide

  33. 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

    View Slide

  34. 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

    View Slide

  35. 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

    View Slide

  36. 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

    View Slide

  37. 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

    View Slide

  38. 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

    View Slide

  39. 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

    View Slide

  40. 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

    View Slide

  41. 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

    View Slide

  42. 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

    View Slide

  43. 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

    View Slide

  44. 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"

    View Slide

  45. 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

    View Slide

  46. 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

    View Slide

  47. 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

    View Slide

  48. 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

    View Slide

  49. 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

    View Slide

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





    android:name="com.google.android.gms.metadata.ModuleDependencies"


    android:enabled="false"


    android:exported="false"


    tools:ignore="MissingClass"


    >













    android:name="photopicker_activity:0:required"


    android:value=""


    />



    50

    View Slide

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

    View Slide