Uni-Directional Architecture on Android Using Realm

Introduction

Uni-directional architecture is gaining momentum on Android. There have been numerous posts and many developers in the Android community advocating for it. For brevity in this article, I’ll refer to Uni-Directional Architecture as UDA. UDA is a bit of a misnomer because it’s not a single architecture, but an attribute of an architecture and a way of designing your app such that the data flows in a single direction and state is managed in only one spot, the Model.
More on that in a minute. In it’s simplest form, UDA, looks a lot like the original MVC pattern designed for Smalltalk-80.

Smalltalk-80 MVC

This definition of MVC differs from that of Apple and other authoritative sources in the Android space like the Big Nerd Ranch - Android Programming 3rd Edition - Chapter 2 [Android and MVC pg 38], both of which put the Controller in the middle as an intermediary between the Model and the View. I’m not going to argue which definition is right or wrong. These two posts, cocoawithlove and the aspiring craftsman, are great reads on the origins and history of MVC. For this post, when I refer to MVC, I’m referring to the original pattern designed for Smalltalk-80.

The Model is at the center of the universe. It manages the state and business logic of the application. The Controller provides a mechanism to update the Model. The View renders the Model and is always reflecting the current state of the Model. When the user interacts with the View, it invokes an action on the Controller and the process starts again. The flow of action and data is Uni-Directional.

Why do I care about UDA?

You might be asking why you care. What’s wrong with something like MVP where your presenter takes actions from the View, updates the Model, then goes back and updates the View itself, telling the View exactly what to display or do next.

There are 2 reasons you might want to consider.

  1. The presenters become filled with a lot of boilerplate, glue code that could be eliminated if the Views were bound directly to the Model.

  2. There are essentially two places that hold state, in your app, at any given time: the View state and the Model state. In a multithreaded, dynamic environment where updates to the Model can occur simultaneously, across threads and from multiple inputs (User Interaction, background updates, and other triggers.), this is difficult to manage and unnecessary.

UDA’s solve these problems by binding your Views directly to the Model. There is only one true state at any given time and it’s stored in the Model.

There is only one hard and fast rule with UDA. The data / actions must always flow in one direction. Outside of that, implementations can and do vary.

Get more development news like this

On Android there are a couple of popular architectures (or frameworks) that have a UDA structure, including Flux, Redux and Model View Intent. There are of course many others that I’m sure I’m leaving out. Outside of formal frameworks or architectures, many are using RxJava to make their architectures uni-directional. Many of the concepts behind these libraries were borrowed from the JavaScript community.

I’m going briefly cover the top 3 here that I’ve mentioned and then I’m going to follow the tradition of many of developers before me and create a Todo app using Realm to demonstrate UDA. It will illustrate what I feel is a simpler approach for Android.

The resulting App will look like this:

First a comparison of Flux, Redux and Model View Intent

Model View Intent (MVI)

MVI has it’s origins in the JavaScript world where it was solidified into a framework called CycleJs by André Staltz. It’s been implemented in Android by Hannes Dorfmann, creator of Mosby. You can read more about Hannes’ take on MVI for Android here. To summarize, though, the relationships are as shown here

Model View Intent

They are represented more functionally in Hannes’ post as View(Model(Intent())).

The Cycle.js documentation does a great job of summarizing the MVI components and their roles.

View

Visually represents the state of the Model, taking Model objects directly as input, and emitting events (from user interaction) to Intents.

Intent

Receives events from the View and interprets them as intended actions, then sends the actions to the Model to manipulate the Model state. Just to clarify, this has nothing to do with Android Intents.

Model

The Model manages the state of the application and business logic. It takes in actions from the Intent and emits state back to the View.

Comparison to Flux & Redux

While on the surface there are differences, when you get down to the code Flux & Redux are actually very similar in concept. Flux & Redux both use the term Dispatcher instead of Intent and the Actions originate upstream, in the View). The only other major difference we’ll see is that Actions are recognized more formally in Flux & Redux, where they are a key part of the architecture diagram.

Flux & Redux

Depending on who you talk to, Redux – which also originates in the JavaScript world – can be considered either an implementation of the Flux architecture or a framework that was inspired by and evolved from Flux Architecture. Regardless of perspective, Flux & Redux end up looking similar to each other and similar to MVI, except that they more explicitly and formally call out the Action.

Flux Architecture Diagram

View

Views are the User Interface, representing Model data from the Store. Views create Actions and pass them to the Dispatcher in response to user interactions.

Action

Simple data components, which contain a type and optionally data. Actions encapsulate discrete units of work that can be asynchronously placed or streamed into a queue, to be applied in serial order.

Dispatcher

Dispatchers are essentially an eventbus responsible for accepting Actions from the queue and invoking methods on the Store to update the underlying Model.

Store

Stores contain the application state and business logic. Their role is somewhat similar to a Model in a traditional MVC, but they may manage the state of many Model objects, not just a single object. See https://facebook.github.io/flux/docs/in-depth-overview.html#stores.

Analysis

Actions are useful as a way of encapsulating discrete units of work and sending them to the Dispatcher asynchronously via an eventbus (like Otto) or via a stream using (RxJava). The Dispatcher will apply them in order or as I like to say FIFO-ly. Using Realm however I get this same functionality for free.

Let me explain and see if I can simplify the Flux architecture at the same time. I get the same effect using Realm, via asynchronous transactions

realm.executeTransactionAsync(Realm.Transaction txBlock)

The txBlock serves as the Action, encapsulating the work you want to dispatch asynchronously. These txBlocks are executed serially regardless of the thread the executeTransactionAsync method was called on. So Realm also fills the role of Dispatcher in that regard. This eliminates the need for both Actions and a Dispatcher and leaves me with the architecture shown in this diagram:

View Store Diagram

Actually the diagram is missing a piece that we’ve already discussed: we haven’t yet added it.
It’s the Model!
Let’s break that out from the store.

Model Store View Diagram

Well, actually, that doesn’t look completely Uni-Directional now that we show the Model(s) as separate entities. Realm can help us out here though because Realm Objects are Active Models. The results are live, meaning you’re always looking at the latest data and you can attach change listeners to react to data changes as they occur. These changes can happen anywhere in the application. It could even be on a background thread. With this in mind, the only reason that we need the Store is to invoke the action on the Models, updating the state via asynchronous transactions. The Models themselves can notify the Views (and any other observers) when they change. The Stores now go back to looking a lot like Controllers and we’re back to a flow that looks like this.

Realm MVC Annotated

Immutability: It is worth mentioning that Flux, Redux, and MVI all promote the use of an immutable Model that is emitted by the Store (or Model) upon the completion of each action. This is a tactic to help enforce UDA and simplify multi-threaded access to the Model. This isn’t necessary using Realm because we avoid this naturally by never mutating the Realm Model objects we supply to our View on the UI thread. We only manipulate the underlying database on a background thread in the Controller which has no ties to the View. You’ll see how this works in the code.

Code

No post on architecture would be complete without the code so without further ado…

Model

We start with a very simple Model.

public class TodoItem extends RealmObject {

    @PrimaryKey
    @Required
    private String id;

    @Required
    private Date createdDate;

    @Required
    private String text;

    private boolean selected;

    public TodoItem(final String text) {
        this.id = UUID.randomUUID().toString();
        this.createdDate = new Date();
        this.text = text;
    }

    public TodoItem() {}

    public String getId() {
        return id;
    }

    public String getText() {
        return text;
    }

    public boolean isSelected() {
        return selected;
    }

    public void setSelected(boolean selected) {
        this.selected = selected;
    }
}


Controller

Next we have a simple Controller to update the Model. The Controller validates input, and updates the Model(s) asynchronously, and serially.

public class TodoController {

   private Realm realm;

   public TodoController() {
       realm = Realm.getDefaultInstance();
   }

   public void close() {
       realm.close();
   }

   public void addItem(final String text) {

       if(TextUtils.isEmpty(text)) { return; }

       realm.executeTransactionAsync(new Realm.Transaction() {
           public void execute(Realm bgRealm) {
               bgRealm.copyToRealmOrUpdate(new TodoItem(text));
           }
       });
   }

   public void deleteAllChecked() {

       realm.executeTransactionAsync(new Realm.Transaction() {
           public void execute(Realm bgRealm) {
               bgRealm.where(TodoItem.class).equalTo("selected", true).findAll().deleteAllFromRealm();
           }
       });

   }

   public void setAllCheckedValue(final boolean isChecked) {

       realm.executeTransactionAsync(new Realm.Transaction() {
           public void execute(Realm bgRealm) {
               RealmResults<TodoItem> items = bgRealm.where(TodoItem.class).findAll();
               for(TodoItem item : items) { item.setSelected(isChecked); }
           }
       });

   }

   public void setSingleCheckedValue(final String itemId, final boolean isChecked) {

       if(TextUtils.isEmpty(itemId)) { return; }

       realm.executeTransactionAsync(new Realm.Transaction() {
           public void execute(Realm bgRealm) {
               TodoItem TodoItem = bgRealm.where(TodoItem.class).equalTo("id", itemId).findFirst();
               TodoItem.setSelected(isChecked);
           }
       });
   }
}


View

The view is comprised of a Layout, Activity and RecyclerViewAdapter which are shown below.

Layout

The Layout is composed of XML to define the structure of the UI

LayoutBreakdown

Activity

The Activity (TodoView) binds the Model to the View and the Controller.

public class TodoView extends AppCompatActivity implements TodoRecyclerViewAdapter.ItemSelectionChangeDelegate {

    private Realm realm;

    @BindView(R.id.add_todo_text) public EditText addTodoText;
    @BindView(R.id.todo_list)     public RecyclerView recyclerView;
    @BindView(R.id.add_item_fab)  public FloatingActionButton addItemFab;
    @BindView(R.id.toolbar)       public Toolbar toolbar;

    private TodoController controller;
    private RealmResults<TodoItem> model;

    /**
     * OnCreate we setup our view and bind the model and controller.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_todo);
        ButterKnife.bind(this);
        setSupportActionBar(toolbar);
        bindController();
        bindModel();
    }

    private void bindController() {
        controller = new TodoController();
    }

    private void bindModel() {
        realm = Realm.getDefaultInstance();
        model = realm.where(TodoItem.class).findAllSortedAsync("createdDate", Sort.DESCENDING);
        recyclerView.setAdapter(new TodoRecyclerViewAdapter(this, model));
        recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));

        model.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<TodoItem>>() {
            @Override
            public void onChange(RealmResults<TodoItem> collection, OrderedCollectionChangeSet changeSet) {
                if (changeSet != null && changeSet.getInsertions().length >= 1) {
                    // Every time the model is updated with a new todo list item, react:
                    // clearing the add todo EditText and scroll to the top of the todo list.
                    addTodoText.setText("");
                    recyclerView.getLayoutManager().scrollToPosition(0);
                }
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_todo, menu);
        return true;
    }

    /**
     * OnDestroy we unbind from the model and controller.
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindModel();
        unbindController();
    }

    private void unbindController() {
        controller.close();
    }

    private void unbindModel() {
        model.removeAllChangeListeners();
        realm.close();
    }

    /**
     *  OnSelectionChanged, onAddItem(), onOptionsItemSelected()
     *  Bind UI Events to the controller.
     */
    @Override
    public void onSelectionChanged(String itemId, boolean isSelected) {
        controller.setSingleCheckedValue(itemId, isSelected);
    }

    @OnClick(R.id.add_item_fab)
    public void onAddItem() {
        controller.addItem(addTodoText.getText().toString());
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        boolean handled = true;

        switch (item.getItemId()) {
            case R.id.action_check_all:
                controller.setAllCheckedValue(true);
                break;
            case R.id.action_uncheck_all:
                controller.setAllCheckedValue(false);
                break;
            case R.id.action_delete_checked:
                controller.deleteAllChecked();
                break;
            default:
                handled = false;
                break;
        }

        return handled;
    }
}

RecyclerViewAdapter

Finally TodoRecyclerViewAdapter is a helper which binds the Model list of todo items to the RecyclerView widget that is rendering them. We’re extending the RealmRecyclerViewAdapter which automatically reacts to Model changes for us.

class TodoRecyclerViewAdapter extends RealmRecyclerViewAdapter<TodoItem, TodoRecyclerViewAdapter.ViewHolder> {

    // Callback to TodoView (an ItemSelectionChangeDelegate).  Fires when an item is checked.
    interface ItemSelectionChangeDelegate {
        void onSelectionChanged(String itemId, boolean isSelected);
    }

    private ItemSelectionChangeDelegate itemSelectionChangeDelegate;

    TodoRecyclerViewAdapter(@NonNull  ItemSelectionChangeDelegate delegate, @NonNull OrderedRealmCollection<TodoItem> todoList) {
        super(todoList, true);
        itemSelectionChangeDelegate = delegate;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.todo_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, final int position) {
        OrderedRealmCollection data = getData();
        if(data != null) {
            holder.checkBox.setOnCheckedChangeListener(null);
            final TodoItem item = getData().get(position);
            holder.itemId = item.getId();

            TextView tv = holder.titleView;
            tv.setText(item.getText());
            if(item.isSelected()) {
                tv.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
            } else {
                tv.setPaintFlags(tv.getPaintFlags() & (~ Paint.STRIKE_THRU_TEXT_FLAG));
            }
            holder.checkBox.setChecked(item.isSelected());
            holder.checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean newCheckedState) {
                    if(item.isValid() && item.isSelected() != newCheckedState) {
                        itemSelectionChangeDelegate.onSelectionChanged(item.getId(), newCheckedState );
                    }
                }
            });
        }
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        String itemId;
        final TextView titleView;
        final CheckBox checkBox;

        ViewHolder(View view) {
            super(view);
            checkBox = (CheckBox) view.findViewById(R.id.todo_item_checkbox);
            titleView = (TextView) view.findViewById(R.id.todo_item_text);
        }
    }
}

And that’s it! It’s as simple as that to create a UDA app on Android using Realm.

Further Exploration

The plain and simple truth is that there is no “best” pattern or architecture for you to follow when making an Android app. UDA architectures have clear benefits as I’ve outlined in this post, but I encourage you to go out and try each for yourself to see what best fits your app, your team and your style. If you’re interested in the full source for this example, you can download that here.

Now go forth and build great software!

Next Up: Realm for Android #10: Realm Primary Keys Tutorial

General link arrow white

Eric Maxwell

Eric is an Product Engineer at Realm. He has spent over a decade architecting and developing software for various companies across different industries, including healthcare, insurance, library science, and private aviation. His current focus is training, mentoring, and mobile development. He has developed and taught courses on Java, Android and iOS. When he’s not working, he enjoys time with family, traveling and improv comedy.