在 Jetpack Compose 中轻松使用持久化(使用mmkv持久化状态)

在实际开发中,我们经常会遇到一些字段在应用中需要反复使用。

过去最常见的做法就是在 Application 中申明一个属性,使用它来暂存这个字段,这是非常常见的内存持久化方案,这些暂存内容会在应用退出后丢失,属于短期持久化

另一个场景就是使用 MMKV 这样的键值对持久化工具,将需要持久化的内容保存到本地文件,这些内容退出后不会丢失,再次打开应用时会重新读取加载,属于长期持久化

在 Compose 中需要我们可以使用 junerver/ComposeHooks 中的 usePersistent 来轻松做到这一点。

默认内存持久化-简单的全局状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Composable
private fun DefaultPersistent() {
var count by usePersistent(key = "count", -1)
Column {
Text(text = "DefaultPersistent : exit app will lose state")
TButton(text = "+1") {
count += 1 // 如同使用状态一样,直接进行赋值写操作
}
SubShowCount()
}
}

@Composable
private fun SubShowCount() {
val count by usePersistent(key = "count", -1) //子组件使用同一个key
Text(text = "persistent: $count") // 父组件数值改变时子组件一样跟随改变
}

它开箱即用,你只需要调用 usePersistent(key = "count", -1),传入一个 key 与默认值即可,默认使用内存进行持久化保存。

在应用全局同一个 key 对应同一个对象,一处修改、处处生效。

自定义持久化-使用 mmkv

除了内存持久化,usePersistent 同样支持自定义持久化方案,例如使用 mmkv:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
val mmkv = MMKV.defaultMMKV()

fun mmkvSave(key: String, value: Any?) {
when (value) {
is Int -> mmkv.encode(key, value)
is Long -> mmkv.encode(key, value)
is Double -> mmkv.encode(key, value)
is Float -> mmkv.encode(key, value)
is Boolean -> mmkv.encode(key, value)
is String -> mmkv.encode(key, value)
is ByteArray -> mmkv.encode(key, value)
is Parcelable -> mmkv.encode(key, value)
} notifyDefaultPersistentObserver(key) //必须要调用该函数触发状态修改
}

fun mmkvGet(key: String, value: Any): Any {
return when (value) {
is Int -> mmkv.decodeInt(key, value)
is Long -> mmkv.decodeLong(key, value)
is Double -> mmkv.decodeDouble(key, value)
is Float -> mmkv.decodeFloat(key, value)
is Boolean -> mmkv.decodeBool(key, value)
is String -> mmkv.decodeString(key, value)
is ByteArray -> mmkv.decodeBytes(key, value)
is Parcelable -> mmkv.decodeParcelable(key, value.javaClass)
else -> error("wrong type of default value!")
} as Any
}

fun mmkvClear(key: String) {
mmkv.remove(key)
notifyDefaultPersistentObserver(key) //必须要调用该函数触发状态修改
}


PersistentContext.Provider( // 使用该Provider提供一个三元组
value = Triple(
first = ::mmkvGet,
second = ::mmkvSave,
third = ::mmkvClear
)
) {
val (hideKeyboard) = useKeyboard()
var token by usePersistent(key = "token", "") // 此时对状态的读写持久化到mmkv
val (state, setState) = useGetState("")
Column {
Text(text = "MMKVPersistent : exit app will NOT lose state")
Text(text = "token: $token")
OutlinedTextField(value = state.value, onValueChange = setState)
TButton(text = "saveToken") {
hideKeyboard()
token = state.value
setState("")
println("now you can exit app,and reopen")
}
MMKVPersistentSub()
}
}

自定义持久化时需要使用 PresistentContext.Provider 这个容器组件,向这个组件提供一个三元组 Triple<PersistentGet, PersistentSave, PersistentClear>

这个三元组接收三个函数,分别对应了持久化获取、保存、移除这三个操作,现在处于 PresistentContext.Provider 这个容器组件下的 usePersistent 函数将会使用自定义的 mmkv 进行持久化操作。

自定义存储与自定义的移除操作时请务必调用 notifyDefaultPersistentObserver(key) 这个函数来通知 hook 状态变更了

清除持久化内容

除了读写,在有的场景下,我们需要移除持久化内容,这种场景下就不能继续使用 by 关键字进行委托了,需要直接使用 =

usePersistent 函数的返回值是一个 data class ,我们可以使用解构声明轻松的获取其成员:

1
2
3
4
5
6
7
8
9
10
11
12
@Stable
data class PersistentHolder<T>(
val state: State<T>,
val save: SaveToPersistent<T>,
val clear: HookClear,
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = state.value

operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
save(newValue)
}
}

我们需要进行如下操作,修改 by=,通过解构声明获取第三个元素的值就是 clear 函数

1
2
3
4
5
@Composable
private fun SubShowCount() {
val (countState,_,clear) = usePersistent(key = "count", -1)
Text(text = "persistent: ${countState.value} ,click to clear",modifier = Modifier.clickable { clear() })
}

这时我们调用 clear 函数,将会移除这个 key 对应的存储内容,并将状态修改为默认值

在自定义存储容器组件下强制使用内存持久化

在使用了自定义持久化的情况下,PresistentContext.Provider 这个容器组件下所有的 usePresistent 函数将默认使用自定义持久化方案,可以通过配置参数 forceUseMemory = true 来强制使用内存持久化:

1
2
3
4
5
6
7
8
9
10
@Composable
private fun MMKVPersistentSub() {
val (token, _, clear) = usePersistent(key = "token", "321")
var clearCount by usePersistent(key = "clearCount", 0, forceUseMemory = true) // 这个key对应的状态将强制使用内存进行持久化
Text(text = "sub component token: ${token.value} ,click to clear", modifier = Modifier.clickable {
clear()
clearCount += 1
})
Text("current clear count: $clearCount")
}

探索更多

好了以上就是 使用 hooks 的一些小小技巧,现在你可以使用 usePresistent 来轻松的使用全局状态、持久化状态!

示例源码地址:UsePersistentExample

项目开源地址:junerver/ComposeHooks

MavenCentral:hooks2

本项目已经迁移到 Compose Multiplatform ,使用新的工件 id:hooks2

如果你在 CMP 依赖,直接使用:

1
implementation("xyz.junerver.compose:hooks2:2.1.0-alpha0")

如果你在 Android 环境依赖,请使用 id:hooks2-android

1
implementation("xyz.junerver.compose:hooks2-android:2.1.0-alpha0")

详细迁移说明请查看wiki

欢迎使用、勘误、pr、star。