在 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。

相关推荐
瑞金彭于晏2 分钟前
Vue CLI项目创建指南:选择预设与包管理器(PNPM vs NPM)
前端·vue.js·npm
HuStoking9 分钟前
Flutter 实现骨架屏
前端·flutter
奔跑吧邓邓子19 分钟前
HTML开发指南
前端·html
Easonmax20 分钟前
【HTML5】html5开篇基础(2)
前端·html·html5
GEEKVIP33 分钟前
升级 Windows 后如何恢复丢失的文件
android·windows·安全·macos·智能手机·电脑·笔记本电脑
奔跑吧邓邓子43 分钟前
CSS开发全攻略
前端·css
宇珩前端踩坑日记1 小时前
深入解析 CSS calc():实现动态布局的终极利器
前端·css
@山海@1 小时前
在Vue中使用ECharts与v-if的问题及解决方案
前端·javascript·vue.js
小刘不知道叫啥2 小时前
从异步传染浅谈代数效应
前端·javascript·reactjs
圈圈的熊2 小时前
EZUIKit.js萤石云vue项目使用
前端·javascript·vue.js