Skip to content

useRequest

Junerver Hou edited this page Sep 29, 2025 · 18 revisions

迁移至参数类型安全版本(hooks2:2.2.1)

过去 useRequest 的函数抽象为:

typealias TParams = Array<Any?>
internal typealias NormalFunction<TData> = (TParams) -> TData

现在修改为:

internal typealias NormalFunction<TParams, TData> = (TParams) -> TData

你必须对旧代码进行迁移,否则可能会无法通过编译。 相关讨论板:#40

保守迁移

// 使用 suspend lambda 形式
val (userInfoState, loadingState, _, request, _, _, cancel) = useRequest<ArrayParams, UserInfo>(
    requestFn = { NetApi.userInfo(it[0] as String) },
    optionsOf = {
        manual = true
        defaultParams = arrayOf("junerver")
    },
}
// 使用 asSuspendNoopFn
val (repoInfoState, loadingState, errorState, request) = useRequest<ArrayParams, RepoInfo>(
    requestFn = NetApi::repoInfo.asSuspendNoopFn(),
    optionsOf = {
        println("Configure closure execution!")
        manual = true
        defaultParams = arrayOf("junerver", "ComposeHooks")
    },
)

追加泛型声明 ArrayParams,即可继续使用过去的写法,但是需要注意,当 manual = false(默认情况),必须要配置 defaultParams,即使请求函数不依赖外部参数,也需要填写 defaultParams = emptyArray()

*使用元组迁移

import xyz.junerver.compose.hooks.invoke

val (repoInfoState, loadingState, errorState, request) = useRequest(
    requestFn = { it: Tuple2<String, String> -> // 通过元组声明参数列表类型
        NetApi.repoInfo(it.first, it.second)
    },
    optionsOf = {
        manual = true
        defaultParams = tuple("junerver", "ComposeHooks") // 使用 tuple 函数创建默认参数
    },
)
TButton(text = "request with default") {
    request()
}
TButton(text = "request with error params") {
    request("unknow", "unknow")
}

只需要将 requestFn 闭包的参数使用元组声明其类型,用 tuple 函数替换原来的 arrayOf 函数可以迁移到参数类型安全版本,依旧支持 ReqFn 不传递参数时使用默认参数。

无参数异步函数

很多时候我们的 requestFn 是一个无参数的异步挂起函数,这种场景下如果使用自动请求,必须要显式配置 defaultParams = None

也可以使用 noneParams 包装选项 lambda,例如:

    val (dataState, loadingState) = useRequest(
        requestFn = {
            mockRequestArticle()
        },
        optionsOf = noneParams {
            this.cacheKey = cacheKey
            staleTime = 5.seconds
        },
    )

快速上手

useRequest 是一个功能丰富的异步数据管理的 Hooks,Compose 项目中的网络请求场景使用 useRequest 就够了。

useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:

  • 自动请求/手动请求
  • 轮询
  • 防抖
  • 节流
  • 错误重试
  • loading delay
  • SWR(stale-while-revalidate)
  • 缓存

默认用法

suspend fun mockRequestArticle(): MockArticle {
    delay(2000L)
    return MockArticle(Clock.System.now().toEpochMilliseconds(), NanoId.generate(200))
}

val (data, loading, error) = useRequest(
    requestFn = {
        mockRequestArticle()
    },
    optionsOf = {
        defaultParams = None,
    },
)

你只需要将在 requestFn 参数赋值一个lambda闭包,在这个闭包中调用一个suspend修饰的异步函数即可

在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的 loading , data , error 等状态。

基础用法

useRequest 的返回值是一个 data class,它的类型如下:

@Stable
data class RequestHolder<TParams, TData>(
    val data: State<TData?>,
    val isLoading: State<Boolean>,
    val error: State<Throwable?>,
    val request: ReqFn<TParams>,
    val mutate: MutateFn<TData>,
    val refresh: RefreshFn,
    val cancel: CancelFn,
)
  1. data 异步数据
  2. loading 加载状态
  3. error 错误
  4. req 请求函数
  5. mutate 修改函数
  6. refresh 刷新函数
  7. cancel 取消请求函数

我们使用 kotlin 的解构语法可以轻松的获取他们。

request 请求函数是一个同步函数,它会使用当前组件的协程作用域,你只需要把他当作同步函数在组件中调用即可。

手动触发

useRequest 的第二个参数是 optionsOf,你可以通过这个参数完成各种请求状态管理的配置。

如果设置了 options.manual = true,则 useRequest 不会默认执行,需要通过 request 来触发执行。

import xyz.junerver.compose.hooks.invoke

val (data, loading, error, req) = useRequest(
    requestFn = { it:None-> // 无参数的异步请求要明确声明参数类型为 None
        mockRequestArticle()
    },
    optionsOf = {
        manual = true
    }
)

TButton(text = "manual request") { req() }

request函数的签名为:(TParams) -> Unit,推荐使用 TupleN作为参数容器。这样,你可以手动导入import xyz.junerver.compose.hooks.invoke,就可以将它如同普通函数一样使用

invoke 支持两种模式来触发请求:

  1. 不传递任何内容,此时将使用配置的默认参数发起请求
  2. 如同普通函数一样使用,传递对应位置的参数,会提供类型提示

例如下面的代码:

import xyz.junerver.compose.hooks.invoke

Text(text = "Manual:")
TButton(text = "request with default") {
    request()
}
TButton(text = "request with error params") {
    request("unknow", "unknow")
}

request 函数的类型为:val request: ReqFn<Tuple2<String, String>>

默认参数

上面我们局的例子的异步请求是没有参数的,但实际场景大部分请求都需要传递参数,我们可以通过 options.defaultParams 来为异步请求设置默认参数:

useRequest(
    requestFn = { NetApi.userInfo(it.first) },
    optionsOf = {
        defaultParams = tuple("junerver")
    }
)

在闭包中我们可以通过 it.indexN 来使用对应顺序的函数参数,如果你在 Jvm/Android 平台使用,我提供了一个方便的转换函数 asSuspendNoopFn

例如上面的例子,如果你可以写成这样:

useRequest(
    requestFn = NetApi::userInfo.asSuspendNoopFn(),
    optionsOf = {
        defaultParams = tuple("junerver")
    }
)

生命周期

useRequest 提供了以下几个生命周期配置项,供你在异步函数的不同阶段做一些处理。

  • onBefore:请求之前触发
  • onSuccess:请求成功触发
  • onError:请求失败触发
  • onFinally:请求完成触发
useRequest(
    requestFn = { NetApi.userInfo(it) },
    optionsOf = {
        defaultParams = "junerver"
        onBefore = {
            state += "onBefore: ${it.joinToString("")}"
        }
        onSuccess = { data, _ ->
            println("Lifecycle Lifecycle: onSuccess")
            state += "\n\nonSuccess:\nData:$data"
        }
        onError = { err, pa ->
            state += "\n\nonError: ${pa.joinToString("")}\nError: ${err.message}"
        }
        onFinally = { _, _, _ ->
            state += "\n\nonFinally!"
        }
    }
)

刷新(重复上一次请求)

useRequest 提供了 refresh 方法,使我们可以使用上一次的参数,重新发起请求。

假如在读取用户信息的场景中

  1. 我们读取了 ID 为 junerver 的用户信息 req("junerver")
  2. 我们通过某种手段更新了用户信息
  3. 我们想重新发起上一次的请求,那我们就可以使用 refresh() 来代替 req("junerver"),这在复杂参数的场景中是非常有用的

mutate 立即变更数据

useRequest 提供了 mutate, 支持立即修改 useRequest 返回的 data 参数。

val (userInfo, loading, _, _, mutate) = useRequest(
    requestFn = { NetApi.userInfo(it) },
    optionsOf = {
        defaultParams = "junerver"
    }
)

TButton(text = "changeName") {
    // 你可以在发起修改的同时,使用 mutate 进行乐观更新
    mockFnChangeName(newName)
    if (userInfo.value.asBoolean()) {
        mutate {
            it!!.copy(name = input.value)
        }
    }
}

mutate 函数的签名是:fun mutate(mutateFn: (TData?) -> TData)

取消响应

useRequest 提供了 cancel 函数,用于忽略当前异步请求返回的数据和错误

注意:调用 cancel 函数并不会立即停止 协程job 的执行(这涉及协程的取消机制)

同时 useRequest 会在以下时机自动忽略响应:

  • 组件卸载时,正在进行的 promise
  • 竞态取消,当上一次 promise 还没返回时,又发起了下一次 promise,则会忽略上一次 promise 的响应

Loading Delay

通过设置 options.loadingDelay ,可以延迟 loading 变成 true 的时间,有效防止闪烁。

这在一些快速反回结果的场景将会很有用,简单来说,只要接口响应时间小于你设置的 loadingDelayloading 状态将会保持 fasle

val (userInfo, loading, _, request) = useRequest(
    requestFn = { NetApi.userInfo(it) },
    optionsOf = {
        defaultParams = "junerver"
        loadingDelay = 1.seconds
    }
)

轮询

通过设置 options.pollingInterval,进入轮询模式,useRequest 会定时触发 service 执行。

val (userInfo, loading) = useRequest(
    requestFn = { NetApi.userInfo(it) },
    optionsOf = {
        defaultParams = "junerver"
        pollingInterval = 3.seconds
        pollingWhenHidden = true
        pollingErrorRetryCount = 5
    }
)
  • pollingInterval 轮询间隔时间
  • pollingWhenHidden 后台仍然发起轮询
  • pollingErrorRetryCount 轮询最大错误重试次数

Ready

通过设置 options.ready,可以控制请求是否发出。当其值为 false 时,请求永远都不会发出。

其具体行为如下:

  1. manual=false 自动请求模式时,每次 readyfalse 变为 true 时,都会自动发起请求,会带上参数 options.defaultParams
  2. manual=true 手动请求模式时,只要 ready=false,则通过 req 触发的请求都不会执行。

它非常适合用在链式请求场景,例如:

// 请求1
val (userInfo, userLoading) = useRequest(
    requestFn = { NetApi.userInfo(it) },
    optionsOf = {
        defaultParams = "junerver"
    }
)
// 请求2
val (repoInfo, repoLoading) = useRequest(
    requestFn = { NetApi.repoInfo(it.first, it.second) },
    optionsOf = {
        defaultParams = tuple(
            userInfo.value?.login,
            "ComposeHooks"
        )
        ready = userInfo.value.asBoolean()
    }
)

请求2请求1 成功后更新 readydefaultParams,并自动进行发出请求。

依赖刷新

通过设置 options.refreshDeps,在依赖变化时, useRequest 会自动调用 refresh 方法,实现刷新(重复上一次请求)的效果。

val (state, setState) = useGetState(0)
val (userInfo, loading, error) = useRequest(
    requestFn = { NetApi.userInfo(it) },
    optionsOf = {
        defaultParams = "junerver"
        refreshDeps = arrayOf(state.value)
    }
)

options.refreshDeps 是一个数组,需要通过 arrayOf 传递,在一些更新数据场景它将非常好用。

防抖 & 节流

通过设置 options.debounceWait,进入防抖模式,此时如果频繁触发 req,则会以防抖策略进行请求。

val (userInfo, loading, _, req) = useRequest(
    requestFn = { NetApi.userInfo(it) },
    optionsOf = {
        defaultParams = "junerver"
        debounceOptionsOf = { wait = 3.seconds } // 使用该参数配置
    }
)

详情可见 useDebounce

节流模式同理:

val (userInfo, loading, _, request) = useRequest(
    requestFn = { NetApi.userInfo(it) },
    optionsOf = {
        defaultParams = "junerver"
        throttleOptionsOf = { wait = 3.seconds }
    }
)

同时设置了防抖、节流会怎样?

会节流,这两者只会有一个生效,同时配置则节流

缓存 & SWR

如果设置了 options.cacheKeyuseRequest 会将当前请求成功的数据缓存起来。下次组件初始化时,如果有缓存数据,会优先返回缓存数据,然后在背后发送新请求,也就是 SWR 的能力。

你可以通过 options.staleTime 设置数据保持新鲜时间,在该时间内,我们认为数据是新鲜的,不会重新发起请求。

你也可以通过 options.cacheTime 设置数据缓存时间,超过该时间,我们会清空该条缓存数据。

SWR

下面的示例,我们设置了 cacheKey,在组件第二次加载时,会优先返回缓存的内容,然后在背后重新发起请求。

@Composable
private fun TestSWR() {
    val (isVisible, toggle) = useBoolean(true)
    Column {
        TButton(text = "show/hide") {
            toggle()
        }
        if (isVisible) {
            SWR()
        }
        HorizontalDivider(modifier = Modifier.fillMaxWidth())
        if (isVisible) {
            SWR(true)
        }
    }
}

@Composable
private fun SWR(useCache: Boolean = false) {
    val (data, loading) = useRequest(
        requestFn = {
            mockRequestArticle()
        },
        optionsOf = {
            defaultParams = None
            if (useCache) cacheKey = "test-swr-key"
        }
    )
    Column(modifier = Modifier.height(210.dp)) {
        Text(text = "cache: $useCache", color = Color.Red)
        Text(text = "Background loading: $loading")
        if (data.asBoolean()) {
            Text(text = "$data")
        }
    }
}

运行可以发现,组件使用cacheKey的组件在退回上一页或者切换显示隐藏时都能首先使用缓存数据显示、然后后台发起请求更新,可以优化体验。

数据保持新鲜

通过设置 staleTime,我们可以指定数据新鲜时间,在这个时间内,不会重新发起请求。

@Composable
private fun StaleTime(cacheKey: String) {
    val (data, loading) = useRequest(
        requestFn = {
            mockRequestArticle()
        },
        optionsOf = {
            defaultParams = None
            this.cacheKey = cacheKey
            staleTime = 5.seconds
        }
    )
    Column(modifier = Modifier.height(210.dp)) {
        Text(text = "statleTime: 5s", color = Color.Red)
        Text(text = "Background loading: $loading")
        if (data.asBoolean()) {
            Text(text = "$data")
        }
    }
}

在设定的 staleTime 之内,即使切换隐藏\显示,都不会发出新的请求。直到时间超出,才会在后台发起请求,更新数据。

数据共享

同一个 cacheKey 的内容,在全局是共享的,这会带来以下几个特性:

  • 请求共享:相同的 cacheKey 同时只会有一个在发起请求,后发起的会共用同一个异步请求的 Deferred
  • 数据同步:当某个 cacheKey 发起请求时,其它相同 cacheKey 的内容均会随之同步

删除缓存

ahooks 提供了一个 clearCache 方法,可以清除指定 cacheKey 的缓存数据。

import xyz.junerver.compose.hooks.userequest.utils.clearCache

@Composable
fun TestStaleTime() {
    val (isVisible, toggle) = useBoolean(true)
    val cacheKey = "test-stale-key"
    Column {
        Text("↓ The following two components use the same 'cacheKey' and they will share the data")
        Row {
            TButton(text = "show/hide") {
                toggle()
            }
            TButton(text = "clearCache") {
                // 通过调用top-level函数 `clearCache` 可以移除指定key的缓存,该函数可以接收多个key
                clearCache(cacheKey)
            }
        }
        if (isVisible) {
            // 相同 cacheKey 的数据全局同步
            StaleTime(cacheKey)
            StaleTime(cacheKey)
        }
    }
}

错误重试

通过设置 options.retryCount,指定错误重试次数,则 useRequest 在失败后会进行重试。

val (mockInfo, stuLoading, err) = useRequest(
    requestFn = {
        mockRequest(it.first, it.second)
    },
    optionsOf = {
        defaultParams = tuple("1", "2")
        retryCount = 5
        retryInterval = 2.seconds
        onError = { _, _ ->
            count += "${Clock.System.now().epochSeconds}\n"
        }
    }
)
  • retryCount 错误重试次数。如果设置为 -1,则无限次重试
  • retryInterval 重试时间间隔。 如果不设置,默认采用简易的指数退避算法,取 (1.seconds * 2f.pow(count).toInt()).coerceAtMost(30.seconds)也就是第一次重试等待 2s,第二次重试等待 4s,以此类推,如果大于 30s,则取 30s
Clone this wiki locally