在Compose中轻松使用异步dispatch管理全局状态

写在前面

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

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

useSelector、useDispatch 足够好用么?

在上一次更新中,为了解决全局状态的管理问题,我们引入了新的钩子:useSelectoruseDispatch

它们的源码非常简单

1
2
3
4
5
6
7
8
9
10
11
@Composable
inline fun <reified T> useSelector(): T {
val map = useContext(context = ReduxContext)
return map.first[T::class] as T
}

@Composable
inline fun <reified A> useDispatch(): Dispatch<A> {
val map = useContext(context = ReduxContext)
return map.second[A::class] as Dispatch<A>
}

得力于 kotlin 的 inlinereified 关键字,我们可以轻松的从store中取出我们的状态、以及dispatch函数。

但是有时我们并不需要整个状态对象,我们可能只需要其中部分成员属性,亦或者需要对状态中的某个属性进行变形映射

说到这里不知道你有没有想到什么?还记得么你可能一直在kt文件中写Java代码?没错就是 run 映射

更好用的 useSelector

我们只需要简单的构建一个重载函数,就可以让 useSelector 变得更好用:

1
2
@Composable
inline fun <reified T, R> useSelector(block: T.() -> R) = useSelector<T>().run(block)

现在我们继续对之前例子的代码进行改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Composable
private fun SubSimpleDataStateText() {
/**
* 使用[useSelector]的另一个重载,你可以轻松的对状态进行变形,或者只取状态对象的部分属性作为你要关注的状态;
*/
val name = useSelector<SimpleData, String> { name }
Text(text = "User Name: $name")
}

@Composable
private fun SubSimpleDataStateText2() {
// age属性类型是Int,我们可以轻松的进行数据变形
val age = useSelector<SimpleData, String> { "age : $age" }
Text(text = "User $age")
}

更好用的 useDispatch

dispatch 函数非常非常简单:typealias Dispatch<A> = (A) -> Unit

1
{ action: Any -> setState(reducer(state, action)) }

但是在异步场景,使用它有一点点麻烦,例如一个网络请求的场景:

1
2
3
4
5
6
7
8
9
val scope = rememberCoroutineScope() //获取协程作用域
TButton(text = "changeName") {
scope.launch {
// 这里在异步任务
delay(1.seconds)
val result = //....
dispatch(SimpleAction.ChangeName(result)) //dispatch
}
}

多多少少我们要写一点模板代码;

我们继续对 useDispatch 进行改造,增加一个异步版本的:

1
2
3
4
5
6
7
8
9
10
11
12
typealias DispatchAsync<A> = (block: suspend CoroutineScope.() -> A) -> Unit

@Composable
inline fun <reified A> useDispatchAsync(): DispatchAsync<A> {
val dispatch: Dispatch<A> = useDispatch()
val asyncRun = useAsync() //等同于 rememberCoroutineScope().launch {}
return { block ->
asyncRun {
dispatch(block())
}
}
}

改造后的 useDispatchAsync 函数将会返回一个异步版本的 dispatch 函数,函数闭包的返回值将会作为 Action 进行 dispatch 操作。

那么上边的模板代码将会变成:

1
2
3
4
5
6
7
8
val asyncDispatch = useDispatchAsync<SimpleAction>()
TButton(text = "Async changeName") {
asyncDispatch {
delay(1.seconds)
val result = //....
SimpleAction.ChangeName(result) //闭包的最后一行是返回值Action
}
}

使用新的 hook 改造你的 retrofit 请求获得全局状态

如果你使用 retrofit ,并且已经使用协程改造了网络请求,你甚至可以将请求结果作为Action,那么这里将会进一步简化

一个极简的例子如下:

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
//网络状态的封装
sealed interface NetFetchResult {
data class Success(val data: String, val code: Int) : NetFetchResult
data class Error(val msg: Throwable) : NetFetchResult
data object Idle : NetFetchResult
data object Loading : NetFetchResult
}

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

// 注册到store
val store = createStore {
fetchReducer with NetFetchResult.Idle
}

// 组件中使用
@Composable
fun UseReduxFetch() {
val fetchResult: NetFetchResult = useSelector()
val dispatchAsync = useDispatchAsync<NetFetchResult>()
Column {
Text(text = "result: $fetchResult")
TButton(text = "fetch") {
dispatchAsync {
delay(2.seconds)
//网络请求结果
NetFetchResult.Success("success", 200) //这里替换成你的retrofit请求即可
}
}
}
}

探索更多

好了以上就是 hooks 1.0.9 版本带来的一点小小改动,现在你的全局状态可以更加轻松的管理与使用了!

项目开源地址:junerver/ComposeHooks

MavenCentral:hooks

1
implementation("xyz.junerver.compose:hooks:1.0.9")

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