12/08/2018, 15:14

MVP Pattern with Data binding Android

Abstract MVP (Model View Presenter) pattern (not an architectural pattern) is a based on the popular MVC (Model View Controller), which has been gaining a lot of reputations in the development of Android applications. This article is an introduction to MVP and by doing so show the advantages and ...

Abstract

MVP (Model View Presenter) pattern (not an architectural pattern) is a based on the popular MVC (Model View Controller), which has been gaining a lot of reputations in the development of Android applications. This article is an introduction to MVP and by doing so show the advantages and disadvantages in embracing this pattern into your Android projects.

Introduction

Architecture Pattern is a fundamental part of computer science. It’s the only way to maintain a project clean, expansible and testable. The patterns are recognized solutions that have been developed over the years and are considered industry standards. They’re constantly evolving, and in the Android SDK, the reliable Model View Controller pattern is steadily being dropped for the Model View Presenter.

So what is MVP? The MVP pattern allows the separation of the presentation layer from the logic, so that everything about how the interface works is separated from how we represent it on screen. Ideally the MVP pattern would achieve that same logic might have completely different and interchangeable views. As MVP isnt an architectural partern and is only responsible for the presentation layer . Therfore it is always better to use it for your architecture rather that not using it at all.

Why use MVP

There have always been the discussion on which of this architectural pattern is best suiltable to use in the project as there are many examples are MVC (Model View Controller), MVVM (Model–view–viewmodel) or MVP. Even though MVC is a reliable and well known solution, it’s losing ground for its younger brother, the Model View Presenter (MVP), that offers some advantages, like a more well defined separation of concerns. In MVP the presenter assumes the functionality of the “middle-man”. In MVP, all presentation logic is pushed to the presenter, this makes it easier for testing such as unit test and cleaner project structure and maintanability in huge and complex projects. In Android, problem arises from the fact that Android activities are closely coupled to both interface and data access mechanisms. We can find extreme examples such as CursorAdapter, which mix adapters, which are part of the view, with cursors, something that should be relegated to the depths of data access layer .For an application to be easily extensible and maintainable and testable we need to define well separated layers and this is probably the biggest advantage that we’ll get with the MVP adoption. What do we do tomorrow if, instead of retrieving the same data from a database, we need to do it from a web service? We would have to redo our entire view. The above representation shows the structural image of MVP when applied on Android. The VIEW consists of the user interfaces such as our activities or fragments while the MODEL is the business logic layer where all data operations is carried out (Database, Api, Models...) and finally the PRESENTER which is the middle man between the model and the view. With MVP we are able to take most of logic out from the activities so that we can test it without using instrumentation tests .

M for Model The Model is an interface for managing data. Its responsibilities includes using APIs, data caching, database managements and so on. The model is not limited to this only however, it can also be an interface that communicates with other modules in charge of these responsibilities. V for View The view, usually implemented by an Activity (it may be a Fragment, a View… depending on how the app is structured), will contain a reference to the presenter. Presenter will be ideally provided by a dependency injector such as Dagger, but in case you don’t use something like this, it will be responsible for creating the presenter object. The only thing that the view will do is calling a method from the presenter every time there is an interface action (a button click for example). P for Presenter Presenter acts as the middle-man between model and view. All your presentation logic belongs to it. The presenter is responsible for querying the model and updating the view, reacting to user interactions updating the model. But unlike the typical MVC, it also decides what happens when you interact with the view.

Why use Data binding with Mvp? Combining Databinding along wih MVP pattern can result in a very clean structure and maintainable project. Databinding saves u a lot of stress and uneccesary long lines of code. Your UI is updated eaily and gone are those days where you need "findViewById" and onclick listeners and so on. Imagine having a very huge project, Databiniding saves a lot of unceccessary codes and make your project easier to read and maintain while MVP will keep everything structured and easy to test. Now i will demo a simple app just to demostrate how the MVP patern works and what it may look like to implement it in a project. This app will be a simple login project.

Implementing MVP for Android

The Project Previously we saw how to enable the databinding feature. Now lets create a simple login project. Create new project, ill be naming mine the MvpDFunDemo.

Change the Activity name to LoginActivity and move to a package called "login". Next create two classes one being the presenter "LoginPresnter" and the other "LoginContract". To demostrate the use of MVP in Android i will create a simple demo project. This project will use MVP alongside databinding so first thing to do will be to edit out gradle file to support databinding:

android {
    ....
    dataBinding {
        enabled = true
    }
}

also add the ormlite library to your dependencies compile 'com.j256.ormlite:ormlite-android:4.48'

Before we begin lets create a App class so that we can easily access our app's context and such:

import android.app.Application;
import android.content.Context;

public class App extends Application {
    private static App mSelf;

    public static App self() {
        return mSelf;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mSelf = this;
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
    }
}

Go to your manifest and add to your aplication tag android:name=".App"

Create a package and name it Model. Here we will create our model class "Contact"

Contact

import com.j256.ormlite.field.DatabaseField;
import java.io.Serializable;

public class Contact implements Serializable {

    @DatabaseField(generatedId = true)
    int mId;
    @DatabaseField
    String mName;
    @DatabaseField
    String mMobile;
    @DatabaseField
    String mEmail;
    @DatabaseField
    String mImageUrl;
    @DatabaseField
    String mPassword;

    public Contact() {
    }

    public Contact(String email, String password) {
        this.mEmail = email;
        this.mPassword = password;
    }

    public Contact(String mName, String mMobile, String mEmail, String mImageUrl, String password) {
        this.mName = mName;
        this.mMobile = mMobile;
        this.mEmail = mEmail;
        this.mImageUrl = mImageUrl;
        this.mPassword = password;
    }

    public Contact(int mId, String mName, String mMobile, String mEmail, String mImageUrl, String password) {
        this.mId = mId;
        this.mName = mName;
        this.mMobile = mMobile;
        this.mEmail = mEmail;
        this.mImageUrl = mImageUrl;
        this.mPassword = password;
    }

    public int getId() {
        return mId;
    }

    public void setId(int mId) {
        this.mId = mId;
    }

    public String getName() {
        return mName;
    }

    public void setName(String mName) {
        this.mName = mName;
    }

    public String getMobile() {
        return mMobile;
    }

    public void setMobile(String mMobile) {
        this.mMobile = mMobile;
    }

    public String getEmail() {
        return mEmail;
    }

    public void setEmail(String mEmail) {
        this.mEmail = mEmail;
    }

    public String getImageUrl() {
        return mImageUrl;
    }

    public void setImageUrl(String mImageUrl) {
        this.mImageUrl = mImageUrl;
    }

    public String getPassword() {
        return mPassword;
    }

    public void setPassword(String password) {
        mPassword = password;
    }
}

LoginContract

import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;

public interface LoginContract {
    interface ViewModel {
        void login(Contact contact);
        void showLoginFailed(String error);
        void register();
    }
    interface Presenter {
        void doLogin();
        void register();
    }
}

Login Presenter

import android.databinding.ObservableField;
import android.text.TextUtils;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.App;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.R;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.database.UserManagerUtil;
import java.util.ArrayList;
import java.util.List;

public class LoginPresenter implements LoginContract.Presenter {
    public ObservableField<String> email;
    public ObservableField<String> password;
    private LoginContract.ViewModel mViewModel;
    private List<Contact> allContacts;

    public LoginPresenter(LoginContract.ViewModel viewModel) {
        mViewModel = viewModel;
        initFields();
        allContacts = UserManagerUtil.getListContacts(App.self());
    }

    private void initFields() {
        email = new ObservableField<>();
        password = new ObservableField<>();
    }

    private boolean isValidate() {
        if (TextUtils.isEmpty(email.get())) {
            mViewModel.showLoginFailed(App.self().getString(R.string.err_email));
            return false;
        }
        if (TextUtils.isEmpty(password.get())) {
            mViewModel.showLoginFailed(App.self().getString(R.string.err_password));
            return false;
        }
        return true;
    }

    @Override
    public void doLogin() {
        if (isValidate()) {
            if (allContacts == null) allContacts = new ArrayList<>();
            Contact validContact = new Contact(email.get(), password.get());
            for (Contact contact : allContacts) {
                if (contact.getEmail().equals(validContact.getEmail()) && contact.getPassword()
                        .equals(validContact.getPassword())) {
                    mViewModel.login(contact);
                    return;
                }
            }
            mViewModel.showLoginFailed(App.self().getString(R.string.err_login));
        }
    }

    @Override
    public void register() {
        mViewModel.register();
    }
}

Next lets create an Activityt and call it LoginAvtivity LoginAvtivity

import android.content.Intent;
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Constants.Constant;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.R;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.databinding.ActivityLoginBinding;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.home.HomeActivity;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.register.RegisterActivity;

public class LoginActivity extends AppCompatActivity implements LoginContract.ViewModel {
    private LoginPresenter mPresenter;
    private ActivityLoginBinding mBinding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_login);
        mPresenter = new LoginPresenter(this);
        mBinding.setPresenter(mPresenter);
    }

    @Override
    public void login(Contact contact) {
        if (contact == null) return;
        startActivity(
                new Intent(this, HomeActivity.class).putExtra(Constant.EXTRA_CONTACT, contact));
        finish();
    }

    @Override
    public void showLoginFailed(String error) {
        Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void register() {
        startActivity(new Intent(this, RegisterActivity.class));
    }
}

Now for the layout of LoginAvtivity. Because we are using data binding, it is necessary to include the <layout> tag and reference our variables (Could be our presenter or contract or even Java imports) activity_login.xml

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    >

    <data>
        <variable
            name="presenter"
            type="com.example.framgiababatundefatoyesunday.mymvpfundemo.login.LoginPresenter"
            />
    </data>

    <RelativeLayout
        android:layout_awidth="match_parent"
        android:layout_height="match_parent"
        >

        <EditText
            android:id="@+id/editTextEmail"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/hint_email"
            android:inputType="textEmailAddress"
            android:padding="10dp"
            android:text="@={presenter.email}"
            />

        <EditText
            android:id="@+id/editTextPassword"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/editTextEmail"
            android:hint="@string/hint_password"
            android:inputType="textPassword"
            android:padding="10dp"
            android:text="@={presenter.password}"
            />

        <TextView
            android:id="@+id/textView"
            android:layout_awidth="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/editTextPassword"
            android:onClick="@{ () -> presenter.register()}"
            android:padding="10dp"
            android:text="@string/text_register"
            android:textColor="@color/colorAccent"
            />

        <Button
            android:id="@+id/btn_login"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/textView"
            android:onClick="@{ () -> presenter.doLogin()}"
            android:padding="10dp"
            android:text="@string/btn_login"
            />
    </RelativeLayout>
</layout>

The variable name "presenter" is a pointer to our LoginPresenter class. This variable pointer can be used to access our methods and more. Next, what is a login without a Registraion? Lets create a Registration for the app. However we need a database ofsort to keep our records and in this project i will be using Ormlite, a popular DB library for android.

Import to Ormlite Gradle compile 'com.j256.ormlite:ormlite-android:4.48'

Next create a package and name it database and in this create this classes DatabaseHelper

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

public class DataBaseHelper extends OrmLiteSqliteOpenHelper {
    public static final Class<?>[] DataBaseClasses = new Class[] {
            Contact.class
    };
    private static final String DATABASE_NAME = "Mvp.sqlite";
    private static final int DATABASE_VERSION = 1;
    private Map<String, Dao<?, ?>> daoMap = new HashMap<>();

    public DataBaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
        try {
            for (Class obj : DataBaseClasses) {
                TableUtils.createTable(connectionSource, obj);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource,
            int oldVersion, int newVersion) {
        if (oldVersion != newVersion) {
            try {
                for (Class obj : DataBaseClasses) {
                    TableUtils.dropTable(connectionSource, obj, false);
                }
                onCreate(database, connectionSource);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    public void clearDatabase() {
        for (Class clazz : DataBaseClasses) {
            try {
                TableUtils.clearTable(getConnectionSource(), clazz);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    @SuppressWarnings("unchecked")
    public <T, ID> Dao<T, ID> getCachedDao(Class<T> clazz) {
        Dao<?, ?> result = daoMap.get(clazz.getName());
        if (result == null) {
            try {
                result = getDao(clazz);
                daoMap.put(clazz.getName(), result);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return (Dao<T, ID>) result;
    }
}

RegistrationContract

import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;

public interface RegisterContract {
    interface ViewModel {
        void login(Contact contact);
        void showToast(String message);
        void pickImage();
    }
    interface Presenter {
        void doLogin();
        void filePicker();
    }
}

ContactManagerUtil

import android.content.Context;
import android.os.AsyncTask;
import android.widget.Toast;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;
import com.j256.ormlite.android.apptools.OpenHelperManager;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.stmt.DeleteBuilder;
import com.j256.ormlite.stmt.PreparedQuery;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.stmt.UpdateBuilder;
import com.j256.ormlite.stmt.Where;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;

public class ContactManagerUtil {
    public static void saveContact(final Context context, final Contact contact) {
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    DataBaseHelper dataBaseHelper =
                            OpenHelperManager.getHelper(context, DataBaseHelper.class);

                    Dao<Contact, Integer> dao = dataBaseHelper.getCachedDao(Contact.class);
                    dao.createOrUpdate(contact);
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    OpenHelperManager.releaseHelper();
                }
            }
        });
        Toast.makeText(context, "SAVED", Toast.LENGTH_SHORT).show();
    }

    public static void saveListContacts(Context context, List<Contact> list) {
        for (Contact contact : list)
            saveContact(context, contact);
    }

    public static List<Contact> getListContacts(Context context) {
        DataBaseHelper dataBaseHelper = OpenHelperManager.getHelper(context, DataBaseHelper.class);

        Dao<Contact, Integer> dao = dataBaseHelper.getCachedDao(Contact.class);
        QueryBuilder<Contact, Integer> query = dao.queryBuilder();
        try {
            //query.where().eq("mId", taskId);
            PreparedQuery<Contact> prepare = query.prepare();
            List<Contact> result = dao.query(prepare);
            Collections.reverse(result);
            return result;
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            OpenHelperManager.releaseHelper();
        }

        return null;
    }

    public static int deleteContact(Context context, int id) {
        DataBaseHelper dataBaseHelper = OpenHelperManager.getHelper(context, DataBaseHelper.class);

        Dao<Contact, Integer> dao = dataBaseHelper.getCachedDao(Contact.class);
        DeleteBuilder<Contact, Integer> query = dao.deleteBuilder();
        Where where = query.where();
        try {
            where.eq("mId", id);
            return dao.delete(query.prepare());
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            OpenHelperManager.releaseHelper();
        }

        return 0;
    }

    public static void updateContact(Context context, Contact contact) {
        DataBaseHelper dataBaseHelper = OpenHelperManager.getHelper(context, DataBaseHelper.class);
        Dao<Contact, Integer> catDao = dataBaseHelper.getCachedDao(Contact.class);
        UpdateBuilder<Contact, Integer> updateBuilder = catDao.updateBuilder();
        try {
            updateBuilder.where().eq("mId", contact.getId());
            // update the value of your field(s)
            updateBuilder.updateColumnValue("mName", contact.getName());
            updateBuilder.updateColumnValue("mMobile", contact.getMobile());
            updateBuilder.updateColumnValue("mEmail", contact.getEmail());
            updateBuilder.updateColumnValue("mImageUrl", contact.getImageUrl());
            updateBuilder.updateColumnValue("mPassword", contact.getPassword());
            updateBuilder.update();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            OpenHelperManager.releaseHelper();
        }
    }
}

Create package Constant. inside create a class "Constant" Constant

public class Constant {
    public static final String EXTRA_CONTACT = "contact";
}

Now that the database is ready, we can proceed in creating the registration. Create a package and name it register.

RegisterContract

public interface RegisterContract {
    interface ViewModel {
        void login(Contact contact);
        void showToast(String message);
        void pickImage();
    }
    interface Presenter {
        void doLogin();
        void filePicker();
    }
}

RegistrationPresenter

import android.databinding.ObservableBoolean;
import android.databinding.ObservableField;
import android.text.TextUtils;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.App;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.R;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.database.ContactManagerUtil;
import java.util.List;

public class RegisterPresenter implements RegisterContract.Presenter {
    public ObservableField<String> username;
    public ObservableField<String> mobile;
    public ObservableField<String> email;
    public ObservableField<String> password;
    public ObservableField<String> imageUrl;
    public ObservableBoolean isImageSelected;
    public ObservableBoolean isNewOrEdit;
    private RegisterContract.ViewModel mViewModel;
    private List<Contact> mContactList;
    private Boolean isUpdate = false;
    private Contact mContact;

    public RegisterPresenter(RegisterContract.ViewModel viewModel, Contact contact) {
        mViewModel = viewModel;
        initFields();
        setValues(contact);
    }

    private void initFields() {
        username = new ObservableField<>();
        mobile = new ObservableField<>();
        email = new ObservableField<>();
        password = new ObservableField<>();
        imageUrl = new ObservableField<>();
        isImageSelected = new ObservableBoolean();
        isNewOrEdit = new ObservableBoolean();
        mContactList = ContactManagerUtil.getListContacts(App.self());
    }

    private void setValues(Contact contact) {
        if (contact == null) return;
        mContact = contact;
        username.set(contact.getName());
        mobile.set(contact.getMobile());
        email.set(contact.getEmail());
        if (TextUtils.isEmpty(contact.getImageUrl())) {
            isImageSelected.set(false);
        } else {
            imageUrl.set(contact.getImageUrl());
            isImageSelected.set(true);
        }
        isNewOrEdit.set(true);
        isUpdate = true;
    }

    private boolean isValidate() {
        if (TextUtils.isEmpty(username.get())) {
            mViewModel.showToast(App.self().getString(R.string.err_username));
            return false;
        }
        if (TextUtils.isEmpty(mobile.get())) {
            mViewModel.showToast(App.self().getString(R.string.err_phone));
            return false;
        }
        if (TextUtils.isEmpty(email.get())) {
            mViewModel.showToast(App.self().getString(R.string.err_email));
            return false;
        }
        if (TextUtils.isEmpty(password.get())) {
            mViewModel.showToast(App.self().getString(R.string.err_password));
            return false;
        }

        if (!isUpdate) {
            for (Contact contact : mContactList) {
                if (contact.getEmail().equals(email.get())) {
                    mViewModel.showToast(App.self().getString(R.string.err_user_exist));
                    return false;
                }
            }
        }

        return true;
    }

    @Override
    public void doLogin() {
        if (isValidate()) {
            if (isUpdate) {
                Contact contact =
                        new Contact(mContact.getId(), username.get(), mobile.get(), email.get(),
                                imageUrl.get(), password.get());
                ContactManagerUtil.updateContact(App.self(), contact);
                mViewModel.login(contact);
                return;
            }
            Contact contact = new Contact(username.get(), mobile.get(), email.get(), imageUrl.get(),
                    password.get());
            ContactManagerUtil.saveContact(App.self(), contact);
            mViewModel.login(contact);
        }
    }

    @Override
    public void filePicker() {
        mViewModel.pickImage();
    }
}

RegistrationActivity

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.databinding.DataBindingUtil;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Constants.Constant;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.R;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.databinding.ActivityRegisterBinding;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.home.HomeActivity;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.util.ImageFilePath;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.util.PermissionsUtil;

public class RegisterActivity extends AppCompatActivity implements RegisterContract.ViewModel {
    private final static int MEDIA_LIBRARY = 1;
    private final int STORAGE_PERMISSION_ID = 0;
    private RegisterPresenter mPresenter;
    private ActivityRegisterBinding mBinding;
    private Contact mContact;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_register);
        getData();
        mPresenter = new RegisterPresenter(this, mContact);
        mBinding.setPresenter(mPresenter);
    }

    private void getData() {
        Intent intent = this.getIntent();
        if (intent==null)return;
        mContact = (Contact) intent.getSerializableExtra(Constant.EXTRA_CONTACT);
    }

    @Override
    public void login(Contact contact) {
        if (contact == null) return;
        startActivity(
                new Intent(this, HomeActivity.class).putExtra(Constant.EXTRA_CONTACT, contact));
        finish();
    }

    @Override
    public void showToast(String message) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void pickImage() {
        selectImage();
    }

    private void selectImage() {
        Intent intent;
        if (checkStorePermission(STORAGE_PERMISSION_ID)) {
            if (Build.VERSION.SDK_INT < 19) {
                intent = new Intent(Intent.ACTION_GET_CONTENT);
                intent.setType("image/*");
            } else {
                intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setType("*/*");
                intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] { "image/*" });
            }
            startActivityForResult(intent, MEDIA_LIBRARY);
        } else {
            showRequestPermission(STORAGE_PERMISSION_ID);
        }
    }

    private boolean checkStorePermission(int permission) {
        if (permission == STORAGE_PERMISSION_ID) {
            return PermissionsUtil.checkPermissions(this,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE);
        } else {
            return true;
        }
    }

    private void showRequestPermission(int requestCode) {
        String[] permissions;
        if (requestCode == STORAGE_PERMISSION_ID) {
            permissions = new String[] {
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.READ_EXTERNAL_STORAGE
            };
        } else {
            permissions = new String[] {
                    Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.READ_EXTERNAL_STORAGE
            };
        }
        PermissionsUtil.requestPermissions(this, requestCode, permissions);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        for (int grantResult : grantResults) {
            if (grantResult != PackageManager.PERMISSION_GRANTED) {
                break;
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, requestCode, data);
        if (resultCode != Activity.RESULT_OK) {
            return;
        }
        try {
            if (data.getData() == null) {
                showToast(getString(R.string.err_image));
                return;
            }
            Uri uri = data.getData();
            String path = ImageFilePath.getPath(this, uri);
            mPresenter.imageUrl.set(path);
            mPresenter.isImageSelected.set(true);
        } catch (Exception e) {
            //e.printStackTrace();
        }
    }
}

Lets create a package and name it utils. In here we will have a class ImageFilePath and PermissionsUtil for android 5.0 and upward

ImageFilePath

import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;

public class ImageFilePath {

    @SuppressLint("NewApi")
    public static String getPath(final Context context, final Uri uri) {

        //check here to KITKAT or new version
        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;

        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {

            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                } else {
                    return String.format("%s%s%s%s", "/storage/", type, "/", split[1]);
                }
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {

                final String id = DocumentsContract.getDocumentId(uri);
                final Uri contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

                return getDataColumn(context, contentUri, null, null);
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[] {
                        split[1]
                };

                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {

            // Return the remote address
            if (isGooglePhotosUri(uri)) return uri.getLastPathSegment();

            return getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }

    public static String getDataColumn(Context context, Uri uri, String selection,
            String[] selectionArgs) {

        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {
                column
        };

        try {
            cursor = context.getContentResolver()
                    .query(uri, projection, selection, selectionArgs, null);
            if (cursor != null && cursor.moveToFirst()) {
                final int index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(index);
            }
        } finally {
            if (cursor != null) cursor.close();
        }
        return null;
    }

    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    public static boolean isGooglePhotosUri(Uri uri) {
        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
    }
}

PermissionUtil

import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;

public class PermissionsUtil {
    public static boolean checkPermissions(Context context, String... permissions) {
        for (String permission : permissions) {
            if (!checkPermission(context, permission)) {
                return false;
            }
        }
        return true;
    }

    public static boolean checkPermission(Context context, String permission) {
        return ActivityCompat.checkSelfPermission(context, permission)
                == PackageManager.PERMISSION_GRANTED;
    }

    public static void requestPermissions(Activity activity, int requestCode,
            String... permissions) {
        ActivityCompat.requestPermissions(activity, permissions, requestCode);
    }
}

activity_register.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    >
    <data>
        <variable
            name="presenter"
            type="com.example.framgiababatundefatoyesunday.mymvpfundemo.register.RegisterPresenter"
            />
        <import type="android.view.View"/>
    </data>
    <RelativeLayout
        android:layout_awidth="match_parent"
        android:layout_height="match_parent"
        >

        <ImageView
            android:id="@+id/imageViewSelectPic"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{ () -> presenter.filePicker()}"
            android:src="@android:drawable/ic_menu_camera"
            android:visibility="@{ presenter.isImageSelected.get()?View.GONE:View.VISIBLE }"
            />

        <ImageView
            android:id="@+id/imageViewPicture"
            android:layout_awidth="match_parent"
            android:layout_height="200dp"
            android:layout_below="@+id/imageViewSelectPic"
            android:onClick="@{ () -> presenter.filePicker()}"
            android:src="@{presenter.imageUrl}"
            android:visibility="@{ presenter.isImageSelected.get()?View.VISIBLE:View.GONE }"
            />
        <EditText
            android:id="@+id/editTextName"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/imageViewPicture"
            android:layout_weight="1"
            android:hint="@string/hint_name"
            android:inputType="textPersonName"
            android:text="@={presenter.username}"
            />
        <EditText
            android:id="@+id/editTextMobile"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/editTextName"
            android:layout_centerVertical="true"
            android:hint="@string/hint_mobile"
            android:inputType="phone"
            android:text="@={presenter.mobile}"
            />
        <EditText
            android:id="@+id/editTextEmail"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/editTextMobile"
            android:hint="@string/hint_email"
            android:inputType="textEmailAddress"
            android:text="@={presenter.email}"
            />
        <EditText
            android:id="@+id/editTextPassword"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/editTextEmail"
            android:hint="@string/hint_password"
            android:inputType="textPassword"
            android:text="@={presenter.password}"
            />
        <Button
            android:id="@+id/btn_login"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:onClick="@{ () -> presenter.doLogin()}"
            android:text="@{ presenter.isNewOrEdit.get()?@string/btn_save:@string/btn_login}"
            />

         
             
             
             
             
             
             
             
             
             
    </RelativeLayout>
</layout>

. Create a package and name it home.

HomeContract

import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;

public interface HomeContract {
    interface ViewModel {
        void logout();
        void editUser(Contact contact);
    }
    interface Presenter {
        void logout();
        void editUser();
    }
}

HomePresenter

import android.databinding.ObservableField;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;

public class HomePresenter implements HomeContract.Presenter {
    public ObservableField<String> email;
    public ObservableField<String> name;
    public ObservableField<String> imageUrl;
    public ObservableField<String> phone;
    private Contact mContact;
    private HomeContract.ViewModel mViewModel;

    public HomePresenter(HomeContract.ViewModel viewModel, Contact contact) {
        mViewModel = viewModel;
        mContact = contact;
        init();
    }

    private void init() {
        email = new ObservableField<>();
        name = new ObservableField<>();
        imageUrl = new ObservableField<>();
        phone = new ObservableField<>();
        email.set(mContact.getEmail());
        name.set(mContact.getName());
        phone.set(mContact.getMobile());
        imageUrl.set(mContact.getImageUrl());
    }

    @Override
    public void logout() {
        mViewModel.logout();
    }

    @Override
    public void editUser() {
        mViewModel.editUser(mContact);
    }
}

HomeActivity

import android.content.Intent;
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Constants.Constant;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.Model.Contact;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.R;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.databinding.ActivityHomeBinding;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.login.LoginActivity;
import com.example.framgiababatundefatoyesunday.mymvpfundemo.register.RegisterActivity;

public class HomeActivity extends AppCompatActivity implements HomeContract.ViewModel {
    private HomePresenter mPresenter;
    private ActivityHomeBinding mBinding;
    private Contact mContact;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_home);
        getIntentExtras();
        mPresenter = new HomePresenter(this, mContact);
        mBinding.setPresenter(mPresenter);
    }

    private void getIntentExtras() {
        Intent intent = this.getIntent();
        mContact = (Contact) intent.getSerializableExtra(Constant.EXTRA_CONTACT);
    }

    @Override
    public void logout() {
        startActivity(new Intent(this, LoginActivity.class));
        finish();
    }

    @Override
    public void editUser(Contact contact) {
        startActivity(new Intent(this, RegisterActivity.class).putExtra(Constant.EXTRA_CONTACT, contact));
        finish();
    }
}

activity_home

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    >

    <data>
        <variable
            name="presenter"
            type="com.example.framgiababatundefatoyesunday.mymvpfundemo.home.HomePresenter"
            />
    </data>

    <RelativeLayout
        android:layout_awidth="match_parent"
        android:layout_height="match_parent"
        >

        <ImageView
            android:id="@+id/iv_profile"
            android:layout_awidth="match_parent"
            android:layout_height="200dp"
            android:scaleType="fitXY"
            android:src="@{presenter.imageUrl}"
            />

        <TextView
            android:id="@+id/tv_username"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/iv_profile"
            android:background="@color/colorAccent"
            android:text="@{String.valueOf(presenter.name).toUpperCase()}"
            android:textColor="@color/colorWhite"
            android:textSize="24dp"
            />

        <TextView
            android:id="@+id/tv_email"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/tv_username"
            android:drawableLeft="@android:drawable/ic_dialog_email"
            android:gravity="center_vertical"
            android:text="@{presenter.email}"
            android:textSize="12dp"
            />

        <TextView
            android:id="@+id/tv_phone"
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/tv_email"
            android:drawableLeft="@android:drawable/ic_menu_call"
            android:gravity="center_vertical"
            android:text="@{presenter.phone}"
            android:textSize="12dp"
            />

        <LinearLayout
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            >
            <Button
                android:id="@+id/btn_login"
                android:layout_awidth="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:onClick="@{ () -> presenter.logout()}"
                android:text="@string/btn_logout"
                />

            <Button
                android:layout_awidth="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:onClick="@{ () -> presenter.editUser()}"
                android:text="@string/btn_edit"
                />
        </LinearLayout>
    </RelativeLayout>
</layout>

Here is the full manifest file. Build the app and thats it. Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.framgiababatundefatoyesunday.mymvpfundemo">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

    <application
        android:name=".App"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".home.HomeActivity"/>
        <activity android:name=".register.RegisterActivity"/>
        <activity android:name=".login.LoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

Also your project structure should look something like this :

Advantages of Mvp

  • Clear separation of responsibilities between components. This separation allows for an easier understanding and maintenance of the code base.
  • Modularity. Modularity allows you to e.g. switch to a different implementation of view component in order to completely change application's UI, while all other components remain intact.
  • Easier testing. Since there are well defined boundaries between components, it becomes much easier to test each component in isolation (by e.g. mocking other components).
  • Unit testing is damn hard on Android (in several aspects). Therefore, having a clear boundary between components and being able to mock them out is especially important if you want the code to be testable.
  • Android framework does not encourage developers to write a clean code (to say the least). Adhering to a clear set of practices in this case is especially important.
  • It separates the UI logic from the UI construction code.
0