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

写在前面

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

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

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

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

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

kotlin 复制代码
@Serializable
data class BaseResp<T>(
  val data: T? = null,
  val status: Int,
  val message: String? = null
)

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

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

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

kotlin 复制代码
@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来获取值

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

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

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

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

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

kotlin 复制代码
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 生命周期之下,这样在修改名称失败后就对乐观更新实施回滚

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

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

kotlin 复制代码
@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 依赖,直接使用:

kotlin 复制代码
implementation("xyz.junerver.compose:hooks2:2.1.0-alpha0")

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

kotlin 复制代码
implementation("xyz.junerver.compose:hooks2-android:2.1.0-alpha0")

详细迁移说明请查看wiki

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

相关推荐
一條狗11 分钟前
隨筆 20241224 ts寫入excel表
开发语言·前端·typescript
小码快撩16 分钟前
vue应用移动端访问缓慢问题
前端·javascript·vue.js
低调之人20 分钟前
Fiddler勾选https后google浏览器网页访问不可用
前端·测试工具·https·fiddler·hsts
Riesenzahn26 分钟前
使用vue如何监听元素尺寸的变化?
前端·javascript
阿征学IT30 分钟前
圣诞快乐(h5 css js(圣诞树))
前端·javascript·css
程序员黄同学33 分钟前
如何使用 Flask 框架创建简单的 Web 应用?
前端·python·flask
Sword9934 分钟前
豆包 MarsCode AI Apply功能揭秘:自动代码应用与 Diff 实现
前端·人工智能·豆包marscode
前端与小赵34 分钟前
什么是全栈应用,有哪些特点
前端
a1ex34 分钟前
shadcn/ui 动态 pagination
前端
TroubleMaker40 分钟前
OkHttp源码学习之retryOnConnectionFailure属性
android·java·okhttp