Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Architecture Components(MVVM) - Ideal way to deal with remote and local data using repository pattern

I have browsed through a lot of sample code available for the new architecture components but I am still facing some issues in setting up my project.

I need to fetch data from my remote service and save it to the room database. I want my view to observe just a single live-data list. My AppRepository handles the RemoteRepository and LocalRepository. Remote Repository has a method fetchMovies() which receives a list of movies from the web-service. I want to save this list in the room database, at present my RemoteRepository class does this.

public void fetchMovieFromRemote(int page){
    movieService.fetchPopularMovies(ApiConstants.BASE_URL, page).enqueue(new Callback<List<Movie>>() {
        @Override
        public void onResponse(Call<List<Movie>> call, Response<List<Movie>> response) {
            int statusCode = response.code();
            if (statusCode == 200){
                mLocalRepository.insert(response.body());
            }
        }
        @Override
        public void onFailure(Call<List<Movie>> call, Throwable t) {
        }
    });
}

According to my understanding, ideally the Remote and Local repositories should be independent and this work should be done by the AppRepository class. One way to do this is to use callbacks but I want to use live-data to do this. Should the fetchMovieFromRemote(int page) method return a live-data for that, but in that case how to handle it in my viewmodel which at present has a live-data of the list of movies which is returned by room.

@Query("SELECT * from movie ORDER BY id ASC")
LiveData<List<Movie>> getAllWords();

I am new to MVVM, kindly guide me on what's the ideal approach for this architecture.

like image 712
Ishita Garg Avatar asked Aug 15 '18 19:08

Ishita Garg


1 Answers

I have adopted the pattern that Google uses in their example for a repository which gives you a single source of truth (your Room database).

It is discussed here: https://developer.android.com/jetpack/docs/guide

The key part to take notice of is the NetworkBoundResource class (Google sample: https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.kt). This example from Google is in Kotlin I did find a Java example.

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package iammert.com.androidarchitecture.data;

import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MediatorLiveData;
import android.os.AsyncTask;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public abstract class NetworkBoundResource<ResultType, RequestType> {
    private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

    @MainThread
    NetworkBoundResource() {
        result.setValue(Resource.loading(null));
        LiveData<ResultType> dbSource = loadFromDb();
        result.addSource(dbSource, data -> {
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource);
            } else {
                result.addSource(dbSource, newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        result.addSource(dbSource, newData -> result.setValue(Resource.loading(newData)));
        createCall().enqueue(new Callback<RequestType>() {
            @Override
            public void onResponse(Call<RequestType> call, Response<RequestType> response) {
                result.removeSource(dbSource);
                saveResultAndReInit(response.body());
            }

            @Override
            public void onFailure(Call<RequestType> call, Throwable t) {
                onFetchFailed();
                result.removeSource(dbSource);
                result.addSource(dbSource, newData -> result.setValue(Resource.error(t.getMessage(), newData)));
            }
        });
    }

    @MainThread
    private void saveResultAndReInit(RequestType response) {
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... voids) {
                saveCallResult(response);
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                result.addSource(loadFromDb(), newData -> result.setValue(Resource.success(newData)));
            }
        }.execute();
    }

    @WorkerThread
    protected abstract void saveCallResult(@NonNull RequestType item);

    @MainThread
    protected boolean shouldFetch(@Nullable ResultType data) {
        return true;
    }

    @NonNull
    @MainThread
    protected abstract LiveData<ResultType> loadFromDb();

    @NonNull
    @MainThread
    protected abstract Call<RequestType> createCall();

    @MainThread
    protected void onFetchFailed() {
    }

    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

This is a repo using that class:

package iammert.com.androidarchitecture.data;

import android.arch.lifecycle.LiveData;
import android.support.annotation.NonNull;

import java.util.List;

import javax.inject.Inject;

import iammert.com.androidarchitecture.data.local.dao.MovieDao;
import iammert.com.androidarchitecture.data.local.entity.MovieEntity;
import iammert.com.androidarchitecture.data.remote.MovieDBService;
import iammert.com.androidarchitecture.data.remote.model.MoviesResponse;
import retrofit2.Call;

/**
 * Created by mertsimsek on 19/05/2017.
 */

public class MovieRepository {

    private final MovieDao movieDao;
    private final MovieDBService movieDBService;

    @Inject
    public MovieRepository(MovieDao movieDao, MovieDBService movieDBService) {
        this.movieDao = movieDao;
        this.movieDBService = movieDBService;
    }

    public LiveData<Resource<List<MovieEntity>>> loadPopularMovies() {
        return new NetworkBoundResource<List<MovieEntity>, MoviesResponse>() {

            @Override
            protected void saveCallResult(@NonNull MoviesResponse item) {
                movieDao.saveMovies(item.getResults());
            }

            @NonNull
            @Override
            protected LiveData<List<MovieEntity>> loadFromDb() {
                return movieDao.loadMovies();
            }

            @NonNull
            @Override
            protected Call<MoviesResponse> createCall() {
                return movieDBService.loadMovies();
            }
        }.getAsLiveData();
    }

    public LiveData<MovieEntity> getMovie(int id){
        return movieDao.getMovie(id);
    }
}

I will try to explain it briefly, you have a method in your Repo, say, loadMovies() that returns a LiveData list of movies from your repository. With NetworkBoundResource, the Room database is checked first, then the API is queried and the results are then loaded into the database. Once the database is updated, the LiveData that you are observing is updated with the new results.

This diagram shows the logical flow behind this:

enter image description here

As you can see above, you are observing the disk for changes, and receive an update when it does change.

I recommend reading through that Jetpack guide I linked previously as they will explain it in more detail. I believe that is what you were looking for.

like image 120
James Pooley Avatar answered Sep 20 '22 21:09

James Pooley