Skip to the content.
Overview Store View Binding and Lifecycle State preservation Logging Time travel

View

It is not necessary to follow any particular guide when implementing Views, however you may find useful what is provided by MVIKotlin.

In MVIKotlin there are two basic interfaces related to View:

There is also the MviView interface which is just a combination of both ViewRenderer and ViewEvents interfaces. Again you normally don’t need to implement the MviView interface directly. Instead you can extend the BaseMviView class.

⚠️ If you are using Jetpack Compose then most likely you don’t need MviView or any of its super types. You can observe the Store directly in @Composable functions, just expose the state (or the mapped UI model) via Observable or Flow.

Implementing a View

Let’s implement a View for the CalculatorStore created here. As always we should first define an interface:

interface CalculatorView : MviView<Model, Event> {

    data class Model(
        val value: String
    )

    sealed class Event {
        object IncrementClicked: Event()
        object DecrementClicked: Event()
    }
}

The CalculatorView is public so it can be implemented natively by every platform, e.g. Android and iOS. Our CalculatorView consumes a simple Model with just a value text and produces two Events (IncrementClicked and DecrementClicked).

You may notice that Model and Events look very similar to CalculatorStore.State and CalculatorStore.Intent. In this particular case our CalculatorView could directly render State and produce Intents. But in general it is a good practice to have separate Models and Events. This removes coupling between Views and Stores.

An Android implementation can look like this:

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
    }
}

Here is a possible iOS implementation using 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")
            }
        }
    }
}

For a more complex UI please refer to the samples.

Efficient view updates

Sometimes it may be inefficient to update the entire View each time a new Model is received. For example, if a View contains a text and a list, it may be useful not to update the list if only the text is changed. MVIKotlin provides the diff tool for this.

Suppose we have a UserInfoView that displays a user’s name and a list of friends:

interface UserInfoView : MviView<Model, Nothing> {

    data class Model(
        val name: String,
        val friendNames: List<String>
    )
}

We can use diff in the following way:

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)
    }
}

Every diff statement accepts a getter that extracts a value from the Model, a setter that sets the value to the view and an optional comparator of values.

Overview Store View Binding and Lifecycle State preservation Logging Time travel