Pain Points of Web Requests under Compose
Compose’s components are known to be state-driven and, as a functional component, it is constantly reorganizing.
When our component is not visible, the state is removed from the state tree, and if we want to keep the state we need to use a ViewModel to do some state preservation, but the viewModel itself loses state due to cross-page navigation, and each time we re-enter the page we have to re-initiate the request, which is undoubtedly a great deal more difficult than preserving the state of the previous request!
show time !!
1. Create a state store
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
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
}
}
}
The previous article showed that, within the closure scope of the createStore
function, you can use the medial function, with
, to create a store and pass the reducer
function to the store with the initial state;
Similarly you can use the named(alias){}
scope function to create a state store with aliases, where fetch1
and fetch2
are aliases for the request state, and you should use names that actually mean something.
All web requests are the same logic, so we can just use forEach to bulk create state stores with aliases;
The logic of the reducer function here is very simple, because I directly use the Action type to distinguish the state of the network request, so Action is also our State at the same time, if you use a different state than my package you should use your own reducer function logic;
2. Exposure of the state store through ReduxProvider
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeHooksTheme {
ReduxProvider(store = store) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
useRoutes(routes = routes)
}
}
}
}
}
}
Place ReduxProvider
in the root component, global shared state
3. On-demand use
@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(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")
To get the state of the corresponding alias, we need to pass a generic type that is the sealed interface type of the result of our web request. All we need to do is make a when judgment on this type, and we can dynamically handle the display logic for the different states in the component.
useDispatchAsync<NetFetchResult>("fetch1")
Then you can get the corresponding asynchronous dispatch
function, in which we can perform asynchronous operations, and the final result will be passed to the reducer function as an Action.
The signature of dispatchAsync is :
typealias DispatchAsync<A> = (block: suspend CoroutineScope.(Dispatch<A>) -> A) -> Unit
Now you don’t need to make any changes to your past web requests, no ViewModel, no LaunchedEffect, just use retrofit in dispatchAsync to initiate the request!
dispatchAsync { it->
it(NetFetchResult.Loading)
delay(2.seconds)
NetFetchResult.Success(SimpleData("Tony Stark", 53), 200)
}
The it
here is the dispatch function, where you can initiate a state change within the closure, make a try-catch
to your network request, and then just wrap the result or exception using NetFetchResult.Success
or NetFetchResult.Error
!
Further encapsulation, automatic handling of Loading, Error
With the code above, there is still some boilerplate code, e.g. lodaing
status, lack of generalization of the request result, and the need to manually try-catch
the request, which, as I said in my previous comment, just requires a simple wrapper:
typealias ReduxFetch<T> = (block: suspend CoroutineScope.() -> T) -> Unit
@Composable
inline fun <reified T> useFetch(alias: String): ReduxFetch<T> {
val dispatchAsync= useDispatchAsync<NetFetchResult>(alias, onBefore = { it(NetFetchResult.Loading) })
return { block ->
dispatchAsync{
try {
NetFetchResult.Success(block())
} catch (t: Throwable) {
NetFetchResult.Error(t)
}
}
}
}
This package is very simple, before the request is issued first dispatch Loading state, the network request for the pending function to perform try-catch
, respectively, the results | error dispatch out can be;
Changes the location in the component where the useDispatchAsync
function was originally used:
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 {
NetApi.SERVICE.userInfo("junerver")
}
}
}
}
You can see that the simplified dispatchFetch
function only needs to execute the retrofit request within the closure, and the state switching is already handled by useFetch
, so there is basically no template code.
Of course we could go one step further and modify our sealed interface
to add result generalization to it, which would provide more accurate result generalization information when using useSelector
, for example:
sealed interface NetFetchResult<out T> {
data class Success<T>(val data: T) : NetFetchResult<T>
data class Error(val msg: Throwable) : NetFetchResult<Nothing>
data object Idle : NetFetchResult<Nothing>
data object Loading : NetFetchResult<Nothing>
}
So when you need to determine the result of the state, the use of kotlin’s intelligent type conversion can get the correct type, without the above example code to the strong conversion, more friendly, detailed code can see Example Code
Now we have the ultimate network request state management under Compose, it’s very simple and easy to use, you can migrate your retrofit network requests to compose at almost no extra cost, no ViewModel, no need to worry about reorganization leading to loss of state, very Goose Girl Boing !!!!
Lastly, please say ⌈ Thanks Elevate!!!⌋
The Three Musketeers of Condition Management
Up to this point we have introduced three hook functions for state management in Compose:
useReducer
: for practicing MVI, just pass the reducer function with the initial state, return to us the state, dispatch function
useContext
: Used for state elevation to decouple state passing between components, the underlying implementation is:ProvidableCompositionLocal
vs.CompositionLocalProvider
useSelector
/useDispatch
: Global version based onuseContext
implementation.useReducer
The ComposeHooks library is a combination of countless tiny packages, which is why Compose is called a composite, so that we can keep reusing our functions to package more complex combinations.