ViewModel
У цьому уроці розглянемо, як використовувати ViewModel. Як зберігати дані при повороті екрана. Як передати Context у ViewModel. Як передати свої дані в конструктор моделі за допомогою фабрики. Як передати дані між фрагментами. Що використовувати: ViewModel або onSavedInstanceState.
ViewModel - клас, що дає змогу Activity і фрагментам зберігати необхідні їм об'єкти живими під час повороту екрана.
Створюємо свій клас, що успадковує ViewModel
public class MyViewModel extends ViewModel {
}
Поки що залишимо його порожнім.
Щоб дістатися до нього в Activity, потрібен такий код:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
// ...
}
У метод ViewModelProviders.of передаємо Activity. Тим самим ми отримаємо доступ до провайдера, який зберігає всі ViewModel для цього Activity.
Методом get запитуємо у цього провайдера конкретну модель за ім'ям класу - MyViewModel. Якщо провайдер ще не створював такий об'єкт раніше, то він його створює і повертає нам. І поки Activity остаточно не буде закрито, під час усіх наступних викликів методу get ми отримуватимемо цей самий об'єкт MyViewModel.
Відповідно, під час поворотів екрана Activity буде перестворюватися, а об'єкт MyViewModel буде спокійно собі жити в провайдері. І Activity після перестворення зможе отримати цей об'єкт назад і продовжити роботу, ніби нічого не сталося.
Звідси випливає важливий висновок. Не зберігайте в ViewModel посилання на Activity, фрагменти, View тощо. Це може призвести до витоків пам'яті.
На зображенні час життя (він же scope) моделі це має такий вигляд:
Модель жива, поки Activity не закриється остаточно.
У методу get, який повертає нам модель із провайдера, є ще такий варіант виклику:
T get (String key, Class<T> modelClass)
Тобто ви можете створювати кілька моделей одного й того самого класу, але використовувати різні текстові ключі для їхнього зберігання в провайдері.
LiveData
LiveData дуже зручно використовувати з ViewModel. У минулих уроках я для роботи з LiveData використовував синглтон, але тепер ми перейдемо на ViewModel.
Розглянемо нескладний приклад асинхронного одноразового завантаження будь-яких даних:
public class MyViewModel extends ViewModel {
// ...
MutableLiveData<String> data;
public LiveData<String> getData() {
if (data == null) {
data = new MutableLiveData<>();
loadData();
}
return data;
}
private void loadData() {
dataRepository.loadData(new Callback<String>() {
@Override
public void onLoad(String s) {
data.postValue(s);
}
});
}
}
Основний метод тут - це getData. Коли Activity захоче отримати дані, воно викличе саме цей метод. Ми перевіряємо, чи створено вже MutableLiveData. Якщо ні, значить цей метод викликається вперше. У цьому випадку створюємо MutableLiveData і стартуємо асинхронний процес отримання даних методом loadData. Далі повертаємо LiveData.
У методі loadData відбувається асинхронне отримання даних з якогось репозиторію. Щойно дані будуть отримані (у методі onLoad), ми передаємо їх у MutableLiveData.
Метод loadData має бути асинхронним, тому що він викликається з методу getData, а getData, своєю чергою, викликається з Activity і все це відбувається в UI потоці. Якщо loadData почне завантажувати дані синхронно, то він заблокує UI потік.
Код в Activity має такий вигляд:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
LiveData<String> data = model.getData();
data.observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
// ...
}
});
}
Отримуємо від провайдера модель. Від моделі отримуємо LiveData, на який підписуємося і чекаємо на дані.
У цьому прикладі ViewModel потрібен, щоб зберегти процес отримання даних під час повороту екрана. А LiveData - для зручного асинхронного отримання даних.
Тобто це матиме такий вигляд:
Activityвикликає метод моделіgetData- модель створює
MutableLiveDataі стартує асинхронний процес отримання даних від репозиторію Activityпідписується на отриманий від моделіLiveDataі чекає на дані- відбувається поворот екрана
- на моделі цей поворот жодним чином не позначається, вона спокійно сидить у провайдері та чекає на відповідь від репозиторію
Activityперестворюється, отримує ту саму модель від провайдера, отримує той самийLiveDataвід моделі, підписується на нього і чекає на дані- репозиторій повертає дані, модель передає їх у
MutableLiveData Activityотримує дані дані відLiveData
Якщо репозиторій раптом надішле відповідь у той момент, коли Activity буде перестворюватися, то Activity отримає цю відповідь, щойно підпишеться на LiveData.
Якщо ваш репозиторій сам вміє повертати LiveData, то все значно спрощується. Ви просто віддаєте цей LiveData в Activity і воно підписується.
public class MyViewModel extends ViewModel {
// ...
LiveData<String> data;
public LiveData<String> getData() {
if (data == null) {
data = dataRepository.loadData();
}
return data;
}
}
Очищення ресурсів
Коли Activity остаточно закривається, провайдер видаляє ViewModel, попередньо викликавши його метод onCleared
public class MyViewModel extends ViewModel {
// ...
@Override
protected void onCleared() {
// clean up resources
}
}
У цьому методі ви зможете виконати всі необхідні операції зі звільнення ресурсів, закриття з'єднань/потоків тощо.
Context
Не варто передавати Activity у модель як Context. Це може призвести до витоків пам'яті.
Якщо вам у моделі знадобився об'єкт Context, то ви можете успадковувати не ViewModel, а AndroidViewModel.
public class MyViewModel extends AndroidViewModel {
public MyViewModel(@NonNull Application application) {
super(application);
}
public void doSomething() {
Context context = getApplication();
// ....
}
}
Під час створення цієї моделі провайдер передасть їй у конструктор клас Application, який є Context. Ви зможете до нього дістатися методом getApplication.
Код отримання цієї моделі в Activity залишиться тим самим.
Передача об'єктів у конструктор моделі
Буває необхідність передати моделі будь-які дані під час створення. Модель створюється провайдером і у нас є можливість втрутитися в цей процес. Для цього використовується фабрика. Ми вчимо цю фабрику створювати модель так, як нам потрібно. І провайдер скористається цією фабрикою, коли йому знадобиться створити об'єкт.
Розглянемо приклад. У нас є така модель
public class MyViewModel extends ViewModel {
private final long id;
public MyViewModel(long id) {
this.id = id;
}
// ...
}
Їй потрібен long при створенні.
Створюємо фабрику
public class ModelFactory extends ViewModelProvider.NewInstanceFactory {
private final long id;
public ModelFactory(long id) {
super();
this.id = id;
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass == MyViewModel.class) {
return (T) new MyViewModel(id);
}
return null;
}
}
Вона має успадковувати клас ViewModelProvider.NewInstanceFactory.
У конструктор передаємо long, який нам необхідно буде передати в модель.
У методі create фабрика отримає від провайдера на вхід клас моделі, яку необхідно створити. Перевіряємо, що це клас MyViewModel, самі створюємо модель і передаємо туди long.
В Activity код отримання моделі матиме такий вигляд:
long id = ...;
MyViewModel model = ViewModelProviders.of(this, new ModelFactory(id))
.get(MyViewModel.class);
Ми створюємо нову фабрику з потрібними нам даними і передаємо її в метод of. Під час виклику методу get провайдер використає фабрику для створення моделі, тобто виконається наш код створення моделі та передавання в неї даних.
Передача даних між фрагментами
ViewModel може бути використана для передачі даних між фрагментами, які знаходяться в одному Activity. У документації є чудовий приклад коду:
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
SharedViewModel - модель із двома методами: один дає змогу помістити дані в LiveData, інший - дає змогу отримати цей LiveData. Відповідно, якщо два фрагменти матимуть доступ до цієї моделі, то один зможе поміщати дані в його LiveData, а інший - підпишеться і отримуватиме ці дані. Таким чином два фрагменти будуть обмінюватися даними нічого не знаючи один про одного.
Щоб два фрагменти могли працювати з однією і тією ж моделлю, вони можуть використовувати спільне Activity. Код отримання моделі у фрагментах виглядає так:
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
Для обох фрагментів getActivity поверне один і той самий Activity. Метод ViewModelProviders.of поверне провайдера цього Activity. Далі методом get отримуємо модель.
Код фрагментів:
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// Update the UI.
});
}
}
Фрагмент MasterFragment поміщає дані в LiveData. А DetailFragment - підписується і отримує дані.
onSavedInstanceState
Чим ViewModel відрізняється від onSavedInstanceState. Для яких даних який із них краще використовувати. Здається, що якщо є ViewModel, який живий весь час, поки не закрито Activity, то можна забути про onSavedInstanceState. Але це не так.
Давайте як приклад розглянемо Activity, яке відображає список якихось даних і може виконувати пошук за ними. Користувач відкриває Activity і виконує пошук. Activity відображає результати цього пошуку. Користувач згортає додаток. Коли він його знову відкриє, він очікує, що там усе залишиться в цьому ж стані.
Але тут раптово системі не вистачає пам'яті і вона вбиває цей згорнутий додаток. Коли користувач знову запустить його, Activity нічого не знатиме про пошук, і просто покаже всі дані. У цьому випадку ViewModel нам ніяк не допоможе, тому що модель буде вбита разом із застосунком. А ось onSavedInstanceState буде виконано. У ньому ми зможемо зберегти пошуковий запит, і під час подальшого запуску отримати його з об'єкта savedInstanceState і виконати пошук. У результаті користувач побачить той самий екран, який був, коли додаток було згорнуто.
Отже.
ViewModel- тут зручно тримати всі дані, які потрібні вам для формування екрана. Вони житимуть під час поворотів екрана, але помруть, коли застосунок буде вбито системою.onSavedInstanceState- тут потрібно зберігати той мінімум даних, який знадобиться вам для відновлення стану екрана і даних у ViewModel після екстреного закриття Activity системою. Це може бути пошуковий запит, ID тощо.
Відповідно, коли ви дістаєте дані з savedInstanceState і пропонуєте їх моделі, це може бути у двох випадках:
- Був звичайний поворот екрана. У цьому разі ваша модель має зрозуміти, що їй ці дані не потрібні, тому що під час повороту екрана модель нічого не втратила. І вже точно модель не повинна заново робити запити в БД, на сервер тощо.
- Додаток було вбито, і тепер запущено заново. У цьому випадку модель бере дані з
savedInstanceStateі використовує їх, щоб відновити свої дані. Наприклад, бере ID і йде в БД за повними даними.
RxJava
На початку цього уроку ми розглянули приклад роботи ViewModel і LiveData. Виникає питання, чи можна замінити LiveData на Flowable?
У LiveData є одна велика перевага - він враховує стан Activity. Тобто він не буде слати дані, якщо Activity згорнуто. І він відпише від себе Activity, яке закривається.
А ось Flowable цього не вміє. Якщо в моделі є Flowable, і Activity підпишеться на нього, то цей Flowable триматиме Activity, поки воно саме явно не відпишеться (або поки Flowable не завершиться).
Давайте розглянемо приклад. ViewModel зазвичай працює з репозиторіями, які можуть бути синглтонами. У репозиторії є якийсь об'єкт для підписки (типу LiveData або Flowable). Репозиторій періодично оновлює в ньому дані. Модель бере цей об'єкт із репозиторію і віддає його в Activity, і Activity підписується на цей об'єкт. Об'єкт тепер зберігає посилання на Activity.
Таким чином вийшло, що репозиторій тримає посилання на Activity через об'єкт підписки. І якщо ми закриємо Activity, але не відпишемо його від об'єкта підписки, то виникне витік пам'яті, тому що репозиторій може жити весь час роботи програми. І весь цей час Activity буде висіти в пам'яті.
Давайте розглянемо, як це вирішується у випадку з LiveData або Flowable. Важливо розуміти, що відбуватиметься з підпискою під час закриття Activity. ViewModel будемо розглядати тільки як інструмент передачі об'єкта з репозиторію в Activity.
ViewModelготовий з репозиторію надати LiveData. І ми в Activiy хотіли б працювати з LiveData.
Ланцюжок посилань:
Repository -> LiveData -> Activity
Тут виходить повна ідилія. Activity бере з моделі LiveData, підписується на нього і все ок. Під час закриття Activity не буде ніяких витоків пам'яті та інших проблем з підпискою, тому що LiveData сам відпише Activity і тим самим розірве ланцюжок посилань.
- ViewModel готовий повернути нам Flowable, а ми в Activity хотіли б працювати з
LiveData.
У цьому випадку конвертуємо Flowable у LiveData всередині моделі і віддаємо LiveData в Activity.
Ланцюжок посилань:
Repository -> Flowable -> LiveData -> Activity
Activity знову буде підписано на LiveData. А це означає, що нам, як і в першому варіанті, не треба піклуватися про цю підписку. LiveData відпише від себе Activity, і сам відпишеться від Flowable. Ланцюжок посилань розірветься у двох місцях.
ViewModelготовий повернути намLiveData, а ми вActivityхотіли б працювати зFlowable.
У цьому випадку передаємо LiveData в Activity і перетворюємо його на Flowable.
Ланцюжок посилань:
Repository -> LiveData -> Flowable -> Activity
Activity буде підписано на Flowable. А Flowable буде підписаний на LiveData. При цьому підписка Flowable на LiveData працюватиме з урахуванням Activity LifeCycle. І коли Activity буде закрито, LiveData сам відпише від себе Flowable.
Ланцюжок посилань розірветься, але, в будь-якому разі, гарною практикою є відписка від Flowable вручну при закритті Activity.
ViewModelготовий повернути намFlowable, і ми вActivityхотіли б працювати зFlowable.
У цьому випадку Activity буде підписано на Flowable.
Ланцюжок посилань:
Repository -> Flowable -> Activity
При закритті Activity нам самим необхідно відписати Activity від Flowable.