MVIKotlin学习笔记(3):View、Binding和Lifecycle
View
在实现Views时并不需要遵循什么特别指南,尽管MVIKotlin提供的东西可能会很有用。
在MVIKotlin中有两个有关View的接口:
- ViewRenderer
使用并渲染``Models。 - ViewEvents 生产
Events。
还有一个MviView接口,它不过是同时包含了ViewRenderer和ViewEvents接口。通常不需要直接实现MviView接口,可以通过继承BaseMviView类来实现。
如果使用的是
Jetpack Compose,那么很有可能你不需要用到MviView或它的其他超类。你可以直接在@Composable函数中监听Store。详情参阅Compose TodoApp example 。
实现一个View
让我们为在store中创建的CalculatorStore实现一个View。
首先,我们总是会定义一个接口:
interface CalculatorView : MviView {
data class Model(
val value: String
)
sealed class Event {
object IncrementClicked: Event()
object DecrementClicked: Event()
}
}
CalculatorView是公开的,所以它可以在任何平台被实现,例如安卓和iOS。CalculatorView使用了一个简单的Model,它只有一个String类型的value并生产了两种Event:IncrementClicked和DecrementClicked。
你可能注意到了Model和Events看起来很像CalculatorStore.State和CalculatorStore.Intent。在这个特定情况下,CalculatorView可以直接渲染State并生产Intents。但在通常情况下分离Models和Events是很好的做法,这样可以解耦Views和Stores。
安卓上的实现看起来像这样:
class CalculatorViewImpl(root: View) : BaseMviView(), CalculatorView {
private val textView = root.requireViewById(R.id.text)
init {
root.requireViewById(R.id.button_increment).setOnClickListener {
dispatch(Event.IncrementClicked)
}
root.requireViewById(R.id.button_decrement).setOnClickListener {
dispatch(Event.DecrementClicked)
}
}
override fun render(model: Model) {
super.render(model)
textView.text = model.value
}
}
iOS上的实现使用SwiftUI,可能看起来像这样:
class CalculatorViewProxy: BaseMviView, CalculatorView, ObservableObject {
@Published var model: CalculatorViewModel?
override func render(model: CalculatorViewModel) {
self.model = model
}
}
struct CalculatorView: View {
@ObservedObject var proxy = CalculatorViewProxy()
var body: some View {
VStack {
Text(proxy.model?.value ?? "")
Button(action: { self.proxy.dispatch(event: CalculatorViewEvent.IncrementClicked()) }) {
Text("Increment")
}
Button(action: { self.proxy.dispatch(event: CalculatorViewEvent.DecrementClicked()) }) {
Text("Decrement")
}
}
}
}
对于更多复杂的UI可以参考 samples。
高效的View更新
有时在每次收到新Model时都更新整个View可能是效率低下的。举个例子,如果一个View包含了一个文本和一个列表,如果在只更新文本的情况下不去更新列表是最好的。MVIKotlin为此提供了diff工具。
假设我们有一个UserInfoView,它用来显示用户的姓名和他的好友列表:
interface UserInfoView : MviView {
data class Model(
val name: String,
val friendNames: List
)
}
我们可以通过以下方式使用diff:
class UserInfoViewImpl : BaseMviView(), UserInfoView {
private val nameText: TextView = TODO()
private val friendsList: ListView = TODO()
override val renderer: ViewRenderer? = diff {
diff(get = Model::name, set = nameText::setText)
diff(get = Model::friendNames, compare = { a, b -> a === b }, set = friendsList::setItems)
}
}
所有的diff语句都接受一个从Model中提取值的getter、一个setter来为视图设置值和一个自定义的值比较器(comparator)。
Binding和Lifecycle
Binding
连接输入和输出听起来是很简单的事,并且事实也的确如此。但如果使用Binder可以变得更简单。它提供了两个方法:start()和stop()。当你使用start()时,它在输入时连接(订阅)输出。当你使用stop()时取消连接(订阅)。
创建Binder
接下来让我们绑定之前创建的CalculatorStore和CalculatorView。
首先,我们需要把CalculatorStore.State映射到CalculatorView.Model:
internal val stateToModel: CalculatorStore.State.() -> CalculatorView.Model =
{
CalculatorView.Model(
value = value.toString()
)
}
我们还需要把CalculatorView.Event映射到CalculatorStore.Intent:
internal val eventToIntent: CalculatorView.Event.() -> CalculatorStore.Intent =
{
when (this) {
is CalculatorView.Event.IncrementClicked -> CalculatorStore.Intent.Increment
is CalculatorView.Event.DecrementClicked -> CalculatorStore.Intent.Decrement
}
}
我们之前提到:可以通过只渲染State和(或)生产Intents来避免分离View Models和View Events。在这种情况下你不需要做映射,但你可能会在Views中引入逻辑。此外,你会耦合Stores和Views。
可以使用mvikotlin-extensions-coroutines和mvikotlin-extensions-reaktive模块提供的DSL来绑定输出和输入:
class CalculatorController {
private val store = CalculatorStoreFactory(DefaultStoreFactory).create()
private var binder: Binder? = null
fun onViewCreated(view: CalculatorView) {
binder = bind {
store.states.map(stateToModel) bindTo view
// 使用store.labels将标签绑定至消费者
view.events.map(eventToIntent) bindTo store
}
}
fun onStart() {
binder?.start()
}
fun onStop() {
binder?.stop()
}
fun onViewDestroyed() {
binder = null
}
fun onDestroy() {
store.dispose()
}
}
这个控制器应该由平台来使用。我们在onViewCreated(CalculatorView)回调中创建Binder,创建时平台会调用该回调。在onStart()中Binder会将CalculatorStore和CalculatorView绑定,在onStop()中取消绑定。
根据同样的方法你可以绑定任何输出和输入。例如,你可以绑定来自StoreA的Labels和来自StoreB的Intents,或者带有分析追踪器的View Events。
Lifecycle
MVIKotlin使用Essenty库(来自同一个作者),它提供了Lifecycle——一个多平台的抽象的声明周期状态和事件。lifecycle模块作为api依赖,所以不需要明确地在已经引入了MVIKotlin的项目中引入。

Binder + Lifecycle
使用Lifecycle可以简化使用Binder的过程,为此只需要增加一个额外模块mvikotlin-extensions-reaktive或mvikotlin-extensions-coroutines。
简化后的绑定示例:
class CalculatorController(lifecycle: Lifecycle) {
private val store = CalculatorStoreFactory(DefaultStoreFactory).create()
init {
lifecycle.doOnDestroy(store::dispose)
}
fun onViewCreated(view: CalculatorView, viewLifecycle: Lifecycle) {
bind(viewLifecycle, BinderLifecycleMode.START_STOP) {
store.states.map(stateToModel) bindTo view
// 使用store.labels将标签绑定至消费者
view.events.map(eventToIntent) bindTo store
}
}
}
我们将viewLifecycle与CalculatorView一起传递,并将其用于绑定。现在,Binder可以自动在开始时连接与在停止时断开连接。
与之前一样,我们在CalculatorController生命周期的最后释放CalculatorStore。
可以参阅samples获取更多示例。