How to save the status when Compose switches between horizontal and vertical screens? rememberSaveable implementation principle analysis

foreword

In this article, it is mentioned that the state saving of Navigation is actually rememberSaveableimplemented . Some students feedback that they hope to introduce the function and implementation principle of rememberSaveable separately. We all know that remember can save data and avoid state loss due to reorganization. But it still cannot avoid data loss on ConfigurationChanged. If you want to save the state in scenarios such as horizontal and vertical screen switching, you need to use rememberSavable.

Start with an error

First of all, there is no difference between rememberSaveable and remember in code usage:

//保存列表状态
val list = rememberSaveable {
    
    
    mutableListOf<String>()
}

//保存普通状态
var value by rememberSaveable {
    
    
    mutableStateOf("")
}

As above, as long as remember is changed to rememberSaveable, the state we created can be saved across horizontal and vertical screen switching or even across processes. However, not any type of value can be stored in rememberSaveable:

data class User(
    val name: String = ""
)

val user = rememberSaveable {
    
    
    User()
}

An error occurs when the above code is run:

java.lang.IllegalArgumentException: User(name=) cannot be saved using the current SaveableStateRegistry. The default implementation only supports types which can be stored inside the Bundle. Please consider implementing a custom Saver for this class and pass it to rememberSaveable().

User cannot be deposited into Bundle. This is very reasonable, because the persistence of data in rememberSaveable is finally performed ComponentActivity#onSaveInstanceStatein , which requires the help of Bundle.

rememberSaveable source code analysis

So, how does rememberSaveable relate to onSaveInstanceState? Next, briefly analyze the internal implementation

@Composable
fun <T : Any> rememberSaveable(
    vararg inputs: Any?,
    saver: Saver<T, out Any> = autoSaver(),
    key: String? = null,
    init: () -> T
): T {
    
    
    //...

    // 通过 CompositionLocal 获取 SaveableStateRegistry
    val registry = LocalSaveableStateRegistry.current
    
    // 通过 init 获取需要保存的数据
    val value = remember(*inputs) {
    
    
        // registry 根据 key 恢复数据,恢复的数据是一个 Saveable
        val restored = registry?.consumeRestored(finalKey)?.let {
    
    
            // 使用 Saver 将 Saveable 转换为业务类型
            saver.restore(it)
        }
        restored ?: init()
    }

    // 用一个 MutableState 保存 Saver,主要是借助 State 的事务功能避免一致性问题发生
    val saverHolder = remember {
    
     mutableStateOf(saver) }
    saverHolder.value = saver

    if (registry != null) {
    
    
        DisposableEffect(registry, finalKey, value) {
    
    
            //ValueProvider:通过 Saver#save 存储数据
            val valueProvider = {
    
    
                with(saverHolder.value) {
    
     SaverScope {
    
     registry.canBeSaved(it) }.save(value) }
            }
            //试探数值是否可被保存
            registry.requireCanBeSaved(valueProvider())
            //将ValueProvider 注册到 registry ,等到合适的时机被调用
            val entry = registry.registerProvider(finalKey, valueProvider)
            onDispose {
    
    
                entry.unregister()
            }
        }
    }
    return value
}

As above, the logic is very clear, mainly registryaround :

  1. Restore persistent data by key
  2. Register ValueProvider based on key and wait for the right time to perform data persistence
  3. Unregistered in onDispose

registry is one SaveableStateRegistry.

restore key data

rememberSaveable is an enhanced version of remember. It must first have the ability to remember. You can see that remember is indeed called internally to create data and cache it in Composition. initProvides the first creation of remember data. The created data is persisted at a later point in time, and the next time rememberSaveable is executed, it will try to restore the previously persisted data. The specific process is divided into the following two steps:

  1. Find the key to get Saveable through registry.consumeRestored,
  2. Saveable is converted to a business type via saver.restore.

The above process involves two roles:

  • SaveableStateRegistry : Obtained through CompositionLocal, it is responsible for deserializing the data in the Bundle and returning a Saveable
  • Saver : Saver is created by autoSaver by default, responsible for the conversion between Saveable and business data.

Saveable is not a concrete type, it can be any type that can be persisted (written to a Bundle). autoSaverFor , this Saveable is the business data type itself.

private val AutoSaver = Saver<Any?, Any>(
    save = {
    
     it },
    restore = {
    
     it }
)

For some complex business structures, sometimes not all fields need to be persisted. Saver provides us with such an opportunity to convert business types into serializable types as needed. Compose also provides two preset Savers: ListSaverand MapSaver, which can be converted to List or Map.

About the Key of restoring data: You can see that the saving and restoring of data depends on a key. It is reasonable that the key needs to be strictly consistent when saving and restoring, but we do not specify a specific key when calling rememberSaveable on weekdays, so switch between horizontal and vertical screens Even after the process is restarted, how to restore the data? In fact, this key is automatically set by Compose for us. It is the key based on the code position generated by the instrumentation at compile time, so it can be guaranteed that it will remain unchanged every time the process is executed.

Register ValueProvider

SaveableStateRegistry is associated with key registry in DisposableEffect ValueProvider.
ValueProvider is a lambda that will be called internally Saver#saveto convert business data into Saveable.

Saver#save is an extension function of SaverScope, so a SaverScope needs to be created here to call the save method. SaverScope is mainly used to provide the canBeSaved method, which can be used to check whether the type can be persisted when customizing Saver

After the ValueProvider is created, it will call registry.registerProviderto register, and wait for the right time (such as Activity's onSaveInstanceState) to be called. Before registering, first call requireCanBeSavedto determine whether the data type can be saved, which is where the error was reported earlier in the article. Mark it first, and then we will look at the implementation of specific inspections.

logout registry

Finally, call unregister in onDispose to cancel the previous registration.

The basic process of rememberSaveable is clear, and you can see that the protagonist is the registry, so it is necessary to go deep into the SaveableStateRegistry to have a look. We LocalSaveableStateRegistrycan .

DisposableSavableStateRegistry source code analysis

override fun setContent(content: @Composable () -> Unit) {
    
    
    //...
    ProvideAndroidCompositionLocals(owner, content)
    //...
}

@Composable
@OptIn(ExperimentalComposeUiApi::class)
internal fun ProvideAndroidCompositionLocals(
    owner: AndroidComposeView,
    content: @Composable () -> Unit
) {
    
    
    val view = owner
    val context = view.context
    
    //...
    
    val viewTreeOwners = owner.viewTreeOwners ?: throw IllegalStateException(
        "Called when the ViewTreeOwnersAvailability is not yet in Available state"
    )
    val saveableStateRegistry = remember {
    
    
        DisposableSaveableStateRegistry(view, viewTreeOwners.savedStateRegistryOwner)
    }

    //...
    CompositionLocalProvider(
        //...
        LocalSaveableStateRegistry provides saveableStateRegistry,
        //...
    ) {
    
    
        ProvideCommonCompositionLocals(
            owner = owner,
            //...
            content = content
        )
    }
}

As above, we set various CompositionLocals in the setContent of the Activity, including the LocalSaveableStateRegistry, so the registry is not only a SaveableStateRegistry, but also a DisposableSaveableStateRegistry.

Next, look at the creation process of DisposableSaveableStateRegistry.

saveableStateRegistry 与 SavedStateRegistry

Note that the DisposableSaveableStateRegistry below is not a real constructor, but a Wrapper of the constructor with the same name. Before calling the constructor to create an instance, it is called to androidxRegistryperform

internal fun DisposableSaveableStateRegistry(
    id: String,
    savedStateRegistryOwner: SavedStateRegistryOwner
): DisposableSaveableStateRegistry {
    
    
    //基于 id 创建 key
    val key = "${
      
      SaveableStateRegistry::class.java.simpleName}:$id"
    
    // 基于 key 获取 bundle 数据
    val androidxRegistry = savedStateRegistryOwner.savedStateRegistry
    val bundle = androidxRegistry.consumeRestoredStateForKey(key)
    val restored: Map<String, List<Any?>>? = bundle?.toMap()

    // 创建 saveableStateRegistry,传入 restored 以及 canBeSaved
    val saveableStateRegistry = SaveableStateRegistry(restored) {
    
    
        canBeSavedToBundle(it)
    }
    
    val registered = try {
    
    
        androidxRegistry.registerSavedStateProvider(key) {
    
    
            //调用 register#performSave 并且转为 Bundle
            saveableStateRegistry.performSave().toBundle()
        }
        true
    } catch (ignore: IllegalArgumentException) {
    
    
        false
    }
    
    return DisposableSaveableStateRegistry(saveableStateRegistry) {
    
    
        if (registered) {
    
    
            androidxRegistry.unregisterSavedStateProvider(key)
        }
    }
}

androidxRigistry does something similar to the registry in rememberSaveable:

  1. Restore bundle data based on key,
  2. Register SavedStateProvider based on key.

But androidxRegistry is not a SaveableStateRegistry but one SavedStateRegistry. The name is a bit confusing, the latter comes from androidx.savedstate, which belongs to the platform code, and SaveableStateRegistry belongs to the platform-independent code of compose-runtime. It can be seen that the Wrapper with the same name as this constructor is very important. It is like a bridge, decoupling and associating platform-dependent and platform-independent code.

DisposableSaveableStateRegistry 与 SaveableStateRegistryImpl

The real constructor of DisposableSaveableStateRegistry is defined as follows:

internal class DisposableSaveableStateRegistry(
    saveableStateRegistry: SaveableStateRegistry,
    private val onDispose: () -> Unit
) : SaveableStateRegistry by saveableStateRegistry {
    
    

    fun dispose() {
    
    
        onDispose()
    }
}

The parameter saveableStateRegistry is used here as a proxy for the SaveableStateRegistry interface. saveableStateRegistry is actually a SaveableStateRegistryImplobject , which is created like this:

val saveableStateRegistry = SaveableStateRegistry(restored) {
    
    
    canBeSavedToBundle(it)
}

fun SaveableStateRegistry(
    restoredValues: Map<String, List<Any?>>?,
    canBeSaved: (Any) -> Boolean
): SaveableStateRegistry = SaveableStateRegistryImpl(restoredValues, canBeSaved)

SaveableStateRegistryImpl is created with two parameters:

  • restoredValues: The bundle data restored by androidxRegistry is a Map object.
  • canBeSaved : Used to check whether the data can be persisted, you can see that canBeSavedToBundle is actually called here.

canBeSavedToBundle

The error report at the beginning of the article was requireCanBeSaved -> canBeSavedToBundlechecked out. Check out the persistence types supported by rememberSaveable through canBeSavedToBundle:

private fun canBeSavedToBundle(value: Any): Boolean {
    
    
    // SnapshotMutableStateImpl is Parcelable, but we do extra checks
    if (value is SnapshotMutableState<*>) {
    
    
        if (value.policy === neverEqualPolicy<Any?>() ||
            value.policy === structuralEqualityPolicy<Any?>() ||
            value.policy === referentialEqualityPolicy<Any?>()
        ) {
    
    
            val stateValue = value.value
            return if (stateValue == null) true else canBeSavedToBundle(stateValue)
        } else {
    
    
            return false
        }
    }
    for (cl in AcceptableClasses) {
    
    
        if (cl.isInstance(value)) {
    
    
            return true
        }
    }
    return false
}

private val AcceptableClasses = arrayOf(
    Serializable::class.java,
    Parcelable::class.java,
    String::class.java,
    SparseArray::class.java,
    Binder::class.java,
    Size::class.java,
    SizeF::class.java
)

First, SnapshotMutableStateit is allowed to be persisted, because we need to call mutableStateOf in rememberSaveable; second, the generic type of SnapshotMutableState must be the type AcceptableClassesin , and our custom User obviously does not meet the requirements, so the error at the beginning is reported.

SaveableStateRegistryImpl source code analysis

The relationship between several Registry types has been clarified earlier, as shown in the figure below

The main methods of the SaveableStateRegistry interface are proxied by SaveableStateRegistryImpl:

  • consumeRestored: restore data according to key
  • registerProvider: register ValueProvider
  • canBeSaved: Used to check whether the data is a saveable type
  • performSave: perform data save

canBeSaved As mentioned earlier, it will actually call back canBeSavedToBundle. Next, let's take a look at how the other methods in SaveableStateRegistryImpl are implemented:

consumeRestored

    override fun consumeRestored(key: String): Any? {
    
    
        val list = restored.remove(key)
        return if (list != null && list.isNotEmpty()) {
    
    
            if (list.size > 1) {
    
    
                restored[key] = list.subList(1, list.size)
            }
            list[0]
        } else {
    
    
            null
        }
    }

We know restoredthat is the data recovered from the Bundle, which is actually a Map type. And consumeRestoredit is to find data by key in restored. The Value of restore is of type List. When restoring data, only the last one is kept. By the way, let’s talk about the name consumeRestored, which exposes the private member information of restore to the outside, which is a bit inexplicable.

registerProvider

    override fun registerProvider(key: String, valueProvider: () -> Any?): Entry {
    
    
        require(key.isNotBlank()) {
    
     "Registered key is empty or blank" }
        @Suppress("UNCHECKED_CAST")
        valueProviders.getOrPut(key) {
    
     mutableListOf() }.add(valueProvider)
        return object : Entry {
    
    
            override fun unregister() {
    
    
                val list = valueProviders.remove(key)
                list?.remove(valueProvider)
                if (list != null && list.isNotEmpty()) {
    
    
                    // if there are other providers for this key return list back to the map
                    valueProviders[key] = list
                }
            }
        }
    }
    

Register ValueProvider to valueProviders, valueProviders is also a Map whose value is List, and the same Key can correspond to multiple Values. The returned Entry is used to call unregister in onDispose.

DisposableSaveableStateRegistry is a CompositionLocal singleton, so unregister is required to avoid unnecessary leaks. Note here to ensure that other values ​​in the List in the same key are not removed

Puzzled: Under what circumstances will the same key registerProvider multiple values?

performSave

    override fun performSave(): Map<String, List<Any?>> {
    
    
        val map = restored.toMutableMap()
        valueProviders.forEach {
    
     (key, list) ->
            if (list.size == 1) {
    
    
                val value = list[0].invoke()
                if (value != null) {
    
    
                    check(canBeSaved(value))
                    map[key] = arrayListOf<Any?>(value)
                }
            } else {
    
    
                map[key] = List(list.size) {
    
     index ->
                    val value = list[index].invoke()
                    if (value != null) {
    
    
                        check(canBeSaved(value))
                    }
                    value
                }
            }
        }
        return map
    }

Here, the ValueProvider is called to get the data and store it in restored. There is also a special treatment for the List type of Value. The call timing of performSave has already appeared before, and it is called in the Provider registered by androidxRegistry:

 androidxRegistry.registerSavedStateProvider(key) {
    
    
            //调用 register#performSave 并且转为 Bundle
            saveableStateRegistry.performSave().toBundle()
        }

SavedStateProvider will be executed when onSaveInstance.

So far, the timing of rememberSaveable persistence has been associated with the platform.

Finally look back at androidxRegistry

Finally, let's look back at the DisposableSavableStateRegistry, mainly using androidxRegistry to obtain the data corresponding to the key, and register the Provider corresponding to the key. So how did androidxRegistry and key come from?

internal fun DisposableSaveableStateRegistry(
    id: String,
    savedStateRegistryOwner: SavedStateRegistryOwner
): DisposableSaveableStateRegistry {
    
    
    
    val key = "${
      
      SaveableStateRegistry::class.java.simpleName}:$id"

    val androidxRegistry = savedStateRegistryOwner.savedStateRegistry
    
    //...
    
 }

Let's talk about the key first. The key is uniquely determined by the id, which is actually the layoutId ComposeViewof . We know that ComposeView is the container of Activity/Fragment carrying Composable, and rememberSaveable will persist data in units of ComposeView.

Because the id of your ComposeView determines the location where rememberSaveable stores data, if there are multiple ComposeViews using the same id within the Activity/Fragment range, only the first ComposeView can restore data normally, so pay special attention to this

Take another look at androidxRegistry, which is provided by SavedStateRegistryOwner, and this owner is the value assigned when ComposeView is attached to Activity, which is the Activity itself:

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        ContextAware,
        LifecycleOwner,
        ViewModelStoreOwner,
        HasDefaultViewModelProviderFactory,
        SavedStateRegistryOwner, // ComponentActivity 是一个 SavedStateRegistryOwner
        OnBackPressedDispatcherOwner,
        ActivityResultRegistryOwner,
        ActivityResultCaller {
    
    
        
    //...
    
    public final SavedStateRegistry getSavedStateRegistry() {
    
    
        return mSavedStateRegistryController.getSavedStateRegistry();
    }
    
    //...
}

mSavedStateRegistryControllerIt will be called in onCreate when the Activity is rebuilt performRestore; it will be executed in onSaveInstanceState performSave.

protected void onCreate(@Nullable Bundle savedInstanceState) {
    
    
    mSavedStateRegistryController.performRestore(savedInstanceState);
    //...
}


protected void onSaveInstanceState(@NonNull Bundle outState) {
    
    
    //...
    mSavedStateRegistryController.performSave(outState);
}

mSavedStateRegistryController finally calls the method of the same name of SavedStateRegistry, take a look SavedStateRegistry#performSave:

fun performSave(outBundle: Bundle) {
    
    
    //...
    val it: Iterator<Map.Entry<String, SavedStateProvider>> =
        this.components.iteratorWithAdditions()
    while (it.hasNext()) {
    
    
        val (key, value) = it.next()
        components.putBundle(key, value.saveState())
    }
    if (!components.isEmpty) {
    
    
        outBundle.putBundle(SAVED_COMPONENTS_KEY, components)
    }
}

components is a Map of registered SavedStateProvider. Call Provider's saveState method in performSave to obtain the bundle saved in rememberSaveable, and then store it in outBundle for persistence.

So far, rememberSaveable has completed the state saving when switching between horizontal and vertical screens on the Android platform.

Finally, we end with a diagram. Red is the data flow direction when saving data, and green is the data flow direction when restoring data:

Guess you like

Origin blog.csdn.net/vitaviva/article/details/127484220