MVIKotlin学习笔记(3):View、Binding和Lifecycle


View

在实现Views时并不需要遵循什么特别指南,尽管MVIKotlin提供的东西可能会很有用。

MVIKotlin中有两个有关View的接口:

  • ViewRenderer 使用并渲染``Models
  • ViewEvents 生产Events

还有一个MviView接口,它不过是同时包含了ViewRendererViewEvents接口。通常不需要直接实现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并生产了两种EventIncrementClickedDecrementClicked

你可能注意到了ModelEvents看起来很像CalculatorStore.StateCalculatorStore.Intent。在这个特定情况下,CalculatorView可以直接渲染State并生产Intents。但在通常情况下分离ModelsEvents是很好的做法,这样可以解耦ViewsStores

安卓上的实现看起来像这样:

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

接下来让我们绑定之前创建的CalculatorStoreCalculatorView

首先,我们需要把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 ModelsView Events。在这种情况下你不需要做映射,但你可能会在Views中引入逻辑。此外,你会耦合StoresViews

可以使用mvikotlin-extensions-coroutinesmvikotlin-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会将CalculatorStoreCalculatorView绑定,在onStop()中取消绑定。

根据同样的方法你可以绑定任何输出和输入。例如,你可以绑定来自StoreALabels和来自StoreBIntents,或者带有分析追踪器的View Events

Lifecycle

MVIKotlin使用Essenty库(来自同一个作者),它提供了Lifecycle——一个多平台的抽象的声明周期状态和事件。lifecycle模块作为api依赖,所以不需要明确地在已经引入了MVIKotlin的项目中引入。

img

Binder + Lifecycle

使用Lifecycle可以简化使用Binder的过程,为此只需要增加一个额外模块mvikotlin-extensions-reaktivemvikotlin-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
        }
    }
}

我们将viewLifecycleCalculatorView一起传递,并将其用于绑定。现在,Binder可以自动在开始时连接与在停止时断开连接。

与之前一样,我们在CalculatorController生命周期的最后释放CalculatorStore

可以参阅samples获取更多示例。