Hindemburg Hindemburg - 16 days ago 6
Android Question

How to use Android MVVM pattern with fragments?

First I request apologizes about my not to good English.

I have developed a lot of years Java SE software, and I used to use the MVC design pattern. Now I develop android apps, and I'm not happy with the argument that says that android already uses an MVC pattern, with the xml files acting as the view.

I did a lot of research on the web, but it seems that there is not unanimity about this topic. Some use the MVC pattern, others the MVP pattern, but I'm my opinion, there is no unanimity.

Recently I bought a book (Android Best Practices, from Godfrey Nolan, Onur Cinar and David Truxall), and in the chapter two, you can find the MVC, the MVVM and the Dependency Injection patterns explained. After trying all of them, I think that for my apps and my work mode the best is the MVVM pattern.

I find this pattern very easy to use when programming with activities, but I'm confused about how to use it when programming with fragments. I will reproduce the example of the MVVM pattern applied to simple "todo app", downloaded from the website of the "Android Best Practices" book.

The View (activity)

package com.example.mvvm;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;

public class TodoActivity extends Activity
{
public static final String APP_TAG = "com.logicdrop.todos";

private ListView taskView;
private Button btNewTask;
private EditText etNewTask;
private TaskListManager delegate;

/*The View handles UI setup only. All event logic and delegation
*is handled by the ViewModel.
*/

public static interface TaskListManager
{
//Through this interface the event logic is
//passed off to the ViewModel.
void registerTaskList(ListView list);
void registerTaskAdder(View button, EditText input);
}

@Override
protected void onStop()
{
super.onStop();
}

@Override
protected void onStart()
{
super.onStart();
}

@Override
public void onCreate(final Bundle bundle)
{
super.onCreate(bundle);

this.setContentView(R.layout.main);

this.delegate = new TodoViewModel(this);
this.taskView = (ListView) this.findViewById(R.id.tasklist);
this.btNewTask = (Button) this.findViewById(R.id.btNewTask);
this.etNewTask = (EditText) this.findViewById(R.id.etNewTask);
this.delegate.registerTaskList(taskView);
this.delegate.registerTaskAdder(btNewTask, etNewTask);
}
}


The Model

package com.example.mvvm;

import java.util.ArrayList;
import java.util.List;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

final class TodoModel
{
//The Model should contain no logic specific to the view - only
//logic necessary to provide a minimal API to the ViewModel.
private static final String DB_NAME = "tasks";
private static final String TABLE_NAME = "tasks";
private static final int DB_VERSION = 1;
private static final String DB_CREATE_QUERY = "CREATE TABLE " + TodoModel.TABLE_NAME + " (id integer primary key autoincrement, title text not null);";

private final SQLiteDatabase storage;
private final SQLiteOpenHelper helper;

public TodoModel(final Context ctx)
{
this.helper = new SQLiteOpenHelper(ctx, TodoModel.DB_NAME, null, TodoModel.DB_VERSION)
{
@Override
public void onCreate(final SQLiteDatabase db)
{
db.execSQL(TodoModel.DB_CREATE_QUERY);
}

@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
final int newVersion)
{
db.execSQL("DROP TABLE IF EXISTS " + TodoModel.TABLE_NAME);
this.onCreate(db);
}
};

this.storage = this.helper.getWritableDatabase();
}

/*Overrides are now done in the ViewModel. The Model only needs
*to add/delete, and the ViewModel can handle the specific needs of the View.
*/
public void addEntry(ContentValues data)
{
this.storage.insert(TodoModel.TABLE_NAME, null, data);
}

public void deleteEntry(final String field_params)
{
this.storage.delete(TodoModel.TABLE_NAME, field_params, null);
}

public Cursor findAll()
{
//Model only needs to return an accessor. The ViewModel will handle
//any logic accordingly.
return this.storage.query(TodoModel.TABLE_NAME, new String[]
{ "title" }, null, null, null, null, null);
}
}


The ViewModel

package com.example.mvvm;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

public class TodoViewModel implements TodoActivity.TaskListManager
{
/*The ViewModel acts as a delegate between the ToDoActivity (View)
*and the ToDoProvider (Model).
* The ViewModel receives references from the View and uses them
* to update the UI.
*/

private TodoModel db_model;
private List<String> tasks;
private Context main_activity;
private ListView taskView;
private EditText newTask;

public TodoViewModel(Context app_context)
{
tasks = new ArrayList<String>();
main_activity = app_context;
db_model = new TodoModel(app_context);
}

//Overrides to handle View specifics and keep Model straightforward.

private void deleteTask(View view)
{
db_model.deleteEntry("title='" + ((TextView)view).getText().toString() + "'");
}

private void addTask(View view)
{
final ContentValues data = new ContentValues();

data.put("title", ((TextView)view).getText().toString());
db_model.addEntry(data);
}

private void deleteAll()
{
db_model.deleteEntry(null);
}

private List<String> getTasks()
{
final Cursor c = db_model.findAll();
tasks.clear();

if (c != null)
{
c.moveToFirst();

while (c.isAfterLast() == false)
{
tasks.add(c.getString(0));
c.moveToNext();
}

c.close();
}

return tasks;
}

private void renderTodos()
{
//The ViewModel handles rendering and changes to the view's
//data. The View simply provides a reference to its
//elements.
taskView.setAdapter(new ArrayAdapter<String>(main_activity,
android.R.layout.simple_list_item_1,
getTasks().toArray(new String[]
{})));
}

public void registerTaskList(ListView list)
{
this.taskView = list; //Keep reference for rendering later
if (list.getAdapter() == null) //Show items at startup
{
renderTodos();
}

list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id)
{ //Tapping on any item in the list will delete that item from the database and re-render the list
deleteTask(view);
renderTodos();
}
});
}

public void registerTaskAdder(View button, EditText input)
{
this.newTask = input;
button.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View view)
{ //Add task to database, re-render list, and clear the input
addTask(newTask);
renderTodos();
newTask.setText("");
}
});
}
}


The problem is that when I try to reproduce this pattern when using fragments, I'm no sure how to proceed. May I have a view model and a model for each fragment or only for the activity that contains those fragments?

With the classic approach to fragment (the fragment is a inner class inside the activity), it is easy to interact with the activity, or to access the fragment manager to do changes, but if I decouple the code, and put the logic of my program outside the activity, I have seen that I need very often references to the activity in my ViewModel (not references to the views of the activity, but references to the activity itself).

Or for example, imagine that that the activity with fragments, is working with data received from an intent, not from a model (database or rest service). Then, I feel that I don't need a model. Maybe I can create the model when I receive the intent in the activity, but I feel that this is not correct (the view should not have relation with the model, only the viewmodel...).

May anybody offer me an explanation about how to use the MVVM pattern with android when using fragments?

Thanks in advance.

Answer

Personally, I prefer an alternate setup:

The Model

Your model. Doesn't need to be changed (beauty of using MVVM :) )

The View (fragment)

Slightly different. The View (Fragment) has a reference to the ViewModel ( Activity ) in my setup. Instead of initializing your delegate like:

// Old way -> I don't like it
this.delegate = new TodoViewModel(this);

I suggest you use a well-known Android pattern:

@Override
public void onAttach(final Activity activity) {
    super.onAttach(activity);
    try {
        delegate = (ITaskListManager) activity;
    } catch (ClassCastException ignore) {
        throw new IllegalStateException("Activity " + activity + " must implement ITaskListManager");
    }
}

@Override
public void onDetach() {
    delegate = sDummyDelegate;
    super.onDetach();
}

This way, your View (Fragment) enforces that the Activity to which it is attached, implemets the ITaskListManager interface. When the Fragment is detached from the Activity, some default implementation is set as the delegate. This prevents getting errors when you have an instance of a fragment which is not attached to an Activity (yes, this can happen).

Here's the complete code for my ViewFragment:

public class ViewFragment extends Fragment {

    private ListView taskView;
    private Button btNewTask;
    private EditText etNewTask;
    private ITaskListManager delegate;

    /**
     * Dummy delegate to avoid nullpointers when
     * the fragment is not attached to an activity
     */
    private final ITaskListManager sDummyDelegate = new ITaskListManager() {

        @Override
        public void registerTaskList(final ListView list) {
        }

        @Override
        public void registerTaskAdder(final View button, final EditText input) {
        }
    };

    /*
     * The View handles UI setup only. All event logic and delegation
     * is handled by the ViewModel.
     */

    public static interface ITaskListManager {

        // Through this interface the event logic is
        // passed off to the ViewModel.
        void registerTaskList(ListView list);

        void registerTaskAdder(View button, EditText input);
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        try {
            delegate = (ITaskListManager) activity;
        } catch (ClassCastException ignore) {
            throw new IllegalStateException("Activity " + activity + " must implement ITaskListManager");
        }
    }

    @Override
    public void onDetach() {
        delegate = sDummyDelegate;
        super.onDetach();
    }

    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.activity_view_model, container, false);
        taskView = (ListView) view.findViewById(R.id.tasklist);
        btNewTask = (Button) view.findViewById(R.id.btNewTask);
        etNewTask = (EditText) view.findViewById(R.id.etNewTask);
        delegate.registerTaskList(taskView);
        delegate.registerTaskAdder(btNewTask, etNewTask);
        return view;
    }
}

The ViewModel (activity)

Using an Activity as your ViewModel is almost the same. Instead, you only need to make sure that you create the Model here, and that you add your View (Fragment) to the activity...

public class ViewModelActivity extends ActionBarActivity implements ITaskListManager {

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_model);

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction().add(R.id.container, new ViewFragment()).commit();
        }

        initViewModel();
    }

    @Override
    public boolean onCreateOptionsMenu(final Menu menu) {

        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.view_model, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(final MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private Model db_model;
    private List<String> tasks;
    private ListView taskView;
    private EditText newTask;

    /**
     * Initialize the ViewModel
     */    
    private void initViewModel() {
        tasks = new ArrayList<String>();
        db_model = new Model(this);
    }

    private void deleteTask(final View view) {
        db_model.deleteEntry("title='" + ((TextView) view).getText().toString() + "'");
    }

    private void addTask(final View view) {
        final ContentValues data = new ContentValues();

        data.put("title", ((TextView) view).getText().toString());
        db_model.addEntry(data);
    }

    private void deleteAll() {
        db_model.deleteEntry(null);
    }

    private List<String> getTasks() {
        final Cursor c = db_model.findAll();
        tasks.clear();

        if (c != null) {
            c.moveToFirst();

            while (c.isAfterLast() == false) {
                tasks.add(c.getString(0));
                c.moveToNext();
            }

            c.close();
        }

        return tasks;
    }

    private void renderTodos() {
        // The ViewModel handles rendering and changes to the view's
        // data. The View simply provides a reference to its
        // elements.
        taskView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, getTasks().toArray(new String[] {})));
    }

    @Override
    public void registerTaskList(final ListView list) {
        taskView = list; // Keep reference for rendering later
        if (list.getAdapter() == null) // Show items at startup
        {
            renderTodos();
        }    

        list.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id) { // Tapping on any
                                                                                                                   // item in the list
                                                                                                                   // will delete that
                                                                                                                   // item from the
                                                                                                                   // database and
                                                                                                                   // re-render the list
                deleteTask(view);
                renderTodos();
            }
        });
    }

    @Override
    public void registerTaskAdder(final View button, final EditText input) {
        newTask = input;
        button.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(final View view) { // Add task to database, re-render list, and clear the input
                addTask(newTask);
                renderTodos();
                newTask.setText("");
            }
        });
    }
}

Extra

Adding new Views, or different views, should be handled in the activity. This is nice, since you can now listen for configuration changes, and swap in a special Fragment for a different orientation...