写在前面
本文中提及的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
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 } else { myError = BusinessErrors(resp.status, resp.message) } } if (reqErr.asBoolean()) { myError = reqErr } }
fun mutate(mutateFn: (TData?) -> TData) { myData = mutateFn(myData) }
return with(holder) { RequestHolder( data = myData, isLoading = isLoading, error = myError, request = request, 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)
Row { TButton(text = "changeName") { mockFnChangeName(input.value) if (userInfo.asBoolean()) { mutate { it!!.copy(name = input.value) } } setInput("") } TButton(text = "rollback") { previous?.let { mutate { _ -> it } } } }
|
previous?.let { mutate { _ -> it } }
这行代码可以放到 mockFnChangeName
的 onError
生命周期之下,这样在修改名称失败后就对乐观更新实施回滚
如果你有大量的乐观更新场景,每次都要写这么一堆代码,无疑是很麻烦的一件事,那么我们是否可以在每次请求成功之后保存成功状态,然后对外暴露一个函数,使用这个成功状态用作回滚。
完全可以,我们只需要写一个自定义插件就可以实现这一目标:
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
fun rollback() { pervState?.let { fetchInstance.setState(it.asMap()) } }
override val invoke: GenPluginLifecycleFn<TData> get() = { fetch: Fetch<TData>, options: RequestOptions<TData> -> initFetch(fetch, options) object : PluginLifecycle<TData>() { override val onMutate: PluginOnMutate<TData> get() = { pervState = fetch.fetchState } } } }.also { ref.current = it::rollback } }
@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 = { }) val requestHolder = useRequest( requestFn = requestFn, optionsOf = optionsOf, plugins = arrayOf({ useRollbackPlugin(ref = rollbackRef) }) ) return with(requestHolder) { tuple( data, isLoading, error, request, mutate, refresh, cancel, eighth = { rollbackRef.current.invoke() } ) } }
|
探索更多
好了以上就是 使用 hooks 的一些小小技巧,现在你可以自由的扩展 useRequest
来满足你对网络请求的个性化需求。
示例源码地址:Mutate 、CustomPlugin
项目开源地址: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。