kotlin 流
MVI stands for Model-View-Intent.
MVI代表Model-View-Intent 。
Recently this has been a hot topic in our developer community and there has been lot of libraries that solve this problem, but it becomes too overwhelming to understand the actual inner working behind it.
最近,这已成为我们开发人员社区中的热门话题,并且有很多库可以解决此问题,但是对于了解其背后的实际内部工作而言,它变得势不可挡。
If you are new to MVI I’d suggest you to read a thorough series of post http://hannesdorfmann.com/android/mosby3-mvi-1
如果您不熟悉MVI,建议您阅读一系列详尽的文章http://hannesdorfmann.com/android/mosby3-mvi-1
In this post we would go through how we can setup our own uni-directional data flow architecture using coroutines & flow in kotlin multi-platform.
在这篇文章中,我们将介绍如何在Kotlin多平台中使用协程和流来设置自己的单向数据流体系结构。
To build this MVI flow we would be looking at following interface
要构建此MVI流,我们将查看以下界面
Action : It is mapped as user input in the screen like click or swipe.
行动 :它被映射为用户点击在屏幕上的输入,如点击或滑动。
Result: The actions or the user input are passed to viewmodel. It filters the actions by it type and process it. Every action generates a Result. A new State is created by copying the current State and changing the necessary bits according to the Result.
结果 :操作或用户输入被传递到视图模型。 它按类型过滤并处理操作。 每个动作都会生成一个Result 。 通过复制当前状态并根据结果更改必要的位来创建新状态。
State: It is the View state of the screen. Whatever the View shows on screen is represented by a state.
状态:这是屏幕的查看状态。 视图在屏幕上显示的任何内容均由状态表示。
This interface are than subclass by sealed classes, except for state that is a data class. So using sealed class this makes sure every action is mapped to a result.
该接口是密封类的子类,但状态是数据类。 因此,使用密封类可确保将每个操作映射到结果。
This modeling incorporates every actions or input the user can trigger and what will be the resultant change in the screen.
这种建模方法结合了用户可以触发的每项动作或输入,以及屏幕上将发生的最终变化。
data class NewsListState(
val loading: Boolean = false,
val newsList: NewsList? = null,
val error: String = "",
val showError: Boolean = false
) : State
sealed class NewsListAction : Action {
object LoadNews : NewsListAction()
}
sealed class NewsListResult : Result {
object Loading : NewsListResult()
data class Result(val newsList: NewsList) : NewsListResult()
data class Error(val error: String) : NewsListResult()
}
As, we see the architecture really forces us to model it in a particular structure. Every actions should be listened by Viewmodel.
正如我们看到的那样,该架构确实迫使我们在特定结构中对其进行建模。 Viewmodel应该监听每个动作。
For actions we would be using Channels.
对于动作,我们将使用Channels 。
It is is how we can transfer streams of values in Coroutines. It is similar to RxJava subject or MutableLiveData.
这就是我们可以在协程中传递值流的方式。 它类似于RxJava主题或MutableLiveData 。
private val actions: Channel<A> = Channel(Channel.UNLIMITED)
We would be using offer(element: E)
method to send items in channel.
我们将使用offer(element: E)
在频道中发送项目的方法。
This action is listened by viewmodel, actions are processed to generate Result.
这个动作被viewmodel监听,动作被处理以生成Result 。
To follow the reactive approach the actionToResults method should return a stream. Flow is similar like Observables in RxJava or LiveData.
要遵循React式方法,actionToResults方法应返回流。 流类似于RxJava或LiveData中的 Observables。
abstract suspend fun actionToResults(action: A): Flow<R>
This method is responsible to filter the actions and generate the result and pass it in the stream
此方法负责过滤操作并生成结果并将其传递到流中
suspend fun actionToResults(action: NewsListAction): Flow<NewsListResult> {
return when (action) {
is NewsListAction.LoadNews ->
newsRepository.getNewsSource().map {
when (it) {
is Lce.Loading -> NewsListResult.Loading
is Lce.Content -> NewsListResult.Result(it.packet)
else -> NewsListResult.Error("Something went wrong")
}
}.flowOn(ioCoroutineDispatcher)
}
}
Suspending functions aren’t compiled to ObjC so we can’t use those on iOS but thanks to CFlow
from KotlinConf app, we are able to do so. See this for the source code they have solved this use case in their app. This is one of the way of solving this.
暂挂功能未编译到ObjC,因此我们无法在iOS上使用这些功能,但多亏KotlinConf应用程序CFlow
了CFlow,我们才能够这样做。 请参阅此以获取他们已在其应用程序中解决此用例的源代码。 这是解决此问题的方法之一。
import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
fun <T> ConflatedBroadcastChannel<T>.wrap(): CFlow<T> = CFlow(asFlow())
fun <T> Flow<T>.wrap(): CFlow<T> = CFlow(this)
class CFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job(/*ConferenceService.coroutineContext[Job]*/)
onEach {
block(it)
}.launchIn(CoroutineScope(dispatcher() + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
Now we have actions and results. Let us discuss about State now.
现在我们有了行动和结果。 让我们现在讨论状态 。
ViewModel should have a way to communicate to view when there is change in state. So to solve this we would be using, Channels again but ConflatedBroadcastChannel.
当状态发生变化时,ViewModel应该可以进行通信以进行查看。 因此,要解决此问题,我们将再次使用Channels,但使用ConflatedBroadcastChannel 。
ConflatedBroadcastChannel : Whenever Back-to-back states elements are send— only the the most recently sent value is received, while previously sent elements are lost.
ConflatedBroadcastChannel:每当发送背对背状态元素时-仅接收到最近发送的值,而先前发送的元素会丢失 。
For coroutines in multiplatform world to work in Main thread, we would be using expect/actual
为了使多平台世界中的协程在主线程中工作,我们将使用期望/实际
The expect
keyword declares that the common code can expect different actual
implementations for each platform. We can use this for things which are platform specific.
expect
关键字声明公用代码可以期望每个平台的不同actual
实现。 我们可以将其用于特定于平台的事物。
import kotlinx.coroutines.CoroutineDispatcher
expect fun uiDispatcher(): CoroutineDispatcher
expect fun ioDispatcher(): CoroutineDispatcher
Android actual implementation
Android实际实现
actual fun uiDispatcher() : CoroutineDispatcher = Dispatchers.Main
actual fun ioDispatcher() : CoroutineDispatcher = Dispatchers.IO
iOS actual implementation
iOS实际实现
import kotlinx.coroutines.*
import platform.darwin.*
import kotlin.coroutines.*
actual fun uiDispatcher(): CoroutineDispatcher = UI
actual fun ioDispatcher(): CoroutineDispatcher = UI
@UseExperimental(InternalCoroutinesApi::class)
private object UI : CoroutineDispatcher(), Delay {
override fun dispatch(context: CoroutineContext, block: Runnable) {
val queue = dispatch_get_main_queue()
dispatch_async(queue) {
block.run()
}
}
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
val queue = dispatch_get_main_queue()
val time = dispatch_time(DISPATCH_TIME_NOW, (timeMillis * NSEC_PER_MSEC.toLong()))
dispatch_after(time, queue) {
with(continuation) {
resumeUndispatched(Unit)
}
}
}
}
Below is the implementation of ViewModel. We can see here we pass initial state and CoroutineDispatcher that we discussed above as constructor parameter.
下面是ViewModel的实现。 我们可以在这里看到我们将上面讨论的初始状态和CoroutineDispatcher传递为构造函数参数。
abstract class ViewModel<A : Action, S : State, R : Result>
(initialState: S, mainCoroutineDispatcher: CoroutineDispatcher) {
/** Any coroutine launched in this scope is automatically
canceled if the ViewModel is cleared.*/
val viewModelScope = CoroutineScope(SupervisorJob() + mainCoroutineDispatcher)
private val actions: Channel<A> = Channel(Channel.UNLIMITED)
// Listening to actions and converting to results
@FlowPreview
private val results: Flow<R> = actions.consumeAsFlow().flatMapLatest { actionToResults(it) }
private val stateChannel: ConflatedBroadcastChannel<S> = ConflatedBroadcastChannel(initialState)
fun sendEvent(action: A) = actions.offer(action)
protected abstract suspend fun actionToResults(action: A): Flow<R>
protected abstract suspend fun resultToState(result: R, state: S): S
@ExperimentalCoroutinesApi
val stateJob = results.scan(initialState) { state, result -> resultToState(result, state) }
.distinctUntilChanged()// Only emit when the current state value is different than the last.
.onEach { stateChannel.offer(it) }
.launchIn(viewModelScope)
// Use this method to observe the state
fun observeState(): CFlow<S> = CFlow(stateChannel.asFlow())
// Need to call this method when view is destroyed to cancel coroutines
fun onCleared() {
viewModelScope.cancel()
}
}
In above code we are using something called scan. Scan operator takes the current state and result to combine it into a new state.
在上面的代码中,我们使用的是所谓的扫描。 扫描运算符获取当前状态和结果以将其组合为新状态。
We need to call onCleared() function from the view, whenever the view is getting destroyed.
每当视图被破坏时,我们都需要从视图中调用onCleared()函数。
If you want to check the sample example. You can check here:
如果要检查示例示例。 您可以在这里查看:
Conclusion
结论
Hopefully, you would have a better understanding of implementation of MVI or uni-directional data flow and how to get started. There can be multiple ways to approach this choose whatever works for your project.
希望您对MVI或单向数据流的实现以及入门有更好的了解。 可以采用多种方法来选择适合您项目的方法。
Please feel free to ask any questions or views about modularization, do reach out at LinkedIn or Twitter.
请随时询问有关模块化的任何问题或看法,请访问LinkedIn或Twitter 。
If you like the article, remember to share it with the android community. Happy Coding!
如果您喜欢这篇文章,请记住与android社区分享。 编码愉快!
资源资源 (Resources)
kotlin 流