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:

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.