Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper implementation of MVVM in Android

Tags:

android

mvvm

I have been struggling to find the right way to implement MVVM in Android.

The whole Idea is still blurry to me, the pattern is to have a separate layer in which the logic is done (ViewModel).

This piece of code only animates the alpha of a background in which a bunch of fragments live.

public class StartActivity extends AppCompatActivity implements EntryFragment.EntryFragementListener {

    private static final float MINIMUM_ALPHA = 0.4f;
    private static final float MAXIMUM_ALPHA = 0.7f;

    @State
    float mCurrentAlpha = MINIMUM_ALPHA;

    @State
    String mCurrentTag = EntryFragment.TAG;

    private ActivityStartBinding mBinding;

    private StartViewModel mStartViewModel = new StartViewModel();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_start);
        mBinding.setStartViewModel(mStartViewModel);
        mBinding.bgBlackLayer.setAlpha(mCurrentAlpha);

        if (getSupportFragmentManager().findFragmentByTag(mCurrentTag) == null) {
            switch (mCurrentTag) {
                case EntryFragment.TAG:
                    setEntryFragment();
                    break;
                case FreeTrialFragment.TAG:
                    setFreeTrialFragment();
                    break;
            }
        }
    }

    private void setEntryFragment() {
        mCurrentAlpha = MINIMUM_ALPHA;
        mCurrentTag = EntryFragment.TAG;
        FragmentManager fm = getSupportFragmentManager();
        Fragment fragment = new EntryFragment();
        fm.beginTransaction().
                add(R.id.fragment_content, fragment, EntryFragment.TAG).commit();
    }

    private void setFreeTrialFragment() {
        mCurrentTag = FreeTrialFragment.TAG;
        Fragment fragment = new FreeTrialFragment();
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.setCustomAnimations(R.anim.anim_enter_right, R.anim.anim_exit_left, R.anim.anim_enter_left, R.anim.anim_exit_right);
        ft.replace(R.id.fragment_content, fragment, FreeTrialFragment.TAG);
        ft.addToBackStack(FreeTrialFragment.TAG);
        ft.commit();
        StartViewModel.setAnimation(mBinding.bgBlackLayer,true, MAXIMUM_ALPHA);
    }

    private void setForgotPasswordFragmet() {
    }

    private void setLoginFragment() {
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        StartViewModel.setAnimation(mBinding.bgBlackLayer,true, MINIMUM_ALPHA);
        mCurrentAlpha = MINIMUM_ALPHA;
    }

    @Override
    public void onEntryLoginButton() {
        setLoginFragment();
    }

    @Override
    public void onEntryFreeTrialButton() {
        setFreeTrialFragment();
    }
}

-The ViewModel only does the logic in doing the animation -Fragments have a listener to pass the events to the activity -The Binding helps to define the views

public class StartViewModel {

    public ObservableBoolean hasToAnimate = new ObservableBoolean(false);
    public float alpha;

    @BindingAdapter(value={"animation", "alpha"}, requireAll=false)
    public static void setAnimation(View view, boolean hasToAnimate, float alpha) {
        if (hasToAnimate) {
            view.animate().alpha(alpha);
        }
    }    
}

The question is, should all the logic reside in the view model including the fragment transactions, management of orientation changes and so on? Is there a better way to implement MVVM?

like image 597
george_mx Avatar asked Dec 22 '16 16:12

george_mx


People also ask

What is MVVM in Android with example?

MVVM stands for Model, View, ViewModel. Model: This holds the data of the application. It cannot directly talk to the View. Generally, it's recommended to expose the data to the ViewModel through Observables.

Why MVVM is best for Android?

In Android, MVC refers to the default pattern where an Activity acts as a controller and XML files are views. MVVM treats both Activity classes and XML files as views, and ViewModel classes are where you write your business logic. It completely separates an app's UI from its logic.

How does MVVM work?

The viewmodel of MVVM is a value converter, meaning the viewmodel is responsible for exposing (converting) the data objects from the model in such a way that objects are easily managed and presented. In this respect, the viewmodel is more model than view, and handles most if not all of the view's display logic.

What is ViewModel MVVM Android?

Model — View — ViewModel (MVVM) is the industry-recognized software architecture pattern that overcomes all drawbacks of MVP and MVC design patterns. MVVM suggests separating the data presentation logic(Views or UI) from the core business logic part of the application.


2 Answers

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.

like image 146
Yurii Tsap Avatar answered Oct 18 '22 19:10

Yurii Tsap


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

like image 38
Jemil Riahi Avatar answered Oct 18 '22 18:10

Jemil Riahi