Android Design Patterns – MVP, DI using Dagger and Unit Testing

Design Patterns

Various Android Design Patterns can be used to enhance your android code base: among those MVC, MVVM, Reactive, MVP.

The goal of design pattern is to make your code and project more:

  1. Scalable
  2. Maintainable
  3. Testable

Using Standards Design patterns will make you more valuable as a developer. It will help you to work better in a team. Learning and applying patterns will also make you more adaptable to other frameworks.

Clean architecture/MVP pattern and unit testing are practices which helped me improve my code quality and to develop maintainable apps. In the process of applying a new pattern, you will be learning new techniques which you may apply in other places of your code base as well. The MVP pattern can have a slower learning curve than using “classic” android architecture where you have Activities/Fragments which act like a “ViewController” and contain mix of UI and Business logics. But in long run, using classic android does not bring much Pros compared to other design patterns.

Intro to MVP Pattern – with Example

MVP stands for Model-View-Presenter architectural pattern. It is a derivative of Model View Controller and is a more popular architecture.

Model-View-Presenter, the View are our Activities or Fragments which will contain only UI update logics. All logics which depends on Android SDK and related to UI will remain in View (Fragment or Activity). The “Presenter” will contain all business logics. Presenter will communicate to the “View” through interfaces (e.g to send data for the View to display on the User Interface). The Presenter layer should not include any Android SDK specifics implementations. The Presenter logic can be fully tested with JUnit tests. Model will include repositories and all data types we have to manipulate on the app.

Let’s look at simple Logic example to get basic concern. The example includes usage Dagger 2 and Rx Java.

(For more on Dagger 2, please check my blog post: https://devanshramen.com/2017/08/23/explaining-dependency-injection-with-dagger-2-android/ )

 

First thing, we need define a “Contract” which is basically an Interface which will define all the required methods on the View and the Presenter.

LoginContract

public interface LoginContract {

    interface View extends BaseContract.BaseView {

        void onLoginSuccessful();

        void onLoginFailed(String strError);

        void onErrorEmptyEmail();

        void onErrorInvalidEmail();

        void onErrorEmptyPassword();

    }

    interface Presenter extends BaseContract.BasePresenter {

        void callLoginAPI(UserLoginRequest userLogin);

        boolean validateLoginInputs(UserLoginRequest userLogin);
    }
}

 

In this example, we have a client-side validation to check for empty/invalid email, and password and finally and API call to Login Service. The validation logic, will be implemented inside the “validateLoginInputs(UserLoginRequest userLogin)” method which will be implemented by Presenter.

The Fragment will implement the View so as to implement the display error or success UI updates. The Presenter will be trigger UI update by calling the corresponding “View” methods. E.g, onErrorInvalidEmail() will display a hint message stating user’s have input an invalid email.

Below are implementations for LoginPresenter and LoginFragment:

LoginPresenter

/**
 * Created by devanshramen on 4/17/17.
 * @author devansh
 */

public class LoginPresenter extends BasePresenter implements LoginContract.Presenter {

    @NonNull
    LoginContract.View mView;

    public LoginPresenter(@NonNull LoginContract.View mView) {
        super(mView);
        this.mView = mView;
    }
    
    // Client validation
    @Override
    public boolean validateLoginInputs(UserLoginRequest userLogin) {

        boolean isValid = true;

        if (userLogin.getEmail().length() <= 0) {
            mView.onErrorEmptyEmail();
            isValid = false;
        }

        if (!EmailUtils.isValidEmail(userLogin.getEmail())) {
            mView.onErrorInvalidEmail();
            isValid = false;
        }

        if (userLogin.getPassword().length() <= 0) {
            mView.onErrorEmptyPassword();
            isValid = false;
        }
        return isValid;
    }

    @Override
    public void callLoginAPI(UserLoginRequest userLoginRequest) {

        mView.showLoading();

        Subscription s = apiService.loginRequest(userLoginRequest)
            .compose(RxUtils.applySchedulers())
            .subscribe(
                (UserLoginResponse userLoginResponse) -> {  // on Success

                    mView.hideLoading();
                    LogUtils.showLogInfo(userLoginResponse.toString());

                    if (userLoginResponse.isSuccess()) {
                        appSessionRepository.setFirstTime(true);
                        appSessionRepository.setLogin(true);

                        mView.onLoginSuccessful();

                    } else
                        new Throwable(context.getString(R.string.str_login_invalid_credentials));
                },
                (Throwable e) -> { // on Fail

                    if (e instanceof GenericException)
                        mView.onLoginFailed(e.getMessage());
                    else
                        mView.onLoginFailed(context.getString(R.string.str_login_invalid_credentials));

                    mView.hideLoading();
                },
                () -> { // on Complete
                    mView.hideLoading();
                });

        mSubscriptions.add(s);
    }
}

LoginFragment

/**
 * Created by devanshramen on 4/17/17.
 * @author devansh
 */

public class LoginFragment extends BaseFragment implements LoginContract.View {

    @Bind(R.id.edt_username)
    EditText edtUsername;

    @Bind(R.id.edt_password)
    EditText edtPassword;

    @Bind(R.id.txt_no_username_error)
    TextView txtInvalidUsername;

    @Bind(R.id.txt_invalid_password)
    TextView txtInvalidPassword;

    LoginActivity activity;

    private LoginContract.Presenter mPresenter;

    public LoginFragment() {
        // Required empty public constructor
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    protected int getResourceLayout() {
        return R.layout.fragment_login;
    }

    @Override
    protected void onViewReady(Bundle savedInstanceState) {
        initViews();
    }

    private void initViews() {
        mPresenter = new LoginPresenter(this);
        activity = (LoginActivity) getActivity();
    }

    @OnClick(R.id.btn_login)
    public void onClickBtnLogin() {

        UserLoginRequest userLogin = new UserLoginRequest();
        userLogin.setEmail(edtUsername.getText().toString());
        userLogin.setPassword(edtPassword.getText().toString());

        txtInvalidUsername.setVisibility(View.INVISIBLE);
        txtInvalidPassword.setVisibility(View.INVISIBLE);

        if (mPresenter.validateLoginInputs(userLogin)) {
            KeyboardUtils.hideKeyboard(getActivity());
            mPresenter.callLoginAPI(userLogin);
        }
    }

    @OnClick(R.id.txt_forget_password)
    public void onClickTxtForgetPassword() {
        ActivityUtils.openActivity(getContext(), ResetPasswordActivity.class,"");
    }

    @Override
    public void onLoginSuccessful() {
        ActivityUtils.openActivityAndFinish(getActivity(), MainDrawerActivity.class, "login");
    }

    @Override
    public void onLoginFailed(String strError) {
        dialogUtils.showDialogBox(getString(R.string.str_error_dialog), strError);
    }


    @Override
    public void onErrorEmptyEmail() {
        txtInvalidUsername.setText(getString(R.string.str_login_username_error_empty));
        txtInvalidUsername.setVisibility(View.VISIBLE);
    }

    @Override
    public void onErrorInvalidEmail() {
        txtInvalidUsername.setText(getString(R.string.str_login_username_error_invalid));
        txtInvalidUsername.setVisibility(View.VISIBLE);
    }

    @Override
    public void onErrorEmptyPassword() {
        txtInvalidPassword.setVisibility(View.VISIBLE);
    }
}

BasePresenter & Dependency Injection with Dagger 2

You may have noticed that our LoginPresenter is extending BasePresenter. In these Base classes, we have common initialization methods e.g Database, RxJava stuffs we will put in itself BasePresenter.

public class BasePresenter implements BaseContract.AppBasePresenter {

    @Nonnull
    BaseContract.AppBaseView mView;

    @NonNull
    protected CompositeSubscription mSubscriptions;

    @Inject
    protected APIService apiService;

    @Inject
    protected Context context;

    @Inject
    protected ProfileRepository profileRepository;

    public BasePresenter(@Nonnull BaseContract.AppBaseView mView) {
        this.mView = mView;

        MApplication.getAppComponent().inject(this); // DI using Dagger

        mSubscriptions = new CompositeSubscription();

        LogUtils.showLogDebug("subscribe");
    }

    @Override
    public void subscribe() {

    }

    @Override
    public void unSubscribe() {
        mSubscriptions.clear();
    }
}

We are using Dagger 2 here as our Dependency Injection framework. (For more about Dagger 2: Please check http://www.vogella.com/tutorials/Dagger/article.html) which describes it in enough details.

Unit Testing

We will also want to have Unit Tests on the Presenter such that we can check if we are implementing the right/expected business logic and also the right “UI” method is being called by the Presenter:

@RunWith(MockitoJUnitRunner.class)
public class LoginPresenterTest extends TestCase{

    @Mock
    private LoginPresenter mPresenter;

    @Mock
    private LoginContract.View mView;

    @Mock
    SessionRepository appSessionRepository;

    private UserLoginRequest userLogin;

    @Before
    public void setUpTesting() throws Exception{
        MockitoAnnotations.initMocks(this);

        mPresenter = new LoginPresenter(mView);
    }

    @Test
    public void testEmptyUsername() throws Exception {
        //test case passed - Data missing
        userLogin = new UserLoginRequest("", "xxx");
        mPresenter.validateLoginInputs(userLogin);
        verify(mView).onErrorEmptyEmail();
    }


    @Test
    public void testEmptyPassword() throws Exception {
        //test case passed - Data missing
        userLogin = new UserLoginRequest("xxx@xxxx.com", "");
        mPresenter.validateLoginInputs(userLogin);
        verify(mView).onErrorEmptyPassword();
    }

    @Test
    public void testValidUsernamePassword() throws Exception {
        //test case passed - Data missing
        userLogin = new UserLoginRequest("xxx@xxxx.com", "xxxx");
        assertTrue(mPresenter.validateLoginInputs(userLogin));
    }

}

We are using Mockito to mock the Views. (Please check this link for more about Mockito: http://www.vogella.com/tutorials/Mockito/article.html). Note that we are only testing if the appropriate View methods are called, we can’t really check if it is correctly implemented on the View since the implementation contains Android SDK specific implementations and will not be testable using JUnit framework.

That’s all for today. Catch you soon with a new topic. Please leave a comments should this be helpful to you.

Best Regards

 

Refs:

– Google’s Sample

2 thoughts on “Android Design Patterns – MVP, DI using Dagger and Unit Testing

    1. Thank you Mevin.

      “verify()” is a method from Mockito library.

      We actually created a Mock (implementation) of the View interface on this line:

      @Mock
      private LoginContract.View mView;

      verify(mView).onErrorEmptyEmail(); -> will check if the onErrorEmptyEmail() got called by the presenter. All communication from the Presenter to the View goes through the LoginContract.View interface. We can test if Presenter logic is correctly written, by verifying if the appropriate View method is getting called base on test data used.

      I hope that helps.

      Regards

Please Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s