在Jetpack Compose中管理网络请求竟然如此简单!

写在前面

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

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

在上篇文章中我提到如果你项目中使用的是retrofit,并且已经做了协程改造,那么你可以轻松的将你的网络请求改造到 Compose 中,使用状态驱动你的UI。

上一个例子比较粗糙,可能有小伙伴不理解,同时考虑到减少模板代码,我升级了 useRedux 系列钩子,下面我将演示如何在项目中轻松的使用网络请求,并且不用再担心重组导致请求状态消失!amazing!!!!

Compose 下网络请求的痛点

众所周知,Compose的组件是有状态驱动的,并且作为函数式组件,它会不断地重组。

当我们的组件不可见时,状态从状态树移除,如果想要保留状态就需要使用 ViewModel 来进行一些状态保存,但是 viewModel 本身也会因为跨页面导航丢失状态,每次再进入页面都要重新发起请求,不能保存之前的请求状态无疑是非常制杖的!

那么怎么才能丝滑的使用网络请求呢?如何避免网络请求因为重组再次发起?

答案就是上两篇文章,我们通过 ReduxProvider 将状态提升到最根部,那么全局范围内,同一个网络请求在全局使用相同的状态,就不会出现各种场景下的状态丢失了。

show time !!

1. 创建状态存储 store

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
kotlin复制代码// 请求结果状态封装,同时这也是 Action
sealed interface NetFetchResult {
// 成功状态
data class Success<T>(val data: T, val code: Int) : NetFetchResult
// 错误状态
data class Error(val msg: Throwable) : NetFetchResult
// 初始空闲状态
data object Idle : NetFetchResult
// 请求loading状态
data object Loading : NetFetchResult
}

// reducer
val fetchReducer: Reducer<NetFetchResult, NetFetchResult> = { _, action ->
action
}

// 创建存储对象
val store = createStore {
arrayOf("fetch1","fetch2").forEach {
named(it) {
fetchReducer with NetFetchResult.Idle
}
}
}

上篇文章介绍了,在createStore函数的闭包作用域内,你可以使用中缀函数 with,来创建一条存储,并且将 reducer 函数与初始状态传递给store;

同样的你可以使用 named(alias){} 这个作用域函数,来创建一个带别名的状态存储,这里的fetch1fetch2是请求状态的别名,你应该使用有实际意义的名称。

所有的网络请求都是相同的逻辑,所以我们可以直接使用 forEach 来批量创建具有别名的状态存储;

这里的reducer函数逻辑非常简单,因为我直接使用 Action 类型区分网络请求状态,所以Action 也同时是我们的 State,如果你使用的状态不同于我的封装你应该使用自己的 reducer 函数逻辑;

2. 通过 ReduxProvider 暴露状态存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeHooksTheme {
// 在这里将 store 对象传递给 ReduxProvider 组件,向全部子组件暴露状态
ReduxProvider(store = store) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
useRoutes(routes = routes)
}
}
}
}
}
}

ReduxProvider 置于根组件,全局共享状态

3. 按需使用

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
kotlin复制代码@Composable
fun UseReduxFetch() {
val fetchResult: NetFetchResult = useSelector("fetch1")
val dispatchAsync = useDispatchAsync<NetFetchResult>("fetch1")
Column {
Text(text = "result: $fetchResult")
TButton(text = "fetch") {
dispatchAsync {
it(NetFetchResult.Loading)
delay(2.seconds)
//网络请求结果
NetFetchResult.Success("success", 200)
}
}
}
}

@Composable
fun UseReduxFetch2() {
val fetchResult: NetFetchResult = useSelector("fetch2")
val dispatchAsync = useDispatchAsync<NetFetchResult>("fetch2")
Column {
Text(text = "result: $fetchResult")
// 使用 when 可以方便的判断请求结果状态
when(fetchResult) {
is NetFetchResult.Success<*> -> {
// 对成功结果进行转型
val succ= fetchResult as NetFetchResult.Success<SimpleData>
Text(text = succ.toString())
}
else->{}
}
TButton(text = "fetch2") {
dispatchAsync {
// 请求开始
it(NetFetchResult.Loading)
// 这里假装在进行异步操作
delay(2.seconds)
// 包装网络请求结果
NetFetchResult.Success(SimpleData("Tony Stark", 53), 200)
}
}
}
}

useSelector<NetFetchResult>("fetch1") 即可拿到对应别名的状态,需要传递的泛型就是我们的网络请求结果的 sealed interface 类型。我们只需要对这个类型进 行when 判断,即可在组件中动态处理不同状态的显示逻辑。

useDispatchAsync<NetFetchResult>("fetch1") 则可以拿到对应的 异步dispatch函数,我们可以在这个函数中执行异步操作,最终返回结果将作为 Action 传递给 reducer 函数。

dispatchAsync 的签名是 :

1
2
3
> kotlin复制代码typealias DispatchAsync<A> = (block: suspend CoroutineScope.(Dispatch<A>) -> A) -> Unit
>
>

现在你无需对你过去的网络请求做任何改动,不需要 ViewModel,不需要LaunchedEffect,直接在 dispatchAsync 中使用 retrofit 发起请求!

1
2
3
4
5
kotlin复制代码dispatchAsync { it->
it(NetFetchResult.Loading)
delay(2.seconds) //假装在进行携程上的耗时操作
NetFetchResult.Success(SimpleData("Tony Stark", 53), 200)
}

这里的 it 是 dispatch 函数,你可以在闭包内发起状态变更,对你的网络请求进行 try-catch,然后将结果或者异常使用 NetFetchResult.SuccessNetFetchResult.Error 包装即可!

进一步封装,自动处理Loading、Error

上边的代码,还是存在一些模板代码,例如: lodaing 状态、请求结果缺乏泛型、需要对请求手动try-catch,就如我之前评论所言,只需要简单的进行一个封装即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码// 定义函数一个高级函数ReduxFetch,它接收一个挂起函数作为参数
typealias ReduxFetch<T> = (block: suspend CoroutineScope.() -> T) -> Unit

@Composable
inline fun <reified T> useFetch(alias: String): ReduxFetch<T> {
// 在函数调用时首先 dispatch Loading 状态
val dispatchAsync= useDispatchAsync<NetFetchResult>(alias, onBefore = { it(NetFetchResult.Loading) })
return { block ->
dispatchAsync{
try {
// 这里的block 就是上面定义的ReduxFetch的参数,是一个retrofit挂起函数
NetFetchResult.Success(block())
} catch (t: Throwable) {
NetFetchResult.Error(t)
}
}
}
}

这个封装非常简单,请求发出前首先 dispatch Loading状态,对网络请求的挂起函数执行try-catch,分别将结果|错误 dispatch出去即可;

将组件中原来使用 useDispatchAsync 函数的位置进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码interface WebService {
@GET("users/{user}")
suspend fun userInfo(@Path("user") user: String): UserInfo
}

@Composable
fun UseReduxFetch2() {
val fetchResult: NetFetchResult = useSelector("fetch2")
val dispatchFetch = useFetch<UserInfo>("fetch2")
Column {
Text(text = "result: $fetchResult")
TButton(text = "fetch2") {
dispatchFetch {
// 这里是你的 retrofit suspend 请求,返回值是上面声明的泛型
NetApi.SERVICE.userInfo("junerver")
}
}
}
}

可以看到简化后的 dispatchFetch 函数只需要在闭包内执行 retrofit 请求即可,状态的切换已经都由 useFetch 处理了,基本没有模板代码了。

现在我们迎来了在Compose下的终极网络请求状态管理,非常简单易用,你可以几乎没有额外成本的将你的 retrofit 网络请求迁移到 compose 下,无需 ViewModel、
无需操心重组导致的状态丢失,非常的鹅妹子嘤!!!!

最后请说声:⌈ 多谢提升哥!⌋

状态管理三剑客

到此为止我们已经介绍了三位用于在 Compose 中进行状态管理的钩子函数:

  • useReducer:用于实践MVI,只需要传递 reducer 函数与初始状态,返回给我们状态、dispatch函数
  • useContext:用于状态提升,解耦组件之间的状态传递,底层实现是:ProvidableCompositionLocalCompositionLocalProvider
  • useSelector/useDispatch:基于 useContext 实现的的全局版本的 useReducer

题外话

这里我要补充一句:useSelector/useDispatch主要还是用来进行全局 MVI式状态管理,本文展示的其实只是 全局状态管理 的一个小小的应用场景,如果你想在实际生产中使用,可能需要根据我的例子进行改造。

在 ComposeHooks 中提供的 useRequest 才是真正用于服务端状态管理的工具,详情参考:在Compose中使用useRequest轻松管理网络请求

它的功能更加全面,例如:错误重试、依赖刷新、ready、自动|手动请求、轮询、防抖、节流等等

探索更多

好了以上就是 hooks 1.0.10 版本带来的一点小小改动,现在你可以自信在在Compose中使用网络请求了!

示例源码地址:UseReduxExample

项目开源地址:junerver/ComposeHooks

MavenCentral:hooks

1
kotlin复制代码implementation("xyz.junerver.compose:hooks:1.0.10")

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

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%