-
Notifications
You must be signed in to change notification settings - Fork 5
useRequest
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()
}
)
你只需要将在 requestFn
参数赋值一个lambda闭包,在这个闭包中调用一个suspend
修饰的异步函数即可
在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的 loading
, data
, error
等状态。
useRequest
的返回值是一个 七元组,它的类型如下:
Tuple7<TData?, Boolean, Throwable?, ReqFn, MutateFn<TData>, RefreshFn, CancelFn>
-
data
异步数据 -
loading
状态 -
error
错误 -
req
请求函数 -
mutate
修改函数 -
refresh
刷新函数 -
cancel
取消请求函数
我们使用 kotlin 的解构语法可以轻松的获取他们。
req
请求函数是一个同步函数,它会使用当前组件的协程作用域,你只需要把他当作同步函数在组件中调用即可。
useRequest
的第二个参数是 optionsOf
,你可以通过这个参数完成各种请求状态管理的配置。
如果设置了 options.manual = true
,则 useRequest
不会默认执行,需要通过 req
来触发执行。
import xyz.junerver.compose.hooks.invoke
val (data, loading, error, req) = useRequest(
requestFn = {
mockRequestArticle()
},
optionsOf = {
manual = true
}
)
TButton(text = "manual request") { req() }
你可以使用顶层函数optionsOf
来方便的创建选项,这个函数支持多个hooks,在本项目中所有需要设置配置的场合都可以是用这个函数
optionsOf
函数已经废弃,请使用optionsOf
参数传递闭包进行配置,你只需要简单的全局替换options = optionsOf {
为optionsOf = {
req
函数的签名为:(Array<Any?>) -> Unit
,你需要手动导入import xyz.junerver.compose.hooks.invoke
,这样你就可以将它如同普通函数一样使用
上面我们局的例子的异步请求是没有参数的,但实际场景大部分请求都需要传递参数,我们可以通过 options.defaultParams
来为异步请求设置默认参数:
useRequest(
requestFn = { NetApi.userInfo(it[0] as String) },
optionsOf = {
defaultParams = arrayOf("junerver")
}
)
在闭包中我们可以通过 it[index]
对参数进行类型转换,如果你在 Jvm/Android 平台使用,我提供了一个方便的转换函数 asSuspendNoopFn
例如上面的例子,如果你可以写成这样:
useRequest(
requestFn = NetApi::userInfo.asSuspendNoopFn(),
optionsOf = {
defaultParams = arrayOf("junerver")
}
)
useRequest
提供了以下几个生命周期配置项,供你在异步函数的不同阶段做一些处理。
-
onBefore
:请求之前触发 -
onSuccess
:请求成功触发 -
onError
:请求失败触发 -
onFinally
:请求完成触发
useRequest(
requestFn = { NetApi.userInfo(it[0] as String) },
optionsOf = {
defaultParams = arrayOf("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
方法,使我们可以使用上一次的参数,重新发起请求。
假如在读取用户信息的场景中
- 我们读取了 ID 为
junerver
的用户信息req("junerver")
- 我们通过某种手段更新了用户信息
- 我们想重新发起上一次的请求,那我们就可以使用
refresh()
来代替req("junerver")
,这在复杂参数的场景中是非常有用的
useRequest
提供了 mutate
, 支持立即修改 useRequest
返回的 data
参数。
val (userInfo, loading, _, _, mutate) = useRequest(
requestFn = { NetApi.userInfo(it[0] as String) },
optionsOf = {
defaultParams = arrayOf("junerver")
}
)
TButton(text = "changeName") {
// 你可以在发起修改的同时,使用 mutate 进行乐观更新
mockFnChangeName(newName)
if (userInfo.asBoolean()) {
mutate {
it!!.copy(name = input)
}
}
}
mutate
函数的签名是:fun mutate(mutateFn: (TData?) -> TData)
useRequest
提供了 cancel
函数,用于忽略当前异步请求返回的数据和错误
注意:调用 cancel
函数并不会立即停止 协程job 的执行(这涉及协程的取消机制)
同时 useRequest
会在以下时机自动忽略响应:
- 组件卸载时,正在进行的 promise
- 竞态取消,当上一次 promise 还没返回时,又发起了下一次 promise,则会忽略上一次 promise 的响应
通过设置 options.loadingDelay
,可以延迟 loading
变成 true
的时间,有效防止闪烁。
这在一些快速反回结果的场景将会很有用,简单来说,只要接口响应时间小于你设置的 loadingDelay
,loading
状态将会保持 fasle
。
val (userInfo, loading, _, request) = useRequest(
requestFn = { NetApi.userInfo(it[0] as String) },
optionsOf = {
defaultParams = arrayOf("junerver")
loadingDelay = 1.seconds
}
)
通过设置 options.pollingInterval
,进入轮询模式,useRequest
会定时触发 service 执行。
val (userInfo, loading) = useRequest(
requestFn = { NetApi.userInfo(it[0] as String) },
optionsOf = {
defaultParams = arrayOf("junerver")
pollingInterval = 3.seconds
pollingWhenHidden = true
pollingErrorRetryCount = 5
}
)
-
pollingInterval
轮询间隔时间 -
pollingWhenHidden
后台仍然发起轮询 -
pollingErrorRetryCount
轮询最大错误重试次数
通过设置 options.ready
,可以控制请求是否发出。当其值为 false
时,请求永远都不会发出。
其具体行为如下:
- 当
manual=false
自动请求模式时,每次ready
从false
变为true
时,都会自动发起请求,会带上参数options.defaultParams
。 - 当
manual=true
手动请求模式时,只要ready=false
,则通过req
触发的请求都不会执行。
它非常适合用在链式请求场景,例如:
// 请求1
val (userInfo, userLoading) = useRequest(
requestFn = { NetApi.userInfo(it[0] as String) },
optionsOf = {
defaultParams = arrayOf("junerver")
}
)
// 请求2
val (repoInfo, repoLoading) = useRequest(
requestFn = { NetApi.repoInfo(it[0] as String, it[1] as String) },
optionsOf = {
defaultParams = arrayOf(
userInfo?.login,
"ComposeHooks"
)
ready = userInfo.asBoolean()
}
)
请求2
在 请求1
成功后更新 ready
与 defaultParams
,并自动进行发出请求。
通过设置 options.refreshDeps
,在依赖变化时, useRequest
会自动调用 refresh
方法,实现刷新(重复上一次请求)的效果。
val (state, setState) = useGetState(0)
val (userInfo, loading, error) = useRequest(
requestFn = { NetApi.userInfo(it[0] as String) },
optionsOf = {
defaultParams = arrayOf("junerver")
refreshDeps = arrayOf(state)
}
)
options.refreshDeps
是一个数组,需要通过 arrayOf
传递,在一些更新数据场景它将非常好用。
通过设置 options.debounceWait
,进入防抖模式,此时如果频繁触发 req
,则会以防抖策略进行请求。
val (userInfo, loading, _, req) = useRequest(
requestFn = { NetApi.userInfo(it[0] as String) },
optionsOf = {
defaultParams = arrayOf("junerver")
debounceOptions = DebounceOptions.optionOf { wait = 3.seconds }
}
)
详情可见 useDebounce
节流模式同理:
val (userInfo, loading, _, request) = useRequest(
requestFn = { NetApi.userInfo(it[0] as String) },
optionsOf = {
defaultParams = arrayOf("junerver")
throttleOptions = ThrottleOptions.optionOf { wait = 3.seconds }
}
)
会节流,这两者只会有一个生效,同时配置则节流
如果设置了 options.cacheKey
,useRequest
会将当前请求成功的数据缓存起来。下次组件初始化时,如果有缓存数据,会优先返回缓存数据,然后在背后发送新请求,也就是 SWR 的能力。
你可以通过 options.staleTime
设置数据保持新鲜时间,在该时间内,我们认为数据是新鲜的,不会重新发起请求。
你也可以通过 options.cacheTime
设置数据缓存时间,超过该时间,我们会清空该条缓存数据。
下面的示例,我们设置了 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 = {
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 = {
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[0] as String, it[1] as String)
},
optionsOf = {
defaultParams = arrayOf("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