На предыдущей лабораторной работе мы изучали библиотеку Room и компоненты, которые в нее входят (Database, DAO и Entity).
В данной лабораторной работе мы подключим такие компоненты как ViewModel, Repository и разберемся, как нам помогает LiveData для вывода обновленных данных.
1. Создание компоненты ViewModel
Создаем пакет viewmodel и создаем класс MainViewModel. Будем создавать класс ViewModel для каждого активити или фрагмента. Наши классы ViewModel будут наследоваться от класса androidx.lifecycle.AndroidViewModel.
Класс репозитория инкапсулирует логику управления различными источниками данных, с которыми может работать приложение. Пока что наше приложение работает с одним источником данных - с локальной базой данных, в следующих лекциях мы добавим еще один источник данных.
Создадим класс AppRepository в пакете database, класс будет реализовывать паттерн Singleton. В классе объявим поле со списком задач.
Перейдем в MainActivity и добавим обработчик выбора пункта меню.
MainActivity.java
publicclassMainActivityextendsAppCompatActivity {... @OverridepublicbooleanonOptionsItemSelected(MenuItem item) {int id =item.getItemId();if (id ==R.id.action_delete_all) {... } elseif (id ==R.id.action_sample_data) {addSampleData();returntrue; }return super.onOptionsItemSelected(item); }privatevoidaddSampleData() {}}
Метод addSampleData() будет вызывать соответствующий метод из ViewModel, который будет вызывать метод класса репозитория. Получится цепочка вызовов MainActivity --> MainViewModel --> AppRepository
Теперь давайте займемся методом addSampleData() в классе AppRepository.
Для начала мы должны получить создать и получить ссылку на объект AppDatabase, который мы создали в предыдущей лабораторной работе, он отвечает за взаимодействие с локальной базой данных. Для того чтобы создать объект AppDatabase, нам нужен объект контекста. Для этого мы немного изменим класс AppRepository, теперь для создания объекта AppRepository необходимо будет передать объект контекста.
Теперь реализуем метод addSampleData(). Помните, что действия с базой данных должны происходить в отдельном фоновом потоке.
В языке Java и в Android есть множество способов для запуска и работы в фоновом потоке, воспользуемся механизмом ExecutorService. Кроме удобства использования механизма Executor'ов, мы будем использовать один и тот же Executor с одним потоком для всех операций с базой данных. Это гарантирует нам то, что действия будут выполняться поочередно.
Если сейчас запустить приложение и попробовать добавить данные через меню, мы обнаружим, что приложение не реагирует на добавление новых записей, так как нам еще предстоит настроить вывод динамических данных.
4. Отображение динамических данных
Для отображения динамических данных нам понадобится еще один компонент из состава Architectural Components - компонент LiveData.
LiveData - хранилище данных, работающее по принципу паттерна Observer (наблюдатель). Это хранилище умеет делать две вещи:
в него можно поместить какой-либо объект;
на него можно подписаться и получать объекты, которые в него помещают.
То есть, с одной стороны кто-то помещает объект в хранилище, а с другой стороны кто-то подписывается и получает этот объект. В качестве аналогии можно привести, например, каналы в Telegram. Автор пишет пост и отправляет его в канал, а все подписчики получают этот пост.
Особенностью LiveData является то, что он умеет определять активен подписчик или нет, и отправлять данные будет только активным подписчикам. Предполагается, что подписчиками LiveData будут Activity и фрагменты.
Перейдем в класс MainViewModel и изменим тип поля taskList из List<Task> на LiveData<List<Task>>. Это изменение приведет к тому, что мы должны исправить соответствующий тип в классе AppRepository. Также в классе AppRepository создадим класс getAllTasks(), который будет получать список всех задач из базы данных.
Теперь реализуем метод getAllTasks(). В методе будет происходить обращение к DAO, в котором мы ранее создали метод getAll(). Так как нам теперь нужен объект LiveData, то отредактируем интерфейс TaskDAO.
TaskDAO.java
@DaopublicinterfaceTaskDAO {...// Изменим возвращаемый тип с List<Task> на LiveData<List<Task>> @Query("SELECT * FROM tasks ORDER BY date DESC")LiveData<List<Task>> getAll();...}
Обратите внимание, что мы не использовали Executor для получения данных из базы данных. Если метод DAO возвращает объект LiveData, то Room берет на себя всю работу по созданию фонового потока.
Не забудем немного изменить конструктор класса AppRepository - сначала мы должны получить объект AppDatabase, а уже потом выполнить методgetAllTasks().
AppRepository.java
publicclassAppRepository {...privateAppRepository(Context context) { db =AppDatabase.getInstance(context); taskList =getAllTasks(); }...}
Теперь наш список с задачами является "живым", но за изменениями этих "живых" данных никто не наблюдает. Вернемся в MainActivity и подпишемся на изменение данных.
Для начала уберем из класса метода updateAdapter(), вызовы этого метода, а также уберем код для получения объекта AppDatabase и весь код, который обращался к этому объекту.
После этого отредактируем метод initViewModel(). Создадим объект анонимного класса, реализующего интерфейс Observer (обратите внимание, что это интерфейс из пакета androidx.lifecycle.Observer).
Логика работы следующая - при старте приложения мы запрашиваем из базы список всех задач. При получении списка, содержимое LiveData обновляется и вызывается метод onChanged() (причем LiveData вызовет этот метод только в том случае, если слушатель события (активити или фрагмент) будут находиться в нужном состоянии).
В методе onChanged() мы реализуем следующую логику: если объект адаптера еще не создан (это бывает при создании Activity), то создается объект адаптера и добавляется в RecyclerView. Если объект адаптера существует, то просто обновляем содержимое RecyclerView с помощью метода notifyDataSetChanged().
Проверим работу приложения
5. Удаление всех задач
Теперь реализуем простую операцию удаления всех задач. Модифицируем метод обработки нажатия на пункт меню. Добавим локальный метод deleteAllTasks(), который будет вызывать соответствующий метод ViewModel, а тот будет вызывать метод репозитория
MainActivity.java
@OverridepublicbooleanonOptionsItemSelected(MenuItem item) {int id =item.getItemId();if (id ==R.id.action_delete_all) {deleteAllTasks();returntrue; } elseif (id ==R.id.action_sample_data) {addSampleData();returntrue; }return super.onOptionsItemSelected(item);}privatevoiddeleteAllTasks() {mViewModel.deleteAllTasks();}
Теперь реализуем функционал удаления отдельной задачи. Как мы помним, обработчик кнопки находится в адаптере, который вызывает метод deleteTask() в MainActivity.
Теперь реализуем редактирование существующей задачи и создание новой задачи. Для этого необходимо создать еще один класс ViewModel и модифицировать класс TaskActivity.
Объявляем класс TaskViewModel. Для поля задачи используем тип MutableLiveData, который подразумевает изменение содержимого LiveData.
Редактируем метод loadData() в классе TaskViewModel. Добавляем Executor и получаем данные из репозитория.
TaskViewModel.java
publicclassTaskViewModelextendsAndroidViewModel {publicMutableLiveData<Task> task =newMutableLiveData<>();privateAppRepository repository;privateExecutor executor =Executors.newSingleThreadExecutor();publicTaskViewModel(@NonNullApplication application) { super(application); repository =AppRepository.getInstance(application.getApplicationContext()); }publicvoidloadData(long id) {executor.execute(() -> {Task t =repository.getTaskById(id);task.postValue(t); // postValue вызывает срабатывание метода onChanged() }); }}
8. Сохранение существующей заметки
Модифицируем класс TaskActivity, добавляем в обработчик fab получение текста задачи и вызываем метод saveTask() объекта типа TaskViewModel.
TaskActivity.java
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);fab.setOnClickListener(view -> {// Сохранение задачи в базе данныхviewModel.saveTask(mTextView.getText().toString());finish();});
Редактируем метод saveTask() класса TaskViewModel. В методе проверяем, добавляем ли мы новую задачу или редактируем существующую. Пока не будем прописывать логику в случае создания новой задачи, а в случае редактирования существующей задачи, вставляем новый текст задачи и через репозиторий добавляем задачу в базу данных.
Теперь необходимо отредактировать метод добавления задачи в базу данных в классе репозитория. В отдельном потоке вызываем метод добавления новой задачи.
Добавляем логику создания новой задачи в метод saveTask() класса TaskViewModel. Создаем новый объект задачи, добавляем текст из поля ввода и текущую дату.
TaskViewModel.java
publicvoidsaveTask(String taskText) {Task t =task.getValue();if (t ==null) {if (TextUtils.isEmpty(taskText.trim()))return; t =newTask(taskText.trim(),new Date()); } else {t.setText(taskText); }repository.insertTask(t);}
Проверим работу приложения, добавим новую задачу, после чего отредактируем ее.
10. Смена ориентации экрана при редактировании заметки
Приложение почти готово, осталось исправить один баг, связанный с изменением ориентации экрана. Если пользователь в процессе создания или редактирования заметки изменит ориентацию экрана, то изменения текста заметки будет утеряно.
Это связано с тем, что при изменении ориентации экрана, Activity будет пересоздано и объект LiveData заново передаст объект Task в Activity, следовательно в поле ввода будут записаны старые значения.
В класс поля ввода EditText встроен механизм, который позволяет сохранить введенное значение при пересоздании Activity, но объект LiveData перезаписывает значение поля ввода и меняет его на старое. Чтобы это не происходило, мы должны распознать ситуацию пересоздания Activity и не менять значение EditText, если Activity было пересоздано. Для этого воспользуемся методом Activity onSavedInstanceState().
Создадим булево поле isEditing. В случае изменения ориентации экрана, перед уничтожением Activity будет вызыван метод onSaveInstanceState() и нам будет передан объект Bundle, куда мы записываем булеву переменную со значением true.
При старте Activity мы проверяем поле savedInstanceState. Если оно не равно null, то Activity было пересоздано, устанавливаем isEditing равным true. В Observer проверяем, если isEditing равно true, то мы не устанавливаем значение поля ввода.
TaskActivity.java
publicclassTaskActivityextendsAppCompatActivity {...privateboolean isEditing; @OverrideprotectedvoidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);setContentView(R.layout.activity_task);...// Если savedInstanceState !=null, то Activity было пересозданоif (savedInstanceState !=null) { isEditing =savedInstanceState.getBoolean("editing"); }... }privatevoidinitViewModel() { viewModel =ViewModelProviders.of(this).get(TaskViewModel.class);viewModel.task.observe(this, task -> {// Проверяем, было ли Activity пересозданоif (task !=null&&!isEditing)mTextView.setText(task.getText()); }); } @OverrideprotectedvoidonSaveInstanceState(Bundle outState) {// Сохраняем булеву переменнуюoutState.putBoolean("editing",true); super.onSaveInstanceState(outState); }...}
Проверяем работу приложения, убеждаемся, что данные в поле ввода теперь сохраняются