Proper implementation of MVVM in Android

When it comes to design patterns in general. You want to keep business logic away from Activities and Fragments.

MVVM and MVP are both really good choices if you ask me. But since you want to implement MVVM. Then i will try to explain a little on how i implement it.

The activity

public class LoginActivity extends BaseActivity {

    private LoginActivityViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ActivityLoginBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_login);
        NavigationHelper navigationHelper = new NavigationHelper(this);
        ToastHelper toastHelper = new ToastHelper(this);
        ProgressDialogHelper progressDialogHelper = new ProgressDialogHelper(this);


        viewModel = new LoginActivityViewModel(navigationHelper,toastHelper,progressDialogHelper);
        binding.setViewModel(viewModel);
    }

    @Override
    protected void onPause() {
        if (viewModel != null) {
            viewModel.onPause();
        }

        super.onPause();
    }

    @Override
    protected void onDestroy() {
        if (viewModel != null) {
            viewModel.onDestroy();
        }

        super.onDestroy();
    }
}

This is a fairly simple activity. Nothing special. I just start with instantiating what my viewModel need. Because i try to keep everything android specific away from it. Everything to ease the writing of tests

Then i just bind the viewmodel to the view.

The view

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewModel"
            type="com.community.toucan.authentication.login.LoginActivityViewModel" />
    </data>


    <RelativeLayout
        android:id="@+id/activity_login_main_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/background"
        tools:context="com.community.toucan.authentication.login.LoginActivity">

        <ImageView
            android:id="@+id/activity_login_logo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="40dp"
            android:src="@drawable/logo_small" />

        <android.support.v7.widget.AppCompatEditText
            android:id="@+id/activity_login_email_input"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_below="@+id/activity_login_logo"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:layout_marginTop="60dp"
            android:drawableLeft="@drawable/ic_email_white"
            android:drawablePadding="10dp"
            android:hint="@string/email_address"
            android:inputType="textEmailAddress"
            android:maxLines="1"
            android:text="@={viewModel.username}" />

        <android.support.v7.widget.AppCompatEditText
            android:id="@+id/activity_login_password_input"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_below="@+id/activity_login_email_input"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:drawableLeft="@drawable/ic_lock_white"
            android:drawablePadding="10dp"
            android:hint="@string/password"
            android:inputType="textPassword"
            android:maxLines="1"
            android:text="@={viewModel.password}" />

        <Button
            android:id="@+id/activity_login_main_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/activity_login_password_input"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="10dp"
            android:background="@drawable/rounded_button"
            android:onClick="@{() -> viewModel.tryToLogin()}"
            android:paddingBottom="10dp"
            android:paddingLeft="60dp"
            android:paddingRight="60dp"
            android:paddingTop="10dp"
            android:text="@string/login"
            android:textColor="@color/color_white" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/activity_login_main_button"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="20dp"
            android:onClick="@{() -> viewModel.navigateToRegister()}"
            android:text="@string/signup_new_user"
            android:textSize="16dp" />


        <LinearLayout
            android:id="@+id/activity_login_social_buttons"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerInParent="true"
            android:layout_marginBottom="50dp"
            android:orientation="horizontal">


            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/facebook" />

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/twitter" />

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/google" />
        </LinearLayout>

        <TextView
            android:id="@+id/activity_login_social_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@+id/activity_login_social_buttons"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="20dp"
            android:text="@string/social_account"
            android:textSize="16dp" />
    </RelativeLayout>
</layout>

Fairly straight forward from the view side. I bind all the specific values the viewModel need to act on the logic it has.

https://developer.android.com/topic/libraries/data-binding/index.html Check the following link to get more knowledge on how the android databinding library works

The ViewModel

public class LoginActivityViewModel extends BaseViewModel implements FirebaseAuth.AuthStateListener {

    private final NavigationHelper navigationHelper;
    private final ProgressDialogHelper progressDialogHelper;
    private final ToastHelper toastHelper;
    private final FirebaseAuth firebaseAuth;

    private String username;
    private String password;


    public LoginActivityViewModel(NavigationHelper navigationHelper,
                                  ToastHelper toastHelper,
                                  ProgressDialogHelper progressDialogHelper) {

        this.navigationHelper = navigationHelper;
        this.toastHelper = toastHelper;
        this.progressDialogHelper = progressDialogHelper;

        firebaseAuth = FirebaseAuth.getInstance();
        firebaseAuth.addAuthStateListener(this);
    }

    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onDestroy() {
        firebaseAuth.removeAuthStateListener(this);
        super.onDestroy();
    }

    @Override
    public void onStop() {
        progressDialogHelper.onStop();
        super.onStop();
    }

    public void navigateToRegister() {
        navigationHelper.goToRegisterPage();
    }

    public void tryToLogin() {
        progressDialogHelper.show();
        if (validInput()) {
            firebaseAuth.signInWithEmailAndPassword(username, password)
                    .addOnCompleteListener(new OnCompleteListener<AuthResult>() {
                        @Override
                        public void onComplete(@NonNull Task<AuthResult> task) {
                            if (!task.isSuccessful()) {
                                String message = task.getException().getMessage();
                                toastHelper.showLongToast(message);
                            }
                            progressDialogHelper.hide();
                        }
                    });
        }
    }

    private boolean validInput() {
        return true;
    }

    @Override
    public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
        if (firebaseAuth.getCurrentUser() != null) {
            navigationHelper.goToMainPage();
        }
    }

   @Bindable
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
        notifyPropertyChanged(BR.username);
    }

    @Bindable
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
        notifyPropertyChanged(BR.password);
    }
}

Here is where all the fun happens. I use the helper classes to show and act with the android system. Otherwise i try to keep the logic as clean as possible. Everything is made so it is easier for me to create and test the logic.

Keep note

I bound the username and password with the view. So every change made to the EditText will automatically be added to the field. In that way. I do not need to add any specific listener

Hope this small showcase can help you understand a little how you could implement MVVM into your own projects


As for me - MVVM, MVP and other really cool patterns for really cool guys do not have a straightforward receipt/flow. Of course you have a lot of tutorial/recommendations/patterns and approaches how to implement them. But that's actually what all programming is about - you just need to come up with a solution which fits your needs. Depending on your developers vision you can apply a lot of principles to your solution to make it easier/faster to develop/test/support.
In your case I think it is better to move this kind of logic to Fragment transitions(as you have done in setFreeTrialFragment()), it's more customizable and comfortable to use. But nevertheless if your approach should stay the same - existing one is normal. Actually @BindingAdapter is more suitable for xml attributes then a direct usage.
As for me - all of the UI logic should reside in the Activity, the main purpose is to separate business logic from UI. Because of that all animations, fragment transactions and so on are handled inside of the activity - that's mine approach. ViewModel - is responsible for notifying the view that something has changed in corresponding model and the view should arrange itself to those changes. In perfect world you should be able to achieve such a popular term as two-way binding, but it is not always necessary and not always UI-changes should be handled inside the ViewModel. As usual, too much MVVM is bad for your project. It can cause Spaghetti code, "where that's from?", "how to recycler view?" and other popular issues. So it should be used only to make life eaisier, not to make everything ideal, because like every other pattern it will make a lot of head ache and someone who will look through your code will say "OVERENGINEERING!!11".

Per request, MVP example :

Here you have some helpful articles :

  • Quite simple example.
  • Here you have a good description with integration guide.
  • First and second part of this articles may be more then helpful.
  • This one is short and really descriptive.

Short example(generalized), you should fit it to yours architecture :

Package representation :
enter image description here

Implementation :

Model :

public class GalleryItem {

    private String mImagePath;
    //other variables/getters/setters
}

Presenter :

//cool presenter with a lot of stuff
public class GalleryPresenter {

    private GalleryView mGalleryView;

    public void loadPicturesBySomeCreteria(Criteria criteria){
        //perform loading here
        //notify your activity
        mGalleryView.setGalleryItems(yourGaleryItems);
    }

    //you can use any other suitable name
    public void bind(GalleryView galleryView) {
        mGalleryView = galleryView;
    }

    public void unbind() {
        mGalleryView = null;
    }

    //Abstraction for basic communication with activity.
    //We can say that this is our protocol
    public interface GalleryView {
        void setGalleryItems(List<GalleryItem> items);

    }
}

View :

public class NiceGalleryView extends View {
    public NiceGalleryView(Context context) {
        super(context);
    }

    public NiceGalleryView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // TODO: 29.12.16 do your stuff here
}

And of cource the activity code :

public class GalleryActivity extends AppCompatActivity implements GalleryPresenter.GalleryView {

    private GalleryPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_gallery);
        //init views and so on
        mPresenter = new GalleryPresenter();
        mPresenter.bind(this);

    }

    @Override
    public void setGalleryItems(List<GalleryItem> items) {
        //use RecyclerView or any other stuff to fill your UI
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPresenter.unbind();
    }
}

Also be aware that you even have a lot of different approaches while using MVP. I just want to emphasize that I prefer initialize views in activity and do not pass them out of activity. You can manage this through interface and thats really comfortable not just for development, but even for instrumental tests.

Tags:

Android

Mvvm