Compose状态保存rememberSaveable原理解析

目录
  • 前言
  • 从一个报错说起
  • rememberSaveable 源码分析
    • 恢复 key 的数据
    • 注册 ValueProvider
    • 注销 registry
  • DisposableSavableStateRegistry 源码分析
    • saveableStateRegistry 与 SavedStateRegistry
    • DisposableSaveableStateRegistry 与 SaveableStateRegistryImpl
    • canBeSavedToBundle
  • SaveableStateRegistryImpl 源码分析
    • consumeRestored
    • registerProvider
    • performSave
  • 最后回看 androidxRegistry

前言

我曾经在一篇介绍 Compose Navigation 的文章 中提到了 Navigation 的状态保存实际是由 rememberSaveable 实现的,有同学反馈希望单独介绍一下 rememberSaveable 的功能及实现原理。

我们都知道 remember 可以保存数据、避免状态因重组而丢失,但它依然无法避免在 ConfigurationChanged 时的数据丢失。想要在横竖屏切换等场景下依然保存状态,就需要使用 rememberSavable。

从一个报错说起

首先,在代码使用上 rememberSaveable 和 remember 没有区别:

//保存列表状态
val list = rememberSaveable {
    mutableListOf<String>()
}
//保存普通状态
var value by rememberSaveable {
    mutableStateOf("")
}

如上,只要将 remember 改为 rememberSaveable,我们创建的状态就可以跨越横竖屏切换甚至跨越进程持续保存了。不过 rememberSaveable 中并非任何类型的值都可以存储:

data class User(
    val name: String = ""
)
val user = rememberSaveable {
    User()
}

上面代码运行时会发生错误:

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 无法存入 Bundle。这非常合理,因为 rememberSaveable 中数据的持久化最终在 ComponentActivity#onSaveInstanceState 中执行,这需要借助到 Bundle 。

rememberSaveable 源码分析

那么,rememberSaveable 是如何关联到 onSaveInstanceState 的呢?接下来简单分析一下内部实现

@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
}

如上,逻辑很清晰,主要是围绕 registry 展开的:

  • 通过 key 恢复持久化的数据
  • 基于 key 注册 ValueProvider,等待合适时机执行数据持久化
  • 在 onDispose 中被注销注册

registry 是一个 SaveableStateRegistry

恢复 key 的数据

rememberSaveable 是加强版的 remember,首先要具备 remember 的能力,可以看到内部也确实是调用了 remember 来创建数据同时缓存到 Composition 中。init 提供了 remember 数据的首次创建。被创建的数据在后续某个时间点进行持久化,下次执行 rememberSaveable 时会尝试恢复之前持久化的数据。具体过程分为以下两步:

  • 通过 registry.consumeRestored 查找 key 获取 Saveable,
  • Saveable 经由 saver.restore 转换为业务类型。

上述过程涉及到两个角色:

  • SaveableStateRegistry:通过 CompositionLocal 获取,它负责将 Bundle 中的数据反序列化后,返回一个 Saveable
  • Saver:Saver 默认有 autoSaver 创建,负责 Saveable 与业务数据之间的转换。

Saveable 并不是一个在具体类型,它可以是可被持久化(写入 Bundle)的任意类型。对于 autoSaver 来说, 这个 Saveable 就是业务数据类型本身。

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

对于一些复杂的业务结构体,有时并非是所有字段都需要持久化。Saver 为我们提供了这样一个机会机会,可以按照需要将业务类型转化为可序列化类型。Compose 也提供了两个预置的 Saver:ListSaverMapSaver,可以用来转换成 List 或者 Map。

关于恢复数据的 Key :可以看到数据的保存和恢复都依赖一个 key,按道理 key 需要在保存和恢复时严格保持一致 ,但我们平日调用 rememberSaveable 时并没有指定具体的 key,那么在横竖屏切换甚至进程重启后是如何恢复数据的呢?其实这个 key 是 Compose 自动帮我们设置的,它就是编译期插桩生成的基于代码位置的 key ,所以可以保证每次进程执行到此处都保持不变

注册 ValueProvider

SaveableStateRegistry 在 DisposableEffect 中关联 key 注册 ValueProvider。 ValueProvider 是一个 lambda,内部会调用 Saver#save 将业务数据转化为 Saveable。

Saver#save 是 SaverScope 的扩展函数,所以这里需要创建一个 SaverScope 来调用 save 方法。SaverScope 主要用来提供 canBeSaved 方法,我们在自定义 Saver 时可以用来检查类型是否可被持久化

ValueProvider 创建好后紧接着会调用 registry.registerProvider 进行注册,等待合适的时机(比如 Activity 的 onSaveInstanceState)被调用。在注册之前,先调用 requireCanBeSaved 判断数据类型是否可以保存,这也就是文章前面报错的地方。先 mark 一下,稍后我们看一下具体检查的实现。

注销 registry

最后在 onDispose 中调用 unregister 注销之前的注册 。

rememberSaveable 的基本流程理清楚了,可以看见主角就是 registry,因此有必要深入 SaveableStateRegistry 去看一下。我们顺着 LocalSaveableStateRegistry 可以很容易找到 registry 的出处。

DisposableSavableStateRegistry 源码分析

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
        )
    }
}

如上,我们在 Activity 的 setContent 中设置各种 CompositionLocal,其中就有 LocalSaveableStateRegistry,所以 registry 不仅是一个 SaveableStateRegistry,更是一个 DisposableSaveableStateRegistry 。

接下来看一下 DisposableSaveableStateRegistry 的创建过程 。

saveableStateRegistry 与 SavedStateRegistry

注意下面这个 DisposableSaveableStateRegistry 不是真正的构造函数,它是同名构造函数的一个 Wrapper,在调用构造函数创建实例之前,先调用 androidxRegistry 进行了一系列处理:

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 跟 rememberSaveable 中的 registry 做的事情类似:

  • 基于 key 恢复 bundle 数据,
  • 基于 key 注册 SavedStateProvider。

但 androidxRegistry 不是一个 SaveableStateRegistry 而是一个 SavedStateRegistry。名字上有点绕,后者来自 androidx.savedstate ,属于平台代码,而 SaveableStateRegistry 属于 compose-runtime 的平台无关代码。可见这个构造函数的同名 Wrapper 很重要,他就像一个桥梁,解耦和关联了平台相关和平台无关代码。

DisposableSaveableStateRegistry 与 SaveableStateRegistryImpl

DisposableSaveableStateRegistry 真正的构造函数定义如下:

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

这里用了参数 saveableStateRegistry 作为 SaveableStateRegistry 接口的代理。saveableStateRegistry 实际是一个 SaveableStateRegistryImpl 对象,它像这样创建:

val saveableStateRegistry = SaveableStateRegistry(restored) {
    canBeSavedToBundle(it)
}
fun SaveableStateRegistry(
    restoredValues: Map<String, List<Any?>>?,
    canBeSaved: (Any) -> Boolean
): SaveableStateRegistry = SaveableStateRegistryImpl(restoredValues, canBeSaved)

SaveableStateRegistryImpl 被创建时传入两个参数:

  • restoredValues:androidxRegistry 恢复的 bundle 数据,是一个 Map 对象。
  • canBeSaved : 用来检查数据是否可持久化,可以的看到这里实际调用了 canBeSavedToBundle。

canBeSavedToBundle

文章开头的报错就是 requireCanBeSaved -> canBeSavedToBundle 检查出来的,通过 canBeSavedToBundle 看一下 rememberSaveable 支持的持久化类型:

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
)

首先, SnapshotMutableState 允许被持久化,因为我们需要在 rememberSaveable 中调用 mutableStateOf;其次,SnapshotMutableState 的泛型必须是 AcceptableClasses 中的类型,我们自定义的 User 显然不符合要求,因此报了开头的错误。

SaveableStateRegistryImpl 源码分析

前面理清了几个 Registry 类型的关系,整理如下图

SaveableStateRegistry 接口的各主要方法都由 SaveableStateRegistryImpl 代理的:

  • consumeRestored:根据 key 恢复数据
  • registerProvider:注册 ValueProvider
  • canBeSaved:用来检查数据是否是可保存类型
  • performSave:执行数据保存

canBeSaved 前面介绍过,其实会回调 canBeSavedToBundle。接下来看一下 SaveableStateRegistryImpl 中其他几个方法是如何实现的:

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

我们知道 restored 是从 Bundle 中恢复的数据,实际是一个 Map了类型。而 consumeRestored 就是在 restored 中通过 key 查找数据。restore 的 Value 是 List 类型。当恢复数据时,只保留最后一个只。顺便吐槽一下 consumeRestored 这个名字,将 restore 这个 private 成员信息暴露给了外面,有些莫名其妙。

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

将 ValueProvider 注册到 valueProviders ,valueProviders 也是一个值为 List 的 Map,同一个 Key 可以对应多个 Value。返回的 Entry 用于 onDispose 中调用 unregister。

DisposableSaveableStateRegistry 是一个 CompositionLocal 单例,所以需要 unregister 避免不必要的泄露。注意这里要确保同一个 key 中的 List 中的其它值不被移除

不解:什么情况下同一个 key 会 registerProvider 多个值呢?

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
    }

在这里调用了 ValueProvider 获取数据后存入 restored ,这里也是有针对 Value 是 List 类型的特别处理。performSave 的调用时机前面已经出现了,是 androidxRegistry 注册的 Provider 中调用:

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

SavedStateProvider 会在 onSaveInstance 时被执行。

至此, rememberSaveable 持久化发生的时机与平台进行了关联。

最后回看 androidxRegistry

最后我们再回看一下 DisposableSavableStateRegistry,主要是使用 androidxRegistry 获取 key 对应的数据,并注册 key 对应的 Provider。那么 androidxRegistry 和 key 是怎么来的?

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

先说 key 。key 由 id 唯一决定,而这个 id 其实是 ComposeView 的 layoutId。我们知道 ComposeView 是 Activity/Fragment 承载 Composable 的容器,rememberSaveable 会按照 ComposeView 为单位来持久化数据。

因为你 ComposeView 的 id 决定了 rememberSaveable 存储数据的位置,如果 Activity/Fragment 范围内如果有多个 ComposeView 使用了同一个 id,则只有第一个 ComposeView 能正常恢复数据,这一点要特别注意

再看一下 androidxRegistry,他由 SavedStateRegistryOwner 提供,而这个 owner 是ComposeView 被 attach 到 Activity 时赋的值,就是 Activity 本身:

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();
    }
    //...
}

mSavedStateRegistryController 会在 Activity 重建时 onCreate 中调用 performRestore;在 onSaveInstanceState 时执行 performSave

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

mSavedStateRegistryController 最终调用到 SavedStateRegistry 的同名方法,看一下 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 是注册 SavedStateProvider 的 Map。 performSave 中调用 Provider 的 saveState 方法获取到 rememberSaveable 中保存的 bundle,然后存入 outBundle 进行持久化。

至此,rememberSaveable 在 Android 平台 完成了横竖屏切换时的状态保存。

最后我们用一个图收尾,红色是保存数据时的数据流流向,绿色是恢复数据时的数据流流向:

以上就是Compose状态保存rememberSaveable原理解析的详细内容,更多关于Compose rememberSaveable的资料请关注我们其它相关文章!

时间: 2022-11-15

Android动效Compose贝塞尔曲线动画规格详解

目录 正文 贝塞尔曲线 解析动画曲线 曲线源码分析 总结 正文 写Compose动画的时候使用animateXAsState的时候会注意到一个参数——animationSpec,如下: val borderRadius by animateIntAsState( targetValue = if (isRound) 100 else 0, animationSpec = tween( durationMillis = 3000, easing = LinearEasing ) ) 此处就不深入探

docker资源限制和compose部署详解

目录 一.私有仓库建立 二.Cgroup 资源配置方法 三.CPU使用率控制 使用 stress 工具测试 CPU 和内存 四. CPU 周期限制 五. CPU Core 控制 六. CPU 配额控制参数的混合使用 七. 内存限额 八.Block IO 的限制 九. bps 和 iops 的限制 十. 构建镜像(docker build)时指定资源限制 十一. compose部署 十二. consul部署 总结 一.私有仓库建立 docker pull registry 在docker 引擎终端

Jetpack Compose常用组件详细介绍

目录 1. Text 2. Image 3. LazyColumn 1. Text 日常最常用的应该就是显示文字,所以有必要说一下Text控件.首先源码如下: @Composable fun Text( text: String, modifier: Modifier = Modifier, color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = nu

Jetpack&nbsp;Compose自定义动画与Animatable详解

目录 AnimationSpec 1.spring 2.tween 3.keyframes 4.repeatable 5.snap Animatable 本篇主要是自定义动画与Animatable. AnimationSpec 上一篇中,出现了多次animationSpec属性,它是用来自定义动画规范的.例如: fun Modifier.animateContentSize( animationSpec: FiniteAnimationSpec<IntSize> = spring(), fin

Jetpack Compose Canvas绘制超详细介绍

目录 1. Canvas 2. 绘制方法 1. drawLine 2. drawRect 3. drawRoundRect 4. drawImage 5. drawCircle 6. drawArc 7. drawPath 8. drawPoints 3. DrawScope拓展方法 1. inset 2. translate 3. rotate与rotateRad 4. scale 5. clipRect 6. drawIntoCanvas 7. withTransform 4.参考 1. C

Jetpack Compose状态专篇精讲

目录 1.remember 2.rememberSaveable 3.状态提升 4.状态管理 将Composable作为可信来源 将状态容器作为可信来源 将 ViewModel 作为可信来源 应用中的状态是指可以随时间变化的任何值.这是一个非常宽泛的定义,从 Room 数据库到类的变量,全部涵盖在内. 由于Compose是声明式UI,会根据状态变化来更新UI,因此状态的处理至关重要.这里的状态你可以简单理解为页面上展示的数据,那么状态管理就是处理数据的读写. 1.remember remembe

MySQL视图和索引专篇精讲

目录 视图View 代码实现: 索引index 建立索引 删除索引 数据库版本:mysql8.0.27 如果以下代码执行有问题欢迎一起探讨 视图View 什么是视图? 视图是一个虚拟表,是sql语句的查询结果,其内容由查询定义.同真实的表一样,视图包含一系列带有名称的列和行数据,在使用视图时动态生成.视图的数据变化会影响到基表,基表的数据变化也会影响到视图[insertupdate delete ] ; 创建视图需要create view 权限,并且对于查询涉及的列有select权限:使用cre

C/C++堆区专篇精讲

目录 malloc free new delete memcpy memset malloc malloc开辟堆区内存.头文件stdlib.h,函数原形如下. void*malloc(size_tsize); //返回值void指针,该指针就是开辟的内存地址,参数是开辟的字节数. free free释放堆区开辟的内存.头文件stdlib.h,函数原形如下. voidfree(void*ptr); // 参数传入需要释放的堆区内存首地址. 程序: #include<iostream> #incl

Jetpack Compose 实现一个图片选择框架功能

目录 获取图片 拍照策略 NothingCaptureStrategy FileProviderCaptureStrategy MediaStoreCaptureStrategy 总结 拍照权限 取消拍照导致的脏数据 resolveActivity API 的兼容性 File API 的兼容性 Github 知乎的 Matisse应该蛮多 Android 开发者有了解过或者是曾经使用过,这是知乎在 2017 年开源的一个 Android 端图片选择框架,其颜值在现在看来也还是挺不错的 可惜近几年

剑指Offer之Java算法习题精讲链表专题篇

题目一  解法 /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solut

剑指Offer之Java算法习题精讲二叉树专题篇下

题目一  解法 /** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; *

剑指Offer之Java算法习题精讲二叉树专题篇上

来和二叉树玩耍吧~ 题目一 解法 /** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val

利用Jetpack&nbsp;Compose实现经典俄罗斯方块游戏

目录 可组合函数 游戏机身 - TetrisBody 游戏按钮 - TetrisButton 游戏屏幕 - TetrisScreen 调度器 - TetrisViewModel 项目地址 你的童年是否有俄罗斯方块呢,本文就来介绍如何通过 Jetpack Compose 实现一个俄罗斯方块 ~~ 先看下效果图,功能还是挺完善的 就我自己的体验来说,使用 Compose 开发的应用我感受不到和 Android 原生开发之间有什么性能差异,但 Compose 在开发难度上会低很多 Google 官网上

vue-路由精讲 二级路由和三级路由的作用

1.我们继续上一个案例 vue -- 路由精讲制作导航 -- 从无到有 ,在 about文件夹下 创建一些文件夹.如下: 2.编写about.vue代码.当我们点击导航中 "关于我们" ,就会显示该部分内容.代码中写了四个可供点击后可以跳转的模块.和 <router-view></router-view> 表示你点击了哪个组件,哪个组件就会渲染到这里来. 其中注意:css样式,我们直接引入bootstrap中的导航的样式,在标签中直接添加class属性的值就可以

Android&nbsp;Jetpack&nbsp;Compose无限加载列表

目录 前言 方法一: paging-compose 方法二:自定义实现 添加 LoadingIndicator 总结 前言 Android 中使用 ListView 或者 RecycleView 经常有滚动到底部自动 LoadMore 的需求,那么在 Compose 中该如何实现呢? 两种方法可供选择: 基于 paging-compose 自定义实现 方法一: paging-compose Jetpack 的 Paging 组件提供了对 Compose 的支持 dependencies { ..

通过Jetpack&nbsp;Compose实现双击点赞动画效果

目录 实现步骤 先红色画个爱心 点击事件加动画 完整代码 效果图 实现步骤 先红色画个爱心 Icon( Icons.Filled.Favorite, "爱心", Modifier .align(Alignment.Center) tint = Color.Red ) 点击事件加动画 双击监听 .pointerInput(Unit) { detectTapGestures( onDoubleTap = { ... } ) } #### **API 介绍** | API名称 | 作用 |