ZOFTINO.COM android and web dev tutorials

Model View ViewModel MVVM Android Example

Model View ViewModel (MVVM) is an architectural pattern applied in applications to separate user interface code from data and business logic. With the clear separation of these components, all components of an app can be unit-tested, components can be reused within the app or across the app, and enhancements to the app can be made without refactoring all the components.

The other architectural patters which separate UI code from data code are MVP and MVC. You can see Android MVP example to learn how MVP can be implemented in android apps.

MVVM

android mvvm pattern

In MVVM, model is a component which provides data and it may contain business logic or interact with business logic component.

View displays data on the screen. In android, the view is activity or fragment and their layouts.

View uses ViewModel to get data from model by binding to its properties and behavior. View and ViewModel communication using data binding framework or observer and observable framework like RxJava. View contains reference to ViewModel. ViewModel doesn’t contain reference to View.

Advantages of MVVM

The problem with MVP is that there exists tight coupling between presenter and view as presenter holds reference to view. Another disadvantage of using MVP is that, a presenter needs to be created for each activity or view.

The advantage of using MVVM pattern over MVP is that View and ViewModel are not tightly coupled as ViewModel contains no reference to View.

Unlike in MVP where view is passive and doesn’t know about model, view is active in MVVM, meaning view needs to know about model in order for it to bind to model properties exposed by ViewModel.

MVVM Example

You can learn how to implement MVVM in android by going through the following example. The MVVM example uses RxJava to implement MVVM pattern. The example displays list of categories in the list view. In response to the selected category, coupons for the selected category will be displayed in the second listview.

android mvvm example

Activity (View)

Activity displays coupon categories obtained by listening to observable which provides categories. When user clicks a category, item click event handler passes the selected category to ViewModel. Activity obtains the coupons for the selected category by listening to observable which provides coupons for the selected category.

import android.arch.lifecycle.ViewModelProvider;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import com.zoftino.couponsmvvm.R;
import com.zoftino.couponsmvvm.model.Coupon;
import com.zoftino.couponsmvvm.modelview.CouponViewModel;

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

import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;

public class CouponsActivity extends AppCompatActivity {
    @NonNull
    private CompositeDisposable compositeDisposable;
    @NonNull
    private ListView categoriesLst;
    @NonNull
    private ListView couponsLst;
    @NonNull
    private CouponViewModel viewModel;

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

        categoriesLst = findViewById(R.id.categories);
        couponsLst = findViewById(R.id.coupons);

        viewModel = ViewModelProvider.AndroidViewModelFactory.
                getInstance(getApplication()).create(CouponViewModel.class);

        setItemClickListener();
    }

    private void setItemClickListener(){
        categoriesLst.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                String cat = (String)adapterView.getItemAtPosition(i);
                //pass selected category to viewmodel
                viewModel.setSelectedCat(cat);
            }
        });
    }
        @Override
    protected void onResume() {
        super.onResume();
        bind();
    }

    @Override
    protected void onPause() {
        unBind();
        super.onPause();
    }

    private void bind() {
        compositeDisposable = new CompositeDisposable();
        //subscribe to categories observable
        //add the observable to disposable
        compositeDisposable.add(viewModel.getCategories()
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::setCategories));
        
        //subscribe to coupons observable
        //add the observable to disposable
        compositeDisposable.add(viewModel.getCouponsByCat()
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::setCoupons));
    }

    private void unBind() {
        compositeDisposable.clear();
    }

    private void setCategories(ArrayList<String> cats){
        //display received data from viewmodel 
        ArrayAdapter<String> itemsAdapter =
                new ArrayAdapter<String>(this,
                        android.R.layout.simple_list_item_1, cats);
        categoriesLst.setAdapter(itemsAdapter);
    }

    private void setCoupons(List<Coupon> coupons){
        //display received data from viewmodel 
        ArrayAdapter<Coupon> itemsAdapter =
                new ArrayAdapter<Coupon>(this,
                        android.R.layout.simple_list_item_1, coupons);
        couponsLst.setAdapter(itemsAdapter);
    }
}

ViewModel

import android.arch.lifecycle.ViewModel;

import com.zoftino.couponsmvvm.model.Coupon;
import com.zoftino.couponsmvvm.model.CouponModel;

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

import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.BehaviorSubject;

public class CouponViewModel extends ViewModel {
    //model object
    private CouponModel couponModel;
    private final BehaviorSubject<String> selectedCategory = BehaviorSubject.create();

    public CouponViewModel(){
        couponModel = new CouponModel();
    }

    //categories observable
    public Observable<ArrayList<String>> getCategories(){
        return couponModel.getCategories();
    }

    //coupons observable emits coupons when category is selected
    public Observable<List<Coupon>> getCouponsByCat(){
        return selectedCategory
                .observeOn(Schedulers.computation())
                .flatMap(couponModel::getCouponsByCat);
    }

    //pass selected category to model
    public void setSelectedCat(String cat){
        selectedCategory.onNext(cat);
    }
}

Model

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

import io.reactivex.Observable;

public class CouponModel{

    private Map<String, List<Coupon>> couponsByCat;

    public CouponModel(){
        couponsByCat = CouponData.getCoupons();
    }
    public Observable<ArrayList<String>> getCategories(){
       return Observable.just(new ArrayList<String>(couponsByCat.keySet()));
    }
    public Observable<List<Coupon>> getCouponsByCat(String cat){
        return Observable.just(couponsByCat.get(cat));
    }
}

CouponData

public class CouponData {
    private static Map<String, List<Coupon>> couponsByCat =
            new HashMap<String, List<Coupon>>();

    public static void loadCoupons(){
        addMobilesCoupons();
        addApplianceCoupons();
        addFashionCoupons();

        List<Coupon> coupons = new ArrayList<Coupon>();
        couponsByCat.put("sports", coupons);
        couponsByCat.put("travel", coupons);
        couponsByCat.put("furniture", coupons);
        couponsByCat.put("decor", coupons);
        couponsByCat.put("furnishing", coupons);
    }

    public static Map<String, List<Coupon>> getCoupons() {
        if(couponsByCat.isEmpty()){
            loadCoupons();
        }
       return couponsByCat;
    }

    public static void addMobilesCoupons(){
        List<Coupon> coupons = new ArrayList<Coupon>();

        Coupon coupon = new Coupon();
        coupon.setStore("Amazon");
        coupon.setOffer("Upto 20% off on Samsung mobiles");
        coupon.setExpiry("2018/12/30");

        coupons.add(coupon);

        coupon = new Coupon();
        coupon.setStore("BestBuy");
        coupon.setOffer("Upto 10% off on latest smart phonse");
        coupon.setExpiry("2018/12/04");

        coupons.add(coupon);

        couponsByCat.put("mobiles", coupons);
    }

    public static void addApplianceCoupons(){
        List<Coupon> coupons = new ArrayList<Coupon>();

        Coupon coupon = new Coupon();
        coupon.setStore("Amazon");
        coupon.setOffer("Upto 30% off on appliance");
        coupon.setExpiry("2018/12/30");

        coupons.add(coupon);

        coupon = new Coupon();
        coupon.setStore("Sears");
        coupon.setOffer("Flat $100 off on appliance");
        coupon.setExpiry("20181209");

        coupons.add(coupon);
        couponsByCat.put("appliance", coupons);
    }

    public static void addFashionCoupons(){
        List<Coupon> coupons = new ArrayList<Coupon>();

        Coupon coupon = new Coupon();
        coupon.setStore("Amazon");
        coupon.setOffer("Upto 80% off on fashion");
        coupon.setExpiry("2018/12/30");

        coupons.add(coupon);

        coupon = new Coupon();
        coupon.setStore("JCPenny");
        coupon.setOffer("Flat 40% off on branded fashion items");
        coupon.setExpiry("2018/12/20");

        coupons.add(coupon);

        coupon = new Coupon();
        coupon.setStore("Macys");
        coupon.setOffer("Minimum 30% off jeans");
        coupon.setExpiry("2018/12/24");

        coupons.add(coupon);

        coupon = new Coupon();
        coupon.setStore("Nordstrom");
        coupon.setOffer("Upto 30% off formal wear");
        coupon.setExpiry("2018/12/31");

        coupons.add(coupon);

        coupon = new Coupon();
        coupon.setStore("Khols");
        coupon.setOffer("Upto 80% off on kids wear");
        coupon.setExpiry("2018/12/26");

        coupons.add(coupon);

        couponsByCat.put("fashion", coupons);
    }
}