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<Model, Event> {
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<Model, Event>(), CalculatorView {
private val textView = root.requireViewById<TextView>(R.id.text)
init {
root.requireViewById<View>(R.id.button_increment).setOnClickListener {
dispatch(Event.IncrementClicked)
}
root.requireViewById<View>(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<CalculatorViewModel, CalculatorViewEvent>, 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<Model, Nothing> {
data class Model(
val name: String,
val friendNames: List<String>
)
}
我們可以通過以下方式使用diff:
class UserInfoViewImpl : BaseMviView<Model, Nothing>(), UserInfoView {
private val nameText: TextView = TODO()
private val friendsList: ListView = TODO()
override val renderer: ViewRenderer<Model>? = 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 = https://www.cnblogs.com/xuankaicat/p/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獲取更多示例,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/440449.html
標籤:架構設計
