Переклад українською - Арсеній Чеботарьов - Київ 2018

Настанови щодо архитектури застосування

Ця настанова для розробників, що вже знайомі з основами побудови застосувань, та тепер бажають знати кращі практики та рекомендовану архитектуру для побудови надійних застосувань промислової якості.

Загальні проблеми, з якими стикаються розробники застосувань


На відміну від своїх десктопних співродичів, які, в більшості випадків, мають одну точку входу в вигляді іконки запуску, та роблять як єдиний монолітний процес, застосування Android мають значно більш складну структуру. Типове застосування Android побудоване з декількох app компонент, включаючи активності, фрагменти, серсіви, провайдери вмісту та широкополосні отримувачі.

Більшість з ціх застосувань декларовані в app маніфесті, що використовується Android OS для вирішення, як інтегрувати ваше застосування в загальний користувацький досвід роботи з пристроєм. В той час, як зазначено вище, десктопне застосування традиційно робить як монолітний процес, правильно написане застосування Android має бути значно гнучкішим, по мірі того, як користувач прокладає власний шлях поміж застосувань на пристрої, постійно переключаючи потоки та завдання.

Наприклад, розглянемо, що трапиться, коли ви поділяєте фото в вашій улюбленій соціальній мережі. Застосування перемикає намір-інтент камери, з якого Android OS запускає застосування камери для обробки запиту. В цій точці, користувач залишає застосування соціальної мережі, але його досвід не має цього усвідомлювати. Застосування камери, в свою чергу, може запустити інші інтенти, як запустити обирач файлів, що може запустити інше застосування. З часом користувач повернеться до застосування соціальної мережі, та світлина буде передана. Також, користувач може бути перерваний телефонним викликом в любій точці цього процессу, щоб поширити фото після завершення телефонного виклику.

В Android ця поведінка переривання застосувань, так що ваше застосування мусить обрболяти ці потоки коректно. Майте на увазі, що мобільні пристрої обмежені в ресурсах, так що в любий час операційна система може потребувати вбити деякі застосування, щоб вивільнити простір для нових.

Смисл всього цього в тому, що компоненти вашого застосування можуть бути запущені індивідуально в довільному порядку, та можуть бути знищені в кожну мить, користувачем або системою. Оскільки компоненти застосування є ефемірними, та їх життєвий цикл (коли воні створюються та руйнуються) не контролюється вами, ви не повинні зберігати жодні дані застосування або стан в компонентах вашого застосування, та компоненти вашого застосування не повинні залежати один від одного.

Загальні архитектурні принципи


Якщо ви не можете використовувати компоненти застосування для збегірання даних та стану застосування, як ми маємо структурувати застосування?

Найбільш важлива річ, на якій ви маєте зфокусуватись, є розділення концепцій вашого застосування. Є загальною помилкою писати весь ваш код в Activity або Fragment. Любий код, що не обробляє UI, або не взаємодіє з операційною системою, не повинно бути в ціх класах. Утримання їх такими утисненими, як це можливе, дозволить вам уникнути багато проблем, пов'язаних з життєвим циклом. Не забувайте, що ви не володієте ціма класами, вони лише класи-клей, що лише втілюють контракт між OS, та вашим застосуванням. Android OS може зруйнувати їх в любий час, базуючись на взаємодії користувача, або інших факторах, як надостатня пам'ять. Краше мінімізувати вашу залежність від них, щоб провадити солідний користувацький досвід.

Другий важливий принцип в тому, що вам треба будувати ваш UI від моделі, бажано стійкої моделі. Стійкість є ідеальною з двох причин: ваші користувачі не втратяться дані, якщо ваша OS зруйнує ваше застосування, щоб вивільнити ресурси, та ваше застосування буде продовжувати роботу, навіть коли ваше мережеве з'єднання слабке або відсутнє. Моделі є компонентами, що відповідні за обробку даних для застосування. Вони незалежні від Views та компонент застосування, і, таким чином, вони ізольовані від проблем життєвого циклу ціх компонент. Утримуючи код UI простим, та вільним від логіки застосування, робить керування простішим. Базуючи ваше застосування на класах моделі з гарно визначеною відповідальністю по обробці даних, робить їх придатними для тестування, та ваше застосування узгодженим.


В цьому розділі ми подемонструємо, як структурувати застосування, використовуючи Архитектурні компоненти, через проходження випадку-прикладу.

Уявіть, що ми будуємо UI, що показує профіль користувача. Цей користувацький профіль буде підтягуватись з нашого власного бекенду через REST API.

Побудова користувацького інтерфейсу

UI буде складатись з фрагменту UserProfileFragment.java, та його відповідного файлу розташування user_profile_layout.xml.

Щоб розробити UI, наша модель даних має зберігати два елементи даних.

Ми будемо створювати UserProfileViewModel на базі класу ViewModel, що буде зберігати цю інформацію.

ViewModel провадить дані для окремого компоненту UI, такого, як фрагмент або активність, та обробляє комунікацію з бізнес-частиною обробки даних, тобто викликами інших компонент для завантаження даних або передачі модифікацій користувача.  ViewModel не знає щодо View, та не має впливу від змін конфігурації, таких, як перебудова активності через повертання пристрою.

Тепер ми маємо три файли.

Нижче наша початкова реалізація (файл розташування для простоти опущений):

public class UserProfileViewModel extends ViewModel {
   
private String userId;
   
private User user;

   
public void init(String userId) {
       
this.userId = userId;
   
}
   
public User getUser() {
       
return user;
   
}
}
public class UserProfileFragment extends Fragment {
   
private static final String UID_KEY = "uid";
   
private UserProfileViewModel viewModel;

   
@Override
   
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
       
super.onActivityCreated(savedInstanceState);
       
String userId = getArguments().getString(UID_KEY);
        viewModel
= ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel
.init(userId);
   
}

   
@Override
   
public View onCreateView(LayoutInflater inflater,
               
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
       
return inflater.inflate(R.layout.user_profile, container, false);
   
}
}

Тепер, коли ми маємо ці три модулі коду, як пов'язати їх разом? В кінці кінців, коли встановлюється поле користувача в ViewModel, нам треба спосіб поінформувати UI. Це те, де з'являється клас LiveData.

LiveData є прозорий контейнер даних. Він дозволяє компонентам вашого застосування досліджувати об'єкти LiveData щодо змін без створення явних та жорстких шляхів залежностей між ними. LiveData також поважає стан життєвого циклу  компонент вашого застосування (активностей, фрагментів, сервісів), та робить правильні речі для запобігання утічкам об'єкта, так що ваше застосування не споживає більше пам'яті.

Тепер ми заміщуємо поле User в UserProfileViewModel на LiveData<User>, так що фрагмент може бути поінформований, коли дані будуть оновлені. Велика річ щодо LiveData в тому, що він поінформований про життєвий цикл, та буде автоматично очищувати посилання, коли вони більше не потрібні.

public class UserProfileViewModel extends ViewModel {
   
...
   
private User user;
   
private LiveData<User> user;
   
public LiveData<User> getUser() {
       
return user;
   
}
}

Тепер ми модифікуємо UserProfileFragment для нагляду за даними та оновлення UI.

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
   
super.onActivityCreated(savedInstanceState);
    viewModel
.getUser().observe(this, user -> {
     
// оновлення UI
   
});
}

Кожного разу, коли дані користувача оновлюються, буде викликаний зворотній виклик onChanged, та UI буде оновлений.

Якщо ви знайомі з іншими бібліотеками, де використовуютсья бібліотеки для нагляду, ви можете помітити, що ми не перекрили метод фрагмента onStop() для припинення нагляду за даними. Це не потрібно для LiveData, оскільки він в курсі життєвого циклу, що означає, що він не викликатиме зворотній виклик, якщо фрагмент не знаходиться в активному стані (отрав onStart(), але не отримував onStop()). LiveData також буде автоматично видаляти наглядач-обсервер, коли фрагмент отримує onDestroy().

Ми також не робимо нічого особливого для обробки змін конфігурації (наприклад, коли користувач обертає екран). ViewModel автоматично відновлюється при зміні конфігурації, так що коли новий фрагмент приходить до життя, він буде отримувати той самий примірник ViewModel, та зворотній виклик буде викликаний безпосередньо з поточними даними. Це та причина, чому ViewModels не має прямо посилатись на View; вони можуть пережити життєвий цикл View. Дивіться життєвий цикл ViewModel.

Підтягування даних

Тепер ми поєднали ViewModel з фрагментом, але як ViewModel підтягує дані користувача? В цьому прикладі ми вважаємо, що наш бекенд провадить REST API. Ми будемо викоистовувати бібліотеку Retrofit для доступу до нашого бекенду, хоча ви вільні використовувати іншу бібліотеку, що прислуговуєтсья тій самій цілі.

Ось наш retrofit Webservice, що комунікує з нашим бекендомt:

public interface Webservice {
   
/**
     * @GET декларує запит HTTP GET
     * @Path("user") анотація на параметрі userId її як заміну для
     * замінника {user} в шляху @GET
     */

   
@GET("/users/{user}")
   
Call<User> getUser(@Path("user") String userId);
}

Природна реалізація ViewModel може напряму викликати Webservice, щоб підтягнути дані, та присвоїти їх назад об'єкту користувача. Навіть хоча це працює, вашому застосуванню буде складно керувати цім по мірі зростання. Це надає багато відповідальності класу ViewModel, що іде всупереч принципу розділення концепцій, який ми згадували раніше. Додатково, сфера впливу ViewModel прив'язаний до життєвого циклу Activity або Fragment, так що втрата даних, коли життєвй цикл завершиться, стане поганою новиною для користувача. Замість цього, наш ViewModel буде делегавувати цю роботу довому модулю, Repository.

Repository модулі відповідають за обробку операцій з даними. Вони провадять чистий API до загалу застосування. Вони знають, коли отримувати дані від яких викликів API, щоб оновлювати дані. Ви можете розглядати їх як медіатори між різними джерелами даних (модель стійкості, веб сервіс, кеш, тощо).

Клас UserRepository нижче використовує WebService to fetch the user data item.

public class UserRepository {
   
private Webservice webservice;
   
// ...
   
public LiveData<User> getUser(int userId) {
       
// This is not an optimal implementation, we'll fix it below
       
final MutableLiveData<User> data = new MutableLiveData<>();
        webservice
.getUser(userId).enqueue(new Callback<User>() {
           
@Override
           
public void onResponse(Call<User> call, Response<User> response) {
               
// error case is left out for brevity
                data
.setValue(response.body());
           
}
       
});
       
return data;
   
}
}

Навіть якщо модуль репозиторію виглядає непотрібним, він прислуговується важливому призначенні; він абстрагує джерела даних з решти застосування. Тепер ViewModel не знає, що дані отримуються від Webservice, що означає, що ми можемо змінити його на іншу реалізацію в разі потреби.

Керування залежностями між компоннетами:

Клас UserRepository вище потребує примірник Webservice для своєї роботи. Ми можемо просто створити його, але для цього треба знати залежності класу Webservice для його конструювання. Це може значно ускладнити код та привести до його дублікації (тобто, кожний клас, що потребує примірник Webservice, буде потребувати знання, як сконструювати його, разом з залежностями). Додатково UserRepository, можливо, не є єдиним класом, що потребує Webservice. Якщо кожний клас створюватиме новий WebService, це може дуже навантажувати ресурси.

Існують два шаблони, що ви можете використовувати для обходу цієї проблеми:

Ці шаблони дозволяють вам маштабувати ваш код, оскільки вони провадять прозорі шаблони для керування залежностями, без дублікації кода або доданої складності. Обоє з них також дозволяють заміну реалізацій для тестування; це одна з головних вигод з їх використання.

В цьому прикладі ми збираємось використовувати Dagger 2 для керумання залежностями.

Поєднання ViewModel та репозиторію

Тепер ми модифікуємо наш UserProfileViewModel для використання з репозиторієм.

public class UserProfileViewModel extends ViewModel {
   
private LiveData<User> user;
   
private UserRepository userRepo;

   
@Inject // параметр UserRepository провадиться Dagger 2
   
public UserProfileViewModel(UserRepository userRepo) {
       
this.userRepo = userRepo;
   
}

   
public void init(String userId) {
       
if (this.user != null) {
           
// ViewModel стврюється для кожного Fragment,
           
// так що ми знаємо, що userId не змінився
           
return;
       
}
        user
= userRepo.getUser(userId);
   
}

   
public LiveData<User> getUser() {
       
return this.user;
   
}
}

Кешування даних

Реалізація репозиторію вище гарна для абстрагування виклику до веб сервісу, але оскільки вона покладається тільки на одне джерело даних, вона не дуже функціональна.

Проблема з реалізацією UserRepository вище в тому, що після отримання даних вона ніде їх не зберігає. Якщо користувач полишає UserProfileFragment, та потім повертається до нього, дані будуть отримані повторно. Це погано з двох причин: це призводить до втрати дорогоцінного мережевого трафіку, та примушує користувача очікувати завершення нового запиту. Щоб владнати це, ми додамо нове джерело даних до нашого UserRepository, що буде кешувати об'єкти User в пам'яті.

@Singleton  // інформує Dagger, що цей клас треба конструювати тільки один раз
public class UserRepository {
   
private Webservice webservice;
   
// простий кеш в пам'яті, деталі випущені для скорочення
   
private UserCache userCache;
   
public LiveData<User> getUser(String userId) {
       
LiveData<User> cached = userCache.get(userId);
       
if (cached != null) {
           
return cached;
       
}

       
final MutableLiveData<User> data = new MutableLiveData<>();
        userCache
.put(userId, data);
       
// це все ще не оптімально, але краще ніж раніше.
       
// повна реалізація має також обробляти випадки помилок.
        webservice
.getUser(userId).enqueue(new Callback<User>() {
           
@Override
           
public void onResponse(Call<User> call, Response<User> response) {
                data
.setValue(response.body());
           
}
       
});
       
return data;
   
}
}

Зберігання даних

В нашій поточній реалізації, якщо користувач повертає екран, або виходить та повертається до застосування, існуючий UI буде видимий безпосередньо, оскільки репозиторій отримуж дані від кешу в пам'яті. Але ще відбуватиметься, якщо користувач вийде із застосування, та повернетьтся через годину, після того, як Android OS вже вб'є процес?

В поточній реалізації нам буде треба знову підтягнути дані з мережі. Це не тільки поганий досвід для користувача, але також розтринькує мобільні ресурси для отримання тих самих даних. Ви можете просто полагодити це, через кешування веб запитів, але це створює нові проблеми. Що буде, коли ті самі дані користувача трапляються в іншому типі запитів (наприклад, при отриманні списка друзів)? Тоді, можливо, ваше застосування буде показувати неузгоджені дані, що, в кращому випадку, трохи спантеличить користувача. Наприклад, ті самі дані користувача можуть показуватись інакше, оскільки запит списку друзів, та запит окремого користувача відбувались в різний час. Ваше застосування має поєднати їх, щоб уникнути неузгодженості.

Відповідний спосіб для обробки цього випадку полягає в використанні стійкої моделі. Ось де на допомогу приходить бібліотека стійкості Room.

Room є бібліотекою мепінгу, що провадить локальне зберігання даних з мінімумом загального коду. Під час компіляції вона перевіряє кожний запит відносно схеми, так що поламані запити SQL призведуть до помилок часу компіляції, замість збоїв часу виконання. Room абстрагується від деяких деталей реалізації роботи з сирими SQL таблицями та запитами. Він також дозволяє нагляд за змінами вданих бази даних (включаючи колекції та запити поєднання), показуючи такі зміни через об'єкти LiveData. На додаток, він явно визначає потокові обмеження, що націлені на загальні проблеми, такі, як доступ до сховища в головному потоці.

Щоб використати Room, нам треба визначити нашу власну схему. Перше, анотуйте клас User за допомогою @Entity, щоб відмітити його як таблицю в вашій базі даних.

@Entity
class User {
 
@PrimaryKey
 
private int id;
 
private String name;
 
private String lastName;
 
// getters and setters for fields
}

Потім створіть клас бази даних, через розширення RoomDatabase для вашого застосування:

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}

Зауважте, що MyDatabase є абстрактним. Room автоматично провадить його реалізацію. Дивіться докуентацію Room щодо деталей.

Тепер вам треба мати спосіб вставити користувацькі дані в базу даних. Для цього ми створимо об'єкт доступу до даних (data access object, DAO).

@Dao
public interface UserDao {
   
@Insert(onConflict = REPLACE)
   
void save(User user);
   
@Query("SELECT * FROM user WHERE id = :userId")
   
LiveData<User> load(String userId);
}

Потім посилаємось на DAO з класа бази даних.

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
   
public abstract UserDao userDao();
}

Зауважте, що метод load повертає LiveData<User>. Room знає, коли база даних модифікована, та буде автоматично повідомляти всі активні обсервери при зміні даних. Оскільки він використовує LiveData, це буде ефективним, оскільки він буде оновлювати дані тільки якщо буде щонайменше один активний спостерігач.

Тепер ми можемо модифікувати наш UserRepository, щоб вбудувати джерело даних Room.

@Singleton
public class UserRepository {
   
private final Webservice webservice;
   
private final UserDao userDao;
   
private final Executor executor;

   
@Inject
   
public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
       
this.webservice = webservice;
       
this.userDao = userDao;
       
this.executor = executor;
   
}

   
public LiveData<User> getUser(String userId) {
        refreshUser
(userId);
       
// повертаємо LiveData напряму з бази даних.
       
return userDao.load(userId);
   
}

   
private void refreshUser(final String userId) {
        executor
.execute(() -> {
           
// виконуємо в фоновому потоці
           
// перевіряємо, чи користувач отримував недавно
           
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
           
if (!userExists) {
               
// оновлюємо дані
               
Response response = webservice.getUser(userId).execute();
               
// TODO перевірити на помилки, тощо
               
// Оновлення бази даних. LiveData буде автоматично оновлюватись
               
// так що нам не треба нічого робити, окрім оновлення бази даних
                userDao
.save(response.body());
           
}
       
});
   
}
}

Зауважте, що навіть якщо ми змінили, звідки потрабляють дані в UserRepository, нам не треба змінювати наш  UserProfileViewModel або UserProfileFragment. Ця гнучкість провадиться рівнем абстракції. Це також добре для тестування, бо ви можете провадити фейковий UserRepository для тестування вашого UserProfileViewModel.

Тепер наш код завершений. Якщо користувач повертається до того самого UI через декілька днів, вони будуть безпосередньо бачити інформацію користувача, оскільки ми зробили її стійкою. Більше того, наш репозитарій буде оновлювати дані в фоні, якщо дані застарілі. Звичайно, в залежності від вашого випадку, ви можете схилитись не показувати збережені дані, якщо вони застарілі.

В деяких випадках, таких, як натиснути-для-оновлення, важливо для  UI показувати користувачу, якщо нараозі відбувається мережева операція. Є гарною практикою відділяти операції UI від дійсних даних, оскільки вони можуть бути оновлені за багатьох причин (наприклад, якщо ми підтягуємо список друзів, тай самий користувач може бути підтягнутий знову, та спрацює оновлення LiveData<User>). З перспективи UI факт, що наразі є активний запит, є тільки іншою точкою даних, подібно до любого іншого фрагменту даних (як об'єкт User).

Існують дві загальні рішення для цього випадку:

Єдине джерело правди

Є загальним для різних ендпоінітів REST API повертати ті самі дані. Гаприклад, якщо наш бекенд має інший ендпоінт, що повертає список друзів, той самий об'єкт користувача може поступити від різних ендпоінтів API, можливо з різними подробицями. Якщо  UserRepository буде повертати відповідь від запиту Webservice як є, наші UI можуть потенційно показувати різні дані, оскільки дані можуть змінитись на стороні сервера між ціма запитами. Ось чому в реалізації UserRepository зворотній виклик веб сервісу тільки зберігає дані в базі даних. Потім зміни в базі даних спрацьовують тригер зворотнього виклику на активних об'єктах LiveData objects.

В Цій моделі база даних служить як єдине джерело правди, та інші частини застосування отримують доступ через репозиторій. Не важливо, чи ви використовуєте дисковий кеш, ми рекомендуємо, щоб ваш репозиторій виділяв джерело даних як єдене джерело правди до залишку застосування.

Testing

We've mentioned that one of the benefits of separation is testability. Letssee how we can test each code module.

The final architecture

The following diagram shows all the modules in our recommended architectureand how they interact with one another:

Guiding principles


Programming is a creative field, and building Android apps is not anexception. There are many ways to solve a problem, be it communicating data between multiple activities or fragments, retrieving remote data andpersisting it locally for offline mode, or any number of other common scenarios that non-trivial apps encounter.

While the following recommendations are not mandatory, it has been ourexperience that following them will make your code base more robust, testable and maintainable in the long run.

Addendum: exposing network status


In the recommended app architecture section above, we intentionally omitted network error and loading states to keep thesamples simple. In this section, we demonstrate a way to expose network status using a Resource class to encapsulate both the data and its state.

Below is a sample implementation:

//a generic class that describes a data with a status
public class Resource<T> {
   
@NonNull public final Status status;
   
@Nullable public final T data;
   
@Nullable public final String message;
   
private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
       
this.status = status;
       
this.data = data;
       
this.message = message;
   
}

   
public static <T> Resource<T> success(@NonNull T data) {
       
return new Resource<>(SUCCESS, data, null);
   
}

   
public static <T> Resource<T> error(String msg, @Nullable T data) {
       
return new Resource<>(ERROR, data, msg);
   
}

   
public static <T> Resource<T> loading(@Nullable T data) {
       
return new Resource<>(LOADING, data, null);
   
}
}

Because loading data from network while showing it from the disk is a common use case, we are going to create a helper class NetworkBoundResource that can be reused in multiple places. Below is the decision tree forNetworkBoundResource:

It starts by observing database for the resource. When the entry is loaded from the database for the first time, NetworkBoundResource checks whether the result is good enough to be dispatched and/or it should be fetched fromnetwork. Note that both of these can happen at the same time since you probably want to show the cached data while updating it from the network.

If the network call completes successfully, it saves the response into thedatabase and re-initializes the stream. If network request fails, we dispatch a failure directly.

Below is the public API provided by NetworkBoundResource class for its children:

// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource<ResultType, RequestType> {
   
// Called to save the result of the API response into the database
   
@WorkerThread
   
protected abstract void saveCallResult(@NonNull RequestType item);

   
// Called with the data in the database to decide whether it should be
   
// fetched from the network.
   
@MainThread
   
protected abstract boolean shouldFetch(@Nullable ResultType data);

   
// Called to get the cached data from the database
   
@NonNull @MainThread
   
protected abstract LiveData<ResultType> loadFromDb();

   
// Called to create the API call.
   
@NonNull @MainThread
   
protected abstract LiveData<ApiResponse<RequestType>> createCall();

   
// Called when the fetch fails. The child class may want to reset components
   
// like rate limiter.
   
@MainThread
   
protected void onFetchFailed() {
   
}

   
// returns a LiveData that represents the resource, implemented
   
// in the base class.
   
public final LiveData<Resource<ResultType>> getAsLiveData();
}

Notice that the class above defines two type parameters (ResultType, RequestType) since the data type returned from the API may not match the data type used locally.

Also notice that the code above uses ApiResponse for network request. ApiResponse is a simple wrapper around Retrofit2.Call class to convert its response into a LiveData.

Below is the rest of the implementation for the NetworkBoundResource class:

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) {
       
LiveData<ApiResponse<RequestType>> apiResponse = createCall();
       
// we re-attach dbSource as a new source,
       
// it will dispatch its latest value quickly
        result
.addSource(dbSource,
                newData
-> result.setValue(Resource.loading(newData)));
        result
.addSource(apiResponse, response -> {
            result
.removeSource(apiResponse);
            result
.removeSource(dbSource);
           
//noinspection ConstantConditions
           
if (response.isSuccessful()) {
                saveResultAndReInit
(response);
           
} else {
                onFetchFailed
();
                result
.addSource(dbSource,
                        newData
-> result.setValue(
                               
Resource.error(response.errorMessage, newData)));
           
}
       
});
   
}

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

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

           
@Override
           
protected void onPostExecute(Void aVoid) {
               
// we specially request a new live data,
               
// otherwise we will get immediately last cached value,
               
// which may not be updated with latest results received from network.
                result
.addSource(loadFromDb(),
                        newData
-> result.setValue(Resource.success(newData)));
           
}
       
}.execute();
   
}

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

Now, we can use use NetworkBoundResource to write our disk and network bound User implementation in the repository.

class UserRepository {
   
Webservice webservice;
   
UserDao userDao;

   
public LiveData<Resource<User>> loadUser(final String userId) {
       
return new NetworkBoundResource<User,User>() {
           
@Override
           
protected void saveCallResult(@NonNull User item) {
                userDao
.insert(item);
           
}

           
@Override
           
protected boolean shouldFetch(@Nullable User data) {
               
return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
           
}

           
@NonNull @Override
           
protected LiveData<User> loadFromDb() {
               
return userDao.load(userId);
           
}

           
@NonNull @Override
           
protected LiveData<ApiResponse<User>> createCall() {
               
return webservice.getUser(userId);
           
}
       
}.getAsLiveData();
   
}
}