ZOFTINO.COM android and web dev tutorials

Android Architecture Component ViewModel

ViewModel is part of architecture component library. ViewModel can be used to delegate the task of preparing and holding data for activities and fragments. ViewModel component separates view and model and it handles communication between view (activity or fragment), and model (data and business logic).

ViewModel is a lifecycle aware component and can exist as long as component it is associated with is not destroyed. ViewModel will not be destroyed when the owner component is destroyed for configuration change like device rotation. This saves network bandwidth and prevents delay in showing the screen as data fetching and computation is not performed because ViewModel can be reused.

What makes an instance of ViewModel available for an activity or fragment till they are destroyed lies in the way ViewModel is instantiated. ViewModel should not be instantiated using constructor, instead you need to get an instance of ViewModel in its owner component by using ViewModelProviders class as shown below.

       final YourViewModel viewModel = ViewModelProviders.of(this).get(YourViewModel.class);

ViewModel Dependencies

Porject build.gradle file change.

allprojects {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' }
    }
}

Module build.gradle file changes.

compile 'android.arch.lifecycle:runtime:1.0.0-alpha3'
compile 'android.arch.lifecycle:extensions:1.0.0-alpha3'

Steps to Use ViewModel

  • Create a model class that contains data for an activity or fragment.
  • Create view model class extending ViewModel. This class holds the model object defined above and it contains methods to handle UI events which apply business logic and fetch data and finally set model.
  • Get an instance of ViewModel in the activity or fragment it is intended to be used in, by passing the lifecycle of the activity or fragment to ViewModelProviders.
  • Use the view model instance to get data and update UI in response to user events.

ViewModel Example

Below example shows how to use ViewModel in android applications. The example fetches data from rest service using retrofit. ViewModel holds data returned by the service, which is displayed on screen when a button is clicked.

Service call is made asynchronously and the call immediately returns coupon object to ViewModel, later callback handler will set response data to it after receiving response from the service call.

Model Class

 package com.zoftino.viewmodel;

public class Coupon {
    private String store;
    private String coupon;
    private String couponCode;

    public String getStore() {
        return store;
    }

    public void setStore(String store) {
        this.store = store;
    }

    public String getCoupon() {
        return coupon;
    }

    public void setCoupon(String coupon) {
        this.coupon = coupon;
    }

    public String getCouponCode() {
        return couponCode;
    }

    public void setCouponCode(String couponCode) {
        this.couponCode = couponCode;
    }
}
 

Retrofit Service Client Interface

 package com.zoftino.viewmodel;

import retrofit2.Call;
import retrofit2.http.GET;

public interface CouponApi {

    @GET("topCoupon/")
    Call<Coupon>  getTopCoupon();
}
 

Repository Class

 package com.zoftino.viewmodel;

import android.util.Log;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class CouponRepository {

    public static final String BASE_URL = "http://www.zoftino.com/api/";
    private static Retrofit retrofit = null;

    public static Retrofit getRetrofitClient() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }

    public Coupon getTopCoupon() {
        final Coupon coupon = new Coupon();
        getRetrofitClient().create(CouponApi.class).getTopCoupon().enqueue(new Callback<Coupon>() {
            @Override
            public void onResponse(Call<Coupon> call, Response<Coupon> response) {
                Coupon cpn = response.body();

                coupon.setStore(cpn.getStore());
                coupon.setCoupon(cpn.getCoupon());
                coupon.setCouponCode(cpn.getCouponCode());
            }

            @Override
            public void onFailure(Call<Coupon> call, Throwable t) {
                Log.e("", "Error Getting TOP COUPON Data Retrofit");
            }
        });
        return coupon;
    }
}
 

ViewModel

 package com.zoftino.viewmodel;

import android.arch.lifecycle.ViewModel;
import android.util.Log;

public class CouponViewModel extends ViewModel{

    private Coupon coupon;
    private CouponRepository couponRepository = new CouponRepository();

    public Coupon getCoupon() {
        if(coupon == null){
            coupon = couponRepository.getTopCoupon();
        }
        return coupon;
    }

}
 

Activity

 package com.zoftino.viewmodel;

import android.arch.lifecycle.ViewModelProviders;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private CouponViewModel couponViewModel;
    private TextView tv;

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

        tv = (TextView) findViewById(R.id.coupon);

        //get ViewModel using ViewModelProviders and then tech data
        couponViewModel = ViewModelProviders.of(this).get(CouponViewModel.class);
        couponViewModel.getCoupon();
    }

    public void getTopCoupon(View view){
       String coupon =  couponViewModel.getCoupon().getStore()+" "+ couponViewModel.getCoupon().getCoupon()
                            +" "+ couponViewModel.getCoupon().getCouponCode();
        tv.setText(coupon);
    }
}
 

Layout

 <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.zoftino.viewmodel.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:layout_marginRight="8dp"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:id="@+id/coupon"></TextView>

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Get Top Coupon"
        android:layout_marginTop="24dp"
        app:layout_constraintTop_toBottomOf="@+id/coupon"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        android:onClick="getTopCoupon"></Button>

</android.support.constraint.ConstraintLayout>
 

ViewModel with LiveData

In the above example there is a problem, activity or fragment doesn’t know when the response is available from the asynchronous service call.

To fix this problem, we should use LiveData which is also part of architecture components library. LiveData is an observable. Observers of LiveData are notified of data changes. To learn how LiveData works, you may read my previous post Android LiveData examples.

Below are the changes to the above example to use LiveData with ViewModel.

Repository class change

 public LiveData<Coupon> getTopCouponLive() {
    final MutableLiveData<Coupon> coupon = new MutableLiveData<Coupon>();
    getRetrofitClient().create(CouponApi.class).getTopCoupon().enqueue(new Callback<Coupon>() {
        @Override
        public void onResponse(Call<Coupon> call, Response<Coupon> response) {
            Coupon cpn = response.body();
            coupon.setValue(cpn);
        }

        @Override
        public void onFailure(Call<Coupon> call, Throwable t) {
            Log.e("", "Error Getting TOP COUPON Data Retrofit");
        }
    });
    return coupon;
}
 

ViewModel

 public class CouponViewModel extends ViewModel{

    private LiveData<Coupon> liveCoupon;

    private CouponRepository couponRepository = new CouponRepository();

    //using livedata
    public LiveData<Coupon> getLiveCoupon() {
        if(liveCoupon == null){
            liveCoupon = couponRepository.getTopCouponLive();
        }
        return liveCoupon;
    }
}
 

Activity

 public class MainActivity extends LifecycleActivity {

    private CouponViewModel couponViewModel;
    private TextView tv;

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

        tv = (TextView) findViewById(R.id.coupon);

        //get ViewModel using ViewModelProviders and then tech data
        couponViewModel = ViewModelProviders.of(this).get(CouponViewModel.class);

        //livedata
        couponViewModel.getLiveCoupon().observe(this, coupon -> {
            tv.setText(""+coupon.getCoupon()+" "+coupon.getCouponCode());
        });
    }
}
 

ViewModel for Sharing Data

ViewModel can be used to share data between fragments of an activity. To share data between fragments, you need to use ViewModel object associated to the corresponding activity lifecycle. You can get the activity scoped ViewModel instance in both fragments for setting and getting data on it. Using observable in ViewModel like LiveData makes it easy to share data between fragments, as soon as producer (one fragment) sets the data on observable, consumer gets notification (second fragment).

model = ViewModelProviders.of(getActivity()).get(CustomViewModel.class);
model.setValueForFragment("some value for different frgament");

Preventing Memory Leaks

If you use observables in ViewModel, observables need to be disposed before ViewModel is destroyed, otherwise memory leaks can occur. ViewModel has one callback method onCleared that gets called just before ViewModel is destroyed and is the right place to clear objects which viewmodel contains. Below example shows how to dispose RxJava observables in ViewModle.


public class MyViewModel extends ViewModel {

    private CompositeDisposable compositeDisposable;

    public MyViewModel(CompositeDisposable disposable){
        compositeDisposable = disposable;
    }

    public void getMyInfoFromService(){
        compositeDisposable.add(io.reactivex.Observable.just(1)
                .subscribeOn(Schedulers.computation())
                .flatMap(i -> { return remoteRepo.getMyInfo();}).subscribeOn(Schedulers.io())
		.........

    }
    @Override
    public void onCleared(){
        //prevents memory leaks by disposing pending observable objects
        if (compositeDisposable != null && !compositeDisposable.isDisposed()) {
            compositeDisposable.clear();
        }
    }
}

Supplying Dependencies to ViewModel

Creating dependent objects in ViewModel itself makes it tightly coupled to dependent objects. To create a reusable and loosely coupled ViewModel, dependencies must be injected to it. You can inject dependencies to ViewModel either by implementing ViewModelProvider.Factory or using ViewModelProvider.NewInstanceFactory, and ViewModelProviders.DefaultFactory out of the box ViewModelProvider.Factory implementations.

ViewModelProvider.NewInstanceFactory implementation creates instance of view model using no argument constructor of target ViewModel. You can extend ViewModelProvider.NewInstanceFactory class and use setter method injection to inject dependencies to ViewModel as show below.

In the below view model factory, MyViewModel depends on MyRepository and the dependency is injected using setDependencies method of view model class.

    public class CustomVMFactory extends ViewModelProvider.NewInstanceFactory{
	 private MyRepository myRepository;

        public CustomVMFactory(@NonNull MyRepository myrep) {
            myRepository = myrep;
        }
        @Override
        public <T extends ViewModel> T create(Class<T> modelClass) {
            try {
		T t = super.create(modelClass);
		if (t instanceof MyViewModel) {
            		((MyViewModel) t).setDependencies(myRepository);
        	}
        	return t;                
            } catch (Exception e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } 
        }
    } 

Once you define view model factory class, you can instantiate it in activity class and use it to get ViewModel instance.

 customVMFactory = new CustomVMFactory(new MyRepository());        
couponViewModel = ViewModelProviders.of(this, customVMFactory).get(MyViewModel.class); 

You can create your own implementation of ViewModelProvider.Factory and use constructor injection to inject dependencies to ViewModel

 public class CouponViewModelFactory implements ViewModelProvider.Factory {

    LocalRepository localRepository;
 
    public CouponViewModelFactory(LocalRepository localRepo) {
	localRepository = localRepo;
    }
 
    @Override
    public <T extends="" viewmodel=""> T create(Class<T> modelClass) {
        if (modelClass.isAssignableFrom(CouponViewModel.class)) {
            return (T) new CouponViewModel(localRepository);
        }
        throw new IllegalArgumentException("Wrong ViewModel class");
    }
} 

If your view model depends on application context, you can use ViewModelProviders.DefaultFactory. But you need to make sure that your ViewModel has constructor which takes application context as argument.

Injecting ViewModel Dependencies Using Dagger

If you are using dagger or any other dependency library in your application, you can inject dependencies to view model using the same. I’ll show how to inject dependencies to ViewModel using Dagger2. If you are new to Dagger, you may read my earlier article that explains dependency injection with dagger and view example that uses dagger.

 @CouponScope
public class CouponViewModelFactory implements ViewModelProvider.Factory {

    @Inject
    LocalRepository localRepository;

    @Inject
    public CouponViewModelFactory() {
    }

    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        if (modelClass.isAssignableFrom(CouponViewModel.class)) {
            return (T) new CouponViewModel(localRepository);
        }
        throw new IllegalArgumentException("Wrong ViewModel class");
    }
}