在 Jetpack Compose 中扩展 useRequest 实现自定义数据处理、异常回滚

写在前面

本文中提及的use开头的函数,都出自与我的 ComposeHooks 项目,它提供了一系列 React Hooks 风格的状态封装函数,可以帮你更好的使用 Compose,无需关心复杂的状态管理,专注于业务与 UI 组件。

这是系列文章的第 9 篇,前文:

在前面的文章中,我们简单的介绍过 useRequest 这个 hook,他被设计的高度抽象,同时也极易扩展,在下面的两个章节中,我将举两个例子,让你在业务中更好的使用它

自定义数据处理、自定义异常

一般来说我们的后台数据都有一个统一的包装格式,大概这样:

1
2
3
4
5
6
@Serializable
data class BaseResp<T>(
val data: T? = null,
val status: Int,
val message: String? = null
)

通常我们只关心我们的业务数据,也就是 data,如果直接使用 useRequest ,我们就需要在 UI 代码中进行解包装,这多少有点麻烦。

另一个就是后台的自定义错误类型,后台将接口报错进行更友好的包装,我们需要判断返回值的状态码 status 来确定业务是否错误,而不是简单的将数据填充到 useRequest 的 data 中。

我们只需要进行如下操作,即可扩展:

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
@Composable
fun <TData : Any> useAsyncRequest(
requestFn: suspend (TParams) -> BaseResp<TData>, // 实际请求的结果是包装类型
optionsOf: RequestOptions<BaseResp<TData>>.() -> Unit = {},
): RequestHolder<TData> {
val holder = useRequest(
requestFn,
optionsOf = optionsOf
)
val resp by holder.data
val reqErr by holder.error

// 自定义的最终返回data与错误
var myData by _useState<TData?>(null)
var myError by _useState<Throwable?>(null)

//监听真实请求
useEffect(resp, reqErr) {
if (resp.asBoolean()) {
if (resp.status == 200) {
myData = holder.data?.data // 业务状态为 200 是才设置 data的值
} else {
myError = BusinessErrors(resp.status, resp.message) // 否则设置为业务错误
}
}
if (reqErr.asBoolean()) {
myError = reqErr
}
}

fun mutate(mutateFn: (TData?) -> TData) {
myData = mutateFn(myData)
} //mutate函数修改自定义的状态

return with(holder) {
RequestHolder(
data = myData, //替换为自定义的data
isLoading = isLoading,
error = myError,//替换为自定义error
request = request,
mutate = ::mutate, // 替换为自定义mutate函数
refresh = refresh,
cancel = cancel
)
}
}

这里的返回值并不需要与我一致,你如果不需要那么多函数完全可以自定义一个类型,或者使用 tuple 元组直接返回暴露

ps: 在后续版本,data、loading、error 将会转为 State<TData>\ State<Boolean> \ State<Throwable>,届时,你需要使用by来获取值
进行自定义封装时如果使用 RequestHolder 作为返回值要注意类型区别

自定义插件扩展 useRequest 实现 mutate 回滚

之前我们介绍过,可以通过调用 mutate 函数实现乐观更新,乐观更新的概念我们不再复述.

如果乐观更新失败我们如何对数据回滚呢?

在直接使用 useRequest 的情况下,可以调用 usePrevious 来暂存 data 的上一个状态,在失败后调用 mutate 将上个状态回滚

例如一个修改用户名的场景 :

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
val (userInfoState, loadingState, _, _, mutate) = useRequest(
requestFn = { NetApi.userInfo(it[0] as String) },
optionsOf = {
defaultParams = arrayOf("junerver")
}
)
val userInfo by userInfoState
val previous by usePrevious(present = userInfo) // previous保存上一个状态

Row {
TButton(text = "changeName") {
mockFnChangeName(input.value) // 在这里发起修改名称的请求
if (userInfo.asBoolean()) {
// 调用mutate函数实施乐观更新
mutate {
it!!.copy(name = input.value)
}
}
setInput("")
}
TButton(text = "rollback") {
// 回滚
previous?.let { mutate { _ -> it } }
}
}

previous?.let { mutate { _ -> it } } 这行代码可以放到 mockFnChangeNameonError 生命周期之下,这样在修改名称失败后就对乐观更新实施回滚

如果你有大量的乐观更新场景,每次都要写这么一堆代码,无疑是很麻烦的一件事,那么我们是否可以在每次请求成功之后保存成功状态,然后对外暴露一个函数,使用这个成功状态用作回滚。

完全可以,我们只需要写一个自定义插件就可以实现这一目标:

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
@Composable
private fun <TData : Any> useRollbackPlugin(ref: MutableRef<() -> Unit>): Plugin<TData> = remember {
object : Plugin<TData>() {
var pervState: FetchState<TData>? = null // 保存上一次请求成功的状态

// 最终实现的rollback函数
fun rollback() {
pervState?.let { fetchInstance.setState(it.asMap()) }
}

override val invoke: GenPluginLifecycleFn<TData>
get() = { fetch: Fetch<TData>, options: RequestOptions<TData> ->
initFetch(fetch, options) // 自定义插件必须要调用 initFetch 函数
object : PluginLifecycle<TData>() {
override val onMutate: PluginOnMutate<TData>
get() = {
pervState = fetch.fetchState //我们将状态保存时机放在 onMutate 这个生命周期中
}
}
}
}.also { ref.current = it::rollback } //将rollback函数通过ref进行回传,实现子向父转递
}

// 使用自定义插件扩展后的 useRequest
@Composable
fun <TData : Any> useCustomPluginRequest(
requestFn: suspend (TParams) -> TData,
optionsOf: RequestOptions<TData>.() -> Unit = {},
): Tuple8<State<TData?>, State<Boolean>, State<Throwable?>, ReqFn, MutateFn<TData>, RefreshFn, CancelFn, RollbackFn> {
val rollbackRef = useRef(default = { }) // 在父hook创建 Ref 容器
val requestHolder = useRequest(
requestFn = requestFn,
optionsOf = optionsOf,
plugins = arrayOf({
useRollbackPlugin(ref = rollbackRef) //将ref传递给自定义插件函数
})
)
return with(requestHolder) {
tuple(
data,
isLoading,
error,
request,
mutate,
refresh,
cancel,
eighth = { rollbackRef.current.invoke() } // 最终通过 ref 来实现对外暴露 rollback 函数
)
}
}

探索更多

好了以上就是 使用 hooks 的一些小小技巧,现在你可以自由的扩展 useRequest 来满足你对网络请求的个性化需求。

示例源码地址:MutateCustomPlugin

项目开源地址: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。