Let’s Try Model-View-Intent with Android

Cody Engel
AndroidPub
Published in
6 min readApr 4, 2017

The newest cool architectural thing in Android seems to be Model-View-Intent, perhaps better known as MVI. I’ve read some articles about it here and there and overall it just seemed like a very strange concept. The authors of these articles kept mentioning these things called Presenters, which inevitably look similar the ones we’ve become familiar with in Model-View-Presenter. So tonight as I decided to take a stab at MVI, doing as little as possible (because I already put in my eight hours at work today) while keeping with the spirit. Ladies and gentleman, I present to you, Model-View-Intent.

The Sample App Overview

For this app I wanted to keep things simple and as close to a Hello World app as possible. MVI seems to be a great option when you are doing reactive programming so I figured this app may as well react to text changes.

Reacting to these text changes is made possible by RxBinding which allows us to observe on text changes and button clicks. So as the user types into the EditText field we receive the text changes which can then be processed by our Intent and passed along to our Model.

But you may be asking: how does the TextView react to those changes from the EditText? Well to be honest, it’s kind of janky, but our Model is created by passing in Consumer’s that can notify our TextView and EditText of changes to our model, and then voila, the app is working.

Code Sample Please

Alright so now that I’ve added just enough text to match the height of my cool gif demo of the sample app I suppose I can dive into the code sample.

So first we need to take care of our dependencies in our gradle build file. You don’t need to add ButterKnife, but I threw it in because annotations are fun. You’ll also notice in the code examples that I am using lambdas, those are provided by RetroLambda which has a slightly more complicated setup process (though not terribly complicated) so I’m omitting it from here.

compile "io.reactivex.rxjava2:rxjava:2.0.7"

compile "com.jakewharton.rxbinding2:rxbinding:2.0.0"
compile "com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0"
compile "com.jakewharton.rxbinding2:rxbinding-design:2.0.0"

compile "com.jakewharton:butterknife:8.5.1"
annotationProcessor "com.jakewharton:butterknife-compiler:8.5.1"

Okay cool so now that we have our dependencies all ready to go it’s time to look at our Intent, and since this is Android I went ahead and named it MainIntent. Our MainIntent takes in (spoiler alert) MainView and then retrieves the available actions (which are Observables) while also creating our MainModel.

import java.util.Map;

import io.reactivex.Observable;

/**
*
@author cody
*/
class MainIntent {

private Map<String, Observable> actions;
private MainModel mainModel;

MainIntent(MainView mainView) {
actions = mainView.getActions();
mainModel = new MainModel(mainView.getConsumers());
}

@SuppressWarnings("unchecked")
void start() {
actions.get("Button").subscribe(next -> mainModel.resetText());
actions.get("EditText").subscribe(changedText -> mainModel.changeText(changedText.toString()));
}
}

I’m still in the mindset of MVP so I included a method named start which can notify the Intent that we are in-fact ready for it to start doing it’s thing. In hindsight I probably didn’t need to expose any methods, but now I nice jumping off point to dive in deep with the meat and potatoes of MainIntent.

void start() {
actions.get("Button").subscribe(next -> mainModel.resetText());
actions.get("EditText").subscribe(changedText -> mainModel.changeText(changedText.toString()));
}

So as you can see we retrieve our first action which is registered as “Button”, which I subscribe to and tell MainModel to reset when onNext is called. If you don’t know what I mean by onNext then please check out RxJava and come back to this when you feel more comfortable with it. I also subscribe to our “EditText” action which is essentially triggered anytime the text changes, I then transform that into a String and tell MainModel to changeText.

Okay cool, so next up let’s talk about that MainModel I keep yammering on about. Essentially our Model is going to store our state while also containing default text which is used at the start of the app and whenever our awesome user’s tap the reset button.

import java.util.Map;

import io.reactivex.functions.Consumer;

/**
*
@author cody
*/
class MainModel {

private final String defaultText = "Default Text";

private String text;

private Map<String, Consumer> consumers;

MainModel(Map<String, Consumer> consumers) {
this.consumers = consumers;
resetText();
}

@SuppressWarnings("unchecked")
void changeText(String text) {
this.text = text;
try {
consumers.get("TextView").accept(text);
} catch (Exception e) {
e.printStackTrace();
}
}

@SuppressWarnings("unchecked")
void resetText() {
this.text = defaultText;
try {
consumers.get("EditText").accept(text);
} catch (Exception e) {
e.printStackTrace();
}
}

}

So our Intent creates our Model which requires Consumers (an RxJava thing) to be created. I couldn’t really think of a better name for these tonight, but essentially these let us communicate back to our View so as the value of text changes we can update the View with the new value.

I have created two methods which are called by MainIntent, when resetText is called we simply update text to the value of defaultText and call the Consumer for EditText passing in text.

The other method is changeText which will notify the MainIntent that it needs to update the value of text and after doing so it will call the Consumer for Text passing in text.

For our View I defined an interface which can then be implemented by our Activity. Oh, and it’s called MainView because again, Android.

import java.util.Map;

import io.reactivex.Observable;
import io.reactivex.functions.Consumer;

/**
*
@author cody
*/
interface MainView {

Map<String, Observable> getActions();

Map<String, Consumer> getConsumers();

}

It contains two method signatures, getActions which will return Observables that our Intent can subscribe to and getConsumers which will return Consumers that our Model can call to notify the view of changes.

And finally it’s time to take a look at MainActivity which ends up acting as our View while also creating our Intent so we can finally get this party started.

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxTextView;

import java.util.HashMap;
import java.util.Map;

import butterknife.BindView;
import butterknife.ButterKnife;
import io.reactivex.Observable;
import io.reactivex.functions.Consumer;

/**
*
@author cody
*/
public class MainActivity extends AppCompatActivity implements MainView {

@BindView(R.id.button) Button button;
@BindView(R.id.editText) EditText editText;
@BindView(R.id.text) TextView text;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);

new MainIntent(this).start();
}

@Override
public Map<String, Observable> getActions() {
Map<String, Observable> actions = new HashMap<>();
actions.put("Button", RxView.clicks(button));
actions.put("EditText", RxTextView.textChanges(editText));
return actions;
}

@Override
public Map<String, Consumer> getConsumers() {
Map<String, Consumer> consumers = new HashMap<>();
consumers.put("TextView", RxTextView.text(text));
consumers.put("EditText", RxTextView.text(editText));
return consumers;
}
}

As you can see within onCreate we instantiate MainIntent while also calling start because at this point ButterKnife has bound our views and for the purpose of this sample we can safely reference them. Defined are the getActions and getConsumers which pass along the relevant Observers and Consumers. All the while our Intent knows about our View and Model, our Model knows about these Consumers it needs to update and our View knows that it has Actions and Consumers but has no idea how either are actually used.

The End?

So that’s all I have for now. My goal with this short project was to get a better understanding of Model-View-Intent and to actually create something without also having a Presenter. It was difficult to find information to go off of so I referenced Cycle.js for most of this.

This sample is not ready for production and in some ways it’s a little embarrassing. If I was going to actually build this out to be something easy to maintain I’d likely have the Action and Consumer key’s defined as static strings and I would also need to find a solution for typecasting to the correct objects safely. However for now I consider my goal completed and am ready for the constructive criticism.

Oh, almost forgot, here is a link to the sample repository on GitHub.

--

--