MVVM with Android Architecture Components (LiveData + ViewModel)

Ramprasad
Ramprasad
Dec 8, 2017 · 4 min read

In this article, I will explain how I have implemented MVVM pattern with Android Architecture Components(LiveData,ViewModel) in my project to call a simple web service using Retrofit.

Overview:

MVVM pattern with LiveData & ViewModel

Setup Dependencies for Architecture components:

Add following dependencies in app.gradle file,

//Architecture components
implementation "android.arch.lifecycle:extensions:1.0.0"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0"

Final Output:

View:

In view part,we have only Activity and fragments with only UI code. Any business logic or web service calls should move to ViewModel and Repository.

Create a fragment called HomeFragment and add this fragment to HomeActivity(MainActivity).

ViewModel:

  • ViewModel prepares data for UI
  • ViewModel is a mediator between UI and data repository
  • When device orientation changes(or) Activity restarts ViewModel will not change.
  • ViewModel can be used to communicate betweeen fragments without callback interfaces

Create a class HomeViewModel extends AndroidViewModel,

public class HomeViewModel extends AndroidViewModel {

public HomeViewModel(@NonNull Application application) {
super(application);
}
}

Repository:

Repository is a data layer,it is responsible to fetch data from local storage or web service and send it to ViewModel

Create a class HomeRepository,

public class HomeRepository {
}

Now we setup all class needed for MVVM pattern and the app structure looks like below,

Connect View and ViewModel

Add following code in HomeFragment onCreate method,

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHomeViewModel = ViewModelProviders.of(getActivity()).get(HomeViewModel.class);
}

Now we have completed basic structure of MVVM with ViewModel and Repository. Next we need to communicate between UI and ViewModel, also ViewModel and Repository.Here we can use LiveData from Architecture components.

What is LiveData?

LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

Source:https://developer.android.com/topic/libraries/architecture/livedata.html

Repository Code to get data from server:

Add an interface for Retrofit web service,

public interface FoodFormulasAPIService {    @GET("topher/2017/May/59121517_baking/baking.json")
Call<Receipe[]> getReceipesListData();
}

HomeRepository.java

public class HomeRepository {

public HomeRepository() {
}

public MutableLiveData<NetworkResponse> getReceipesListData() {
final MutableLiveData<NetworkResponse> getReceipesListDataResponseMutableLiveData = new MutableLiveData<>();

OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder();
okHttpClient.connectTimeout(60, TimeUnit.SECONDS);
okHttpClient.readTimeout(60, TimeUnit.SECONDS);
okHttpClient.writeTimeout(60, TimeUnit.SECONDS);
okHttpClient.retryOnConnectionFailure(true);

FoodFormulasAPIService foodFormulasAPIService = new Retrofit.Builder()
.baseUrl("https://d17h27t6h515a5.cloudfront.net/")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient.build())
.build()
.create(FoodFormulasAPIService.class);

Call<Receipe[]> couponsListDataResponseCall = foodFormulasAPIService.getReceipesListData();

couponsListDataResponseCall.enqueue(new Callback<Receipe[]>() {
@Override
public void onResponse(Call<Receipe[]> call, Response<Receipe[]> response) {
if (response.isSuccessful()) {

Receipe[] receipes = response.body();

if(receipes != null){
getReceipesListDataResponseMutableLiveData.setValue(NetworkResponse.success(receipes));
}
} else {
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
getReceipesListDataResponseMutableLiveData.setValue(NetworkResponse.unAuthorised("Session Expired"));
} else {
String errorMessage = "Oops something went wrong!"; //Hard coded for code clarity
getReceipesListDataResponseMutableLiveData.setValue(NetworkResponse.error(null,errorMessage));
}
}
}

@Override
public void onFailure(Call<Receipe[]> call, Throwable t) {
String failureMessage = "Please check your network connection";
getReceipesListDataResponseMutableLiveData.setValue(NetworkResponse.error(null,failureMessage));
}
});
return getReceipesListDataResponseMutableLiveData;
}
}

ViewModel Code:

public class HomeViewModel extends AndroidViewModel {

private HomeRepository mHomeRepository;
SnackbarMessage mSnackbarTextLiveData = new SnackbarMessage();
MediatorLiveData<Receipe[]> receipiesListDataResponseMediatorLiveData = new MediatorLiveData<>();
private MutableLiveData<NetworkResponse> networkResponseMutableLiveData;


public HomeViewModel(@NonNull Application application) {
super(application);
this.mHomeRepository = new HomeRepository();
}

public SnackbarMessage getSnackbarMessage() {
return mSnackbarTextLiveData;
}

public LiveData<Receipe[]> getReceipesListData() {
networkResponseMutableLiveData = mHomeRepository.getReceipesListData();

LiveData<Receipe[]> receipiesListDataResponseLiveData = Transformations.switchMap(networkResponseMutableLiveData, new Function<NetworkResponse, LiveData<Receipe[]>>() {
@Override
public LiveData<Receipe[]> apply(NetworkResponse networkResponse) {
if (networkResponse == null) {
getSnackbarMessage().setValue("Oops something went wrong,Try again!");
return null;
}

if (networkResponse.status == NetworkResponse.SUCCESS) {
if(networkResponse.data != null) {
receipiesListDataResponseMediatorLiveData.setValue((Receipe[]) networkResponse.data);
}
}
else if(networkResponse.status == NetworkResponse.BAD_REQUEST){
if(networkResponse.data != null) {
getSnackbarMessage().setValue(networkResponse.errorMessage);
return null;
}
}
else if(networkResponse.status == NetworkResponse.UNAUTHORISED){
if(networkResponse.data != null){
getSnackbarMessage().setValue((String) networkResponse.data);
return null;
}
}
else if(networkResponse.status == NetworkResponse.FAILURE){
if(networkResponse.errorMessage != null) {
getSnackbarMessage().setValue(networkResponse.errorMessage);
return null;
}
}
else if(networkResponse.status == NetworkResponse.NO_NETWORK){
if(networkResponse.data != null) {
getSnackbarMessage().setValue((String) networkResponse.data);
return null;
}
}
else if(networkResponse.status == NetworkResponse.LOADING){

}
return receipiesListDataResponseMediatorLiveData;
}
});
return receipiesListDataResponseLiveData;
}
}

HomeFragment.java

Add RecyclerView on Fragment:

@BindView(R.id.receipes_recyclerview)
private RecyclerView receipesRecyclerView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHomeViewModel = ViewModelProviders.of(getActivity()).get(HomeViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_home, container, false);
setupReceipesRecyclerView(); return rootView;
}
@Override
public void onStart() {
super.onStart();
}private void setupReceipesRecyclerView() {
receipesRecyclerView.setHasFixedSize(true);
receipesRecyclerView.setLayoutManager(new GridLayoutManager(getActivity(),2));
mReceipesListAdapter = new ReceipesListAdapter(getActivity(), mReceipesArrayList, new ReceipesListAdapter.ReceipesListAdapterListener() {
@Override
public void onReceipeClicked(Receipe receipe) {
loadReceipesOnUI(receipe);
}
});
receipesRecyclerView.setAdapter(mReceipesListAdapter);
}

ReceipesListAdapter.java

public class ReceipesListAdapter extends RecyclerView.Adapter<ReceipesListAdapter.ReceipesListViewHolder>{    private final Context mContext;
private final ArrayList<Receipe> mReceipesArrayList;
private final ReceipesListAdapterListener mReceipesListAdapterListener;
public ReceipesListAdapter(Context context, ArrayList<Receipe> receipesArrayList, ReceipesListAdapterListener receipesListAdapterListener) {
this.mContext = context;
this.mReceipesArrayList = receipesArrayList;
this.mReceipesListAdapterListener = receipesListAdapterListener;
}
@Override
public ReceipesListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.receipes_list_item, parent, false);
return new ReceipesListViewHolder(view);
}
@Override
public void onBindViewHolder(ReceipesListViewHolder holder, int position) {
Receipe receipe = mReceipesArrayList.get(position);
String receipeThumbnailURL = receipe.getImage(); if(receipeThumbnailURL != null && !receipeThumbnailURL.isEmpty()){
Picasso.with(mContext).load(receipeThumbnailURL)
.centerCrop()
.resize(holder.receipeThumbnailImageView.getWidth(), holder.receipeThumbnailImageView.getHeight())
.into(holder.receipeThumbnailImageView);
}
}
@Override
public int getItemCount() {
return mReceipesArrayList.size();
}
public class ReceipesListViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { @BindView(R.id.receipe_thuumbnail_imageview)
ImageView receipeThumbnailImageView;
public ReceipesListViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this,itemView);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
Receipe receipe = mReceipesArrayList.get(getAdapterPosition());
mReceipesListAdapterListener.onReceipeClicked(receipe);
}
}
public interface ReceipesListAdapterListener{
void onReceipeClicked(Receipe receipe);
}
}

HomeFragment observes LiveData in ViewModel. The LiveData emits new data to fragment if fragment in active only.

private void loadReceipes() {

progressBar.setVisibility(View.VISIBLE);

final LiveData<Receipe[]> receipesListDataMediatorLiveData = mHomeViewModel.getReceipesListData();
Observer<Receipe[]> receipiesListDataResponseObserver = new Observer<Receipe[]>() {
@Override
public void onChanged(@Nullable Receipe[] receipes) {
if (receipes != null) {
updateReceipesListDataOnUI(receipes);
receipesListDataMediatorLiveData.removeObserver(this);
}
else {
progressBar.setVisibility(View.GONE);
}
}
};

receipesListDataMediatorLiveData.removeObservers(this);
receipesListDataMediatorLiveData.observe(HomeFragment.this, receipiesListDataResponseObserver);
}

private void updateReceipesListDataOnUI(Receipe[] receipesArray) {

if(receipesArray.length > 0){
mReceipesArrayList.clear();

List<Receipe> receipes = Arrays.asList(receipesArray);
mReceipesArrayList.addAll(receipes);
mReceipesListAdapter.notifyDataSetChanged();
}

progressBar.setVisibility(View.GONE);
}

Complete source code of this sample available in my Github

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade