Module 0259: The Model-View-Adapter architecture

Tak Auyeung, Ph.D.

January 29, 2017

1 About this module

2 Why MVA?

MVA is a way to organize a system (including a mobile app) so that its components remain as independent from each other as possible. This helps to make the system more agile to include new features or change existing features without a massive amount of change ripple.

Perhaps it is better to explain what happens when there is no underlying architectural structure in the development of a mobile app.

Mobile apps are, inherently, visual. As a result, it is not uncommon for developers to start with the design of activities. After activities are designed with their respective user interface elements, code is added to respond to user interaction.

This is the first point of time when some representation of the “backend” data is needed. If development is approached in this direction, the “backend” data is fragmented to begin with, and slowly aggregates into its own data structure.

The main problem of this type of development method is that the underlying data representation of an app is directly tied in to the code that interacts with user interface elements. This direct linkage means any change of one side will likely result in matching changes to the other side.

For simpler apps, this may not be too big of an issue. However, for more complex apps. the lack of decoupler can lead to massive changes that are not necessary if the right architectural approach was used to begin with.

3 What is a model (the “M” in MVA)?

In short, the “model” is the backend data and logic of an app. This is the part of an app that is still present and relevant regardless of how interaction is done with the end user.

Let’s think of a simple counter app. The model of a counter consists of the following:

This model applies not only to apps, but just about any physical counter! This is the “core” of what a counter is about.

4 What is a view (the “v” in MVA)?

A “view” of a model is a construct that interfaces with a system other than the one being modeled. This other “system” includes end users as well as other computer information systems.

In our example of a counter, the view on a mechanical counter include a dial for resetting the count, a clicker to increment the count, and a display to display the current count. The view on a counter app may include a number, a button to reset, and another button to increment.

A view has methods to display/show information, but it can also generate events In the case of a counter, a “click” of the increment button is an event, a “click” of the reset button is an event.

5 Keeping the model and the view independent?

Indeed, this is the whole idea of the MVA architecture. However, this separation is also found in the models of other types of work.

In HTML documents, it is not uncommon to find older HTML documents combining content with presentation. Newer documents, on the other hand, make use of CSS (cascading style sheets) to handle the presentation, while using hierarchical elements “h1”, “h2”, “p” to handle the content. This allows a change of the CSS to dramatically change the look and feel of the same content.

In other words, the intentional separation of model and view is to remove any “hard links” between what the user interacts with as opposed to the core business logic. Let us examine another example.

Older vehicles have hard links between the driver’s control to the corresponding components under the hood. For example, the “gas pedal” is mechanically linked to the throttle body, the steering wheel is mechanically linked to the steering column and etc.

While easier to understand and certainly simpler to diagnose, such hard links also mean there is no flexibility. There is no easy way to insert any logic between the user input and the controled device, not even for safety reasons.

Newer cars, on the other hand, utilize a lot of “by-wire” controls. Some cars even make use of steering-by-wire. The steering wheel is an encoder that tells a computer how much it is turned, and the computer instructs an electric motor to turn the steering column as a result.

In this example, the steering wheel is a “view”, whereas the steering column control motor is the “model”. The separation of these two now allow intermediate logic to be applied. It also allows the installation of a completely different steering control motor without any changes to the steering wheel itself. In an engine compartment, the lack of a steering column provides the flexibility of the placement of other components such as transmission and engine.

6 Isn’t this a waste of time? I just need to write an app!

The separation of model from view is not easy from the perspectives of most developers. This is because most competent developers can intuitively come up with “monolithic” solutions that do not separate model from view.

A careful examination of the nature of the app to separate model from view will take time and effort. However, there are benefits from doing so, especially considering the ongoing cost of maintenance as well as extensions to the app.

One interesting effect of MV sepearation is the ease of division of labor. Consider an online bank app that allows a user choose from a list of payees. If there is no MV separation, then any enhancement to be done to the choosing mechanism (part of a view) requires a thorough understanding of the queries involved to get that list according to business logic (part of the model). This means the developer who is going to make this change need to understand both banking business logic and GUI programming.

The separation of MV, on the other hand, allows a GUI expert without banking business expertise to perform any frontend changes (such as “include an icon representing each payee”) without needing to understand the business logic. This makes it easier to find a qualified person to get the job done.

From the perspective of utilizing the right person for the right job, MV separation makes sense. Developers who are more suitable to solve model-related problems are usually not visual or GUI-oriented, whereas developers who are visual and GUI-oriented typically do not have the skill set to solve problems on the business logic side. The separation of model and view allows the right person on the right job.

7 Easier said than done!

Let us examine a simple counter app that serves as an electronic dumb counter. It has a button to increment the count, a button to reset the count, and a text view to display the count. The source code of the MainActivity is listed below.

 
1package edu.losrios.w1234567.nonmvacounter; 
2 
3import android.support.v7.app.AppCompatActivity; 
4import android.os.Bundle; 
5import android.view.View; 
6import android.widget.TextView; 
7 
8public class MainActivity extends AppCompatActivity { 
9    private int count = 0; 
10 
11    private void updateCountView() { 
12        ((TextView)findViewById(R.id.textViewValue)).setText(String.valueOf(count)); 
13    } 
14 
15    @Override 
16    protected void onCreate(Bundle savedInstanceState) { 
17        super.onCreate(savedInstanceState); 
18        setContentView(R.layout.activity_main); 
19        findViewById(R.id.buttonIncrement).setOnClickListener( 
20                new View.OnClickListener() { public void onClick(View x) { 
21                    ++count; 
22                    updateCountView(); 
23                }} 
24        ); 
25        findViewById(R.id.buttonReset).setOnClickListener( 
26                new View.OnClickListener() { public void onClick(View x) { 
27                    count = 0; 
28                    updateCountView(); 
29                }} 
30        ); 
31    } 
32}

While this app is simple and there is little need to separate model from view, it is also a good example to illustrate the lack of separation.

Any code that accesses the count member is a part of the model, whereas any code that accesses any TextView or View objects is a part of the view. You can see how model-related and view-related code are co-mangled into one little mess here.

Sure, this app works, and it only takes minutes to developer. But it is also very rigid and difficult to change! One reason to change this code may be for the purpose to use the cloud to store the counting. In other words, different mobile devices running this app are cloud connected so that the counting is done by updating a shard counter.

This can be helpful in situations where multiple employees count customers coming in from different entrances, and a supervisor in the office tracks the total number customers.

The structure of this code is, unfortunately, not friendly to be modified.

8 Separation anxiety

In the counter example, we can see the model of counting by observing what we do with the private member count:

As such, a new class is created for the model.

 
1package edu.losrios.w1234567.nonmvacounter; 
2 
3import java.util.LinkedList; 
4import java.util.ListIterator; 
5 
6/** 
7 * Created by tauyeung on 1/29/17. 
8 */ 
9 
10public class CountModel { 
11    public interface OnCountChangeListener { 
12        public void onCountChange(int newValue); 
13    } 
14    private LinkedList<OnCountChangeListener> listeners = new LinkedList<OnCountChangeListener>(); 
15    private int count = 0; 
16    private void notifyListeners() { 
17        ListIterator<OnCountChangeListener> i = listeners.listIterator(); 
18        while (i.hasNext()) 
19        { 
20            OnCountChangeListener listener = i.next(); 
21            listener.onCountChange(count); 
22        } 
23    } 
24    public void registerOnCountChangeListener(OnCountChangeListener l) { listeners.add(l); } 
25    public int getCount() { return count; } 
26    public void incCount() { ++count; notifyListeners(); } 
27    public void resetCount() { count = 0; notifyListeners(); } 
28}

The MainActivity class now becomes the view. It is now as follows:

 
1package edu.losrios.w1234567.nonmvacounter; 
2 
3import android.support.v7.app.AppCompatActivity; 
4import android.os.Bundle; 
5import android.view.View; 
6import android.widget.TextView; 
7 
8import java.util.LinkedList; 
9import java.util.ListIterator; 
10 
11public class MainActivity extends AppCompatActivity { 
12    public interface ViewListener { 
13        public void increment(); 
14        public void reset(); 
15    } 
16 
17    private LinkedList<ViewListener> listeners = new LinkedList<ViewListener>(); 
18 
19    private void updateCountView(int value) { 
20        ((TextView)findViewById(R.id.textViewValue)).setText(String.valueOf(value)); 
21    } 
22 
23    public void registerListener(ViewListener vl) { listeners.add(vl); } 
24 
25    @Override 
26    protected void onCreate(Bundle savedInstanceState) { 
27        super.onCreate(savedInstanceState); 
28        setContentView(R.layout.activity_main); 
29        findViewById(R.id.buttonIncrement).setOnClickListener( 
30                new View.OnClickListener() { public void onClick(View x) { 
31                    ListIterator<ViewListener> i = listeners.listIterator(); 
32                    while (i.hasNext()) 
33                    { 
34                        i.next().increment(); 
35                    } 
36                }} 
37        ); 
38        findViewById(R.id.buttonReset).setOnClickListener( 
39                new View.OnClickListener() { public void onClick(View x) { 
40                    ListIterator<ViewListener> i = listeners.listIterator(); 
41                    while (i.hasNext()) 
42                    { 
43                        i.next().reset(); 
44                    } 
45                }} 
46        ); 
47    } 
48}

At this point, we have two classes that are completely disjoint from each other. The CountModel class has no awareness of the MainActivity class, and vice versa is true as well.

The connection of the two is the job of adapter objects. An adapter object performs minimum logic to connect a model to a view, or potentially multiple views.

The following is a MV adapter class. This adapter really does not do much except to relay events of the model and the view.

 
1package edu.losrios.w1234567.nonmvacounter; 
2 
3/** 
4 * Created by tauyeung on 1/29/17. 
5 */ 
6 
7public class SimpleMVA implements CountModel.OnCountChangeListener, MainActivity.ViewListener { 
8    private CountModel model; 
9    private MainActivity view; 
10    public void onCountChange(int x) { 
11        view.updateCountView(x); 
12    } 
13    public void increment() { 
14        model.incCount(); 
15    } 
16    public void reset() { 
17        model.resetCount(); 
18    } 
19    public void setModel(CountModel cm) { 
20        model = cm; 
21        cm.registerOnCountChangeListener(this); 
22    } 
23 
24    public void setView(MainActivity v) { 
25        view = v; 
26        v.registerListener(this); 
27    } 
28}

Almost everything is in place now. What is left to be done is to set up the three components. There are several ways to do this. We can put the code in MainActivity, but that means the view now has awareness of the adapter and the model. To keep the code clean, we can create a static class whose only purpose is to connect all the components. Here is the definition of this helper class.

 
1package edu.losrios.w1234567.nonmvacounter; 
2 
3/** 
4 * Created by tauyeung on 1/29/17. 
5 */ 
6 
7public class MVASetup { 
8    static void setup(MainActivity v) { 
9        SimpleMVA mva = new SimpleMVA(); 
10        mva.setModel(new CountModel()); 
11        mva.setView(v); 
12    } 
13}

With this last piece in place, we only need to attach a call to setup in MainActivity.onCreate. The finished MainActivity definition is as follows.

 
1package edu.losrios.w1234567.nonmvacounter; 
2 
3import android.support.v7.app.AppCompatActivity; 
4import android.os.Bundle; 
5import android.view.View; 
6import android.widget.TextView; 
7 
8import java.util.LinkedList; 
9import java.util.ListIterator; 
10 
11public class MainActivity extends AppCompatActivity { 
12    public interface ViewListener { 
13        public void increment(); 
14        public void reset(); 
15    } 
16 
17    private LinkedList<ViewListener> listeners = new LinkedList<ViewListener>(); 
18 
19    public void updateCountView(int value) { 
20        ((TextView)findViewById(R.id.textViewValue)).setText(String.valueOf(value)); 
21    } 
22 
23    public void registerListener(ViewListener vl) { listeners.add(vl); } 
24 
25    @Override 
26    protected void onCreate(Bundle savedInstanceState) { 
27        super.onCreate(savedInstanceState); 
28        setContentView(R.layout.activity_main); 
29        findViewById(R.id.buttonIncrement).setOnClickListener( 
30                new View.OnClickListener() { public void onClick(View x) { 
31                    ListIterator<ViewListener> i = listeners.listIterator(); 
32                    while (i.hasNext()) 
33                    { 
34                        i.next().increment(); 
35                    } 
36                }} 
37        ); 
38        findViewById(R.id.buttonReset).setOnClickListener( 
39                new View.OnClickListener() { public void onClick(View x) { 
40                    ListIterator<ViewListener> i = listeners.listIterator(); 
41                    while (i.hasNext()) 
42                    { 
43                        i.next().reset(); 
44                    } 
45                }} 
46        ); 
47        MVASetup.setup(this); 
48    } 
49}

9 We paid the price, what did we buy?

The MVA architecture needs some overhead. The overhead is particularly costly in simple apps like the counter app. However, even with such a simple app, there are some benefits.

Yes, really.

As mentioned before, a counter app has the potential of being cloud based. Depending on how it is done, the changes can be done mostly in the model (using a cloud-based counter variable). In this case, the model also needs to become a listener of the update of the cloud-based variable.