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获取更多示例。