Основы Android Kotlin 06.3: использование LiveData для управления состояниями кнопок

Эта практическая работа входит в курс «Основы Android Kotlin». Вы получите максимальную пользу от этого курса, если будете выполнять практические работы последовательно. Все практические работы курса перечислены на целевой странице практической работы «Основы Android Kotlin» .

Введение

В этой практической работе мы рассмотрим совместное использование ViewModel и фрагментов для реализации навигации. Помните, что цель — реализовать логику навигации во ViewModel , а пути — во фрагментах и файле навигации. Для достижения этой цели используются модели представлений, фрагменты, LiveData и наблюдатели.

В заключение практической работы демонстрируется умный способ отслеживания состояний кнопок с минимальным количеством кода, чтобы каждая кнопка была включена и нажималась только тогда, когда пользователю имеет смысл нажать эту кнопку.

Что вам уже следует знать

Вам должно быть знакомо:

  • Создание базового пользовательского интерфейса (UI) с использованием активности, фрагментов и представлений.
  • Навигация между фрагментами и использование safeArgs для передачи данных между фрагментами.
  • Просмотр моделей, просмотр фабрик моделей, преобразований, а также LiveData и их наблюдателей.
  • Как создать базу данных Room , создать объект доступа к данным (DAO) и определить сущности.
  • Как использовать сопрограммы для взаимодействия с базой данных и других длительных задач.

Чему вы научитесь

  • Как обновить существующую запись о качестве сна в базе данных.
  • Как использовать LiveData для отслеживания состояний кнопок.
  • Как отобразить снэк-бар в ответ на событие.

Что ты будешь делать?

  • Расширьте возможности приложения TrackMySleepQuality для сбора рейтинга качества, добавления рейтинга в базу данных и отображения результата.
  • Используйте LiveData для отображения снэк-бара.
  • Используйте LiveData для включения и отключения кнопок.

В этой лабораторной работе вы создадите запись качества сна и окончательный пользовательский интерфейс приложения TrackMySleepQuality.

Приложение имеет два экрана, представленных фрагментами, как показано на рисунке ниже.

На первом экране, показанном слева, есть кнопки для запуска и остановки отслеживания. На экране отображаются все данные о сне пользователя. Кнопка «Очистить» безвозвратно удаляет все данные, собранные приложением для пользователя.

Второй экран, показанный справа, предназначен для выбора оценки качества сна. В приложении оценка представлена в числовом виде. В целях разработки приложение отображает как значки лиц, так и их числовые эквиваленты.

Поток действий пользователя выглядит следующим образом:

  • Пользователь открывает приложение и видит экран отслеживания сна.
  • Пользователь нажимает кнопку «Старт» . Время начала фиксируется и отображается на экране. Кнопка «Старт» отключается, а кнопка «Стоп» активируется.
  • Пользователь нажимает кнопку «Стоп» . Записывается время окончания и открывается экран качества сна.
  • Пользователь выбирает значок качества сна. Экран закрывается, и на экране отслеживания отображаются время окончания сна и качество сна. Кнопка «Стоп» отключается, а кнопка «Старт» активируется. Приложение готово к следующей ночи.
  • Кнопка «Очистить» активна, когда в базе данных есть данные. При нажатии кнопки «Очистить » все данные пользователя удаляются без возможности восстановления — сообщение «Вы уверены?» не появляется.

Это приложение использует упрощённую архитектуру, как показано ниже в контексте полной архитектуры. Приложение использует только следующие компоненты:

  • Контроллер пользовательского интерфейса
  • Просмотреть модель и LiveData
  • База данных A Room

В этой лабораторной работе предполагается, что вы знаете, как реализовать навигацию с помощью фрагментов и навигационного файла. Для экономии времени мы предоставляем значительную часть этого кода.

Шаг 1: Проверьте код

  1. Чтобы начать, продолжите работу с собственным кодом из конца последней лабораторной работы или загрузите стартовый код .
  2. В стартовом коде проверьте SleepQualityFragment . Этот класс расширяет макет, получает приложение и возвращает binding.root .
  3. Откройте файл navigation.xml в редакторе дизайна. Вы увидите навигационный путь от SleepTrackerFragment к SleepQualityFragment и обратно от SleepQualityFragment к SleepTrackerFragment .



  4. Проверьте код navigation.xml . В частности, найдите <argument> с именем sleepNightKey .

    Когда пользователь переходит от SleepTrackerFragment к SleepQualityFragment, приложение передает sleepNightKey в SleepQualityFragment для ночи, которую необходимо обновить.

Шаг 2: Добавьте навигацию для отслеживания качества сна

Граф навигации уже включает пути от SleepTrackerFragment к SleepQualityFragment и обратно. Однако обработчики щелчков, реализующие навигацию от одного фрагмента к другому, пока не написаны. Этот код нужно добавить сейчас в ViewModel .

В обработчике щелчков вы устанавливаете переменную LiveData , которая изменяется, когда приложение должно перейти к другому пункту назначения. Фрагмент отслеживает эту LiveData . При изменении данных фрагмент переходит к пункту назначения и сообщает модели представления о завершении, что сбрасывает переменную состояния.

  1. Откройте SleepTrackerViewModel . Вам нужно добавить навигацию, чтобы при нажатии пользователем кнопки «Стоп » приложение переходило к фрагменту SleepQualityFragment для сбора оценки качества.
  2. В SleepTrackerViewModel создайте объект LiveData , который будет изменяться, когда приложение должно переходить к фрагменту SleepQualityFragment . Используйте инкапсуляцию, чтобы предоставить ViewModel только доступную версию LiveData .

    Вы можете поместить этот код в любое место на верхнем уровне тела класса.
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()

val navigateToSleepQuality: LiveData<SleepNight>
   get() = _navigateToSleepQuality
  1. Добавьте функцию doneNavigating() , которая сбрасывает переменную, запускающую навигацию.
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. В обработчике щелчков кнопки «Стоп» onStopTracking() активируйте переход к фрагменту SleepQualityFragment . Установите переменную _ navigateToSleepQuality в конце функции, как последнюю в блоке launch{} . Обратите внимание, что эта переменная имеет значение night . Если эта переменная имеет значение, приложение переходит к фрагменту SleepQualityFragment , передавая ему значение night.
_navigateToSleepQuality.value = oldNight
  1. Фрагмент SleepTrackerFragment должен наблюдать за _ navigateToSleepQuality , чтобы приложение знало, когда осуществлять навигацию. В onCreateView() объекта SleepTrackerFragment добавьте наблюдателя для navigateToSleepQuality() . Обратите внимание, что импорт для этого неоднозначен, и вам необходимо импортировать androidx.lifecycle.Observer .
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})

  1. Внутри блока observer выполните навигацию и передайте идентификатор текущей ночи, а затем вызовите doneNavigating() . Если ваш импорт неоднозначен, импортируйте androidx.navigation.fragment.findNavController .
night ->
night?.let {
   this.findNavController().navigate(
           SleepTrackerFragmentDirections
                   .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
   sleepTrackerViewModel.doneNavigating()
}
  1. Создайте и запустите приложение. Нажмите «Пуск» , затем нажмите «Стоп» , чтобы открыть экран SleepQualityFragment . Чтобы вернуться назад, используйте системную кнопку «Назад».

В этой задаче вы регистрируете качество сна и возвращаетесь к фрагменту трекера сна. Дисплей должен автоматически обновляться, чтобы отображать пользователю обновлённое значение. Вам необходимо создать ViewModel и ViewModelFactory , а также обновить SleepQualityFragment .

Шаг 1: Создайте ViewModel и ViewModelFactory

  1. В пакете sleepquality создайте или откройте SleepQualityViewModel.kt.
  2. Создайте класс SleepQualityViewModel , принимающий в качестве аргументов sleepNightKey и базу данных. Как и для SleepTrackerViewModel , необходимо передать database из фабрики. Также необходимо передать sleepNightKey из навигации.
class SleepQualityViewModel(
       private val sleepNightKey: Long = 0L,
       val database: SleepDatabaseDao) : ViewModel() {
}
  1. Внутри класса SleepQualityViewModel определите Job и uiScope и переопределите onCleared() .
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. To navigate back to the SleepTrackerFragment using the same pattern as above, declare _navigateToSleepTracker . Implement navigateToSleepTracker and doneNavigating() .
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()

val navigateToSleepTracker: LiveData<Boolean?>
   get() = _navigateToSleepTracker

fun doneNavigating() {
   _navigateToSleepTracker.value = null
}
  1. Создайте обработчик одного щелчка onSetSleepQuality() для всех используемых изображений качества сна.

    Используйте тот же шаблон сопрограмм, что и в предыдущей лабораторной работе:
  • Запустите сопрограмму в uiScope и переключитесь на диспетчер ввода-вывода.
  • Получите tonight , используя sleepNightKey .
  • Установите качество сна.
  • Обновите базу данных.
  • Активируйте навигацию.

Обратите внимание, что приведенный ниже пример кода выполняет всю работу в обработчике щелчков, а не выделяет операцию базы данных в другой контекст.

fun onSetSleepQuality(quality: Int) {
        uiScope.launch {
            // IO is a thread pool for running operations that access the disk, such as
            // our Room database.
            withContext(Dispatchers.IO) {
                val tonight = database.get(sleepNightKey) ?: return@withContext
                tonight.sleepQuality = quality
                database.update(tonight)
            }

            // Setting this state variable to true will alert the observer and trigger navigation.
            _navigateToSleepTracker.value = true
        }
    }
  1. В пакете sleepquality создайте или откройте SleepQualityViewModelFactory.kt и добавьте класс SleepQualityViewModelFactory , как показано ниже. Этот класс использует версию того же шаблонного кода, который вы видели ранее. Прежде чем продолжить, проверьте код.
class SleepQualityViewModelFactory(
       private val sleepNightKey: Long,
       private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
           return SleepQualityViewModel(sleepNightKey, dataSource) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Шаг 2: Обновите SleepQualityFragment

  1. Откройте SleepQualityFragment.kt .
  2. В onCreateView() после получения application необходимо получить arguments , передаваемые вместе с навигацией. Эти аргументы находятся в SleepQualityFragmentArgs . Их необходимо извлечь из пакета.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
  1. Далее получаем dataSource .
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. Создайте фабрику, передав ей dataSource и sleepNightKey .
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
  1. Получить ссылку на ViewModel .
val sleepQualityViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepQualityViewModel::class.java)
  1. Добавьте ViewModel к объекту привязки. (Если вы видите ошибку с объектом привязки, пока проигнорируйте ее.)
binding.sleepQualityViewModel = sleepQualityViewModel
  1. Добавьте наблюдателя. При появлении запроса импортируйте androidx.lifecycle.Observer .
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
   if (it == true) { // Observed state is true.
       this.findNavController().navigate(
               SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
       sleepQualityViewModel.doneNavigating()
   }
})

Шаг 3: Обновите файл макета и запустите приложение.

  1. Откройте файл макета fragment_sleep_quality.xml . В блоке <data> добавьте переменную для SleepQualityViewModel .
 <data>
       <variable
           name="sleepQualityViewModel"
           type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
   </data>
  1. Для каждого из шести изображений, демонстрирующих качество сна, добавьте обработчик щелчков, как показано ниже. Сопоставьте оценку качества с изображением.
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
  1. Очистите и пересоберите проект. Это должно устранить все ошибки, связанные с объектом привязки. В противном случае очистите кэш ( Файл > Недействительные кэши / Перезапустить ) и пересоберите приложение.

Поздравляем! Вы только что создали полноценное приложение базы данных Room с использованием сопрограмм.

Теперь ваше приложение работает отлично. Пользователь может нажимать кнопки «Старт» и «Стоп» столько раз, сколько захочет. Нажатие кнопки «Стоп» позволяет выбрать качество сна. Нажатие кнопки «Очистить» автоматически удаляет все данные в фоновом режиме. Однако все кнопки всегда активны и доступны для нажатия, что не нарушает работу приложения, но позволяет пользователям создавать неполные ночи сна.

В этом последнем задании вы узнаете, как использовать карты преобразований для управления видимостью кнопок, чтобы пользователи могли сделать только правильный выбор. Аналогичный метод можно использовать для отображения дружественного сообщения после очистки всех данных.

Шаг 1: Обновите состояния кнопок

Идея состоит в том, чтобы установить состояние кнопки таким образом, чтобы в самом начале была активна только кнопка «Пуск» , то есть ее можно было бы нажать.

После нажатия кнопки «Старт» кнопка «Стоп» становится активной, а кнопка «Старт» — нет. Кнопка «Очистить» активна только при наличии данных в базе данных.

  1. Откройте файл макета fragment_sleep_tracker.xml .
  2. Добавьте свойство android:enabled к каждой кнопке. Свойство android:enabled — это логическое значение, указывающее, включена ли кнопка. ( Включенную кнопку можно нажать, а выключенную — нет.) Присвойте свойству значение переменной состояния, которую вы определите чуть позже.

start_button :

android:enabled="@{sleepTrackerViewModel.startButtonVisible}"

stop_button :

android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"

clear_button :

android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
  1. Откройте SleepTrackerViewModel и создайте три соответствующие переменные. Назначьте каждой переменной преобразование, которое её проверяет.
  • Кнопка «Пуск» должна быть включена, когда tonightnull .
  • Кнопка «Стоп» должна быть включена, когда значение tonight не равно null .
  • Кнопка «Очистить» должна быть включена только в том случае, если nights (и, следовательно, база данных) содержит sleep nights.
val startButtonVisible = Transformations.map(tonight) {
   it == null
}
val stopButtonVisible = Transformations.map(tonight) {
   it != null
}
val clearButtonVisible = Transformations.map(nights) {
   it?.isNotEmpty()
}
  1. Запустите приложение и поэкспериментируйте с кнопками.

Шаг 2: Используйте снэк-бар для уведомления пользователя

После очистки базы данных покажите пользователю подтверждение с помощью виджета Snackbar . Снэк-бар предоставляет краткую обратную связь об операции в виде сообщения в нижней части экрана. Снэк-бар исчезает по истечении времени ожидания, после взаимодействия пользователя с другим элементом экрана или после того, как пользователь смахивает его с экрана.

Отображение снэк-бара — это задача пользовательского интерфейса, и она должна выполняться во фрагменте. Решение о необходимости отображения снэк-бара принимается во ViewModel . Для настройки и запуска снэк-бара при очистке данных можно использовать тот же метод, что и для запуска навигации.

  1. В SleepTrackerViewModel создайте инкапсулированное событие.
private var _showSnackbarEvent = MutableLiveData<Boolean>()

val showSnackBarEvent: LiveData<Boolean>
   get() = _showSnackbarEvent
  1. Затем реализуйте doneShowingSnackbar() .
fun doneShowingSnackbar() {
   _showSnackbarEvent.value = false
}
  1. В SleepTrackerFragment , в onCreateView() , добавьте наблюдателя:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
  1. Внутри блока наблюдателя отобразите снэк-бар и немедленно сбросьте событие.
   if (it == true) { // Observed state is true.
       Snackbar.make(
               activity!!.findViewById(android.R.id.content),
               getString(R.string.cleared_message),
               Snackbar.LENGTH_SHORT // How long to display the message.
       ).show()
       sleepTrackerViewModel.doneShowingSnackbar()
   }
  1. В SleepTrackerViewModel вызовите событие в методе onClear() . Для этого установите значение события true внутри блока launch :
_showSnackbarEvent.value = true
  1. Создайте и запустите свое приложение!

Проект Android Studio: TrackMySleepQualityFinal

Реализация отслеживания качества сна в этом приложении подобна исполнению знакомой мелодии в новой тональности. Хотя детали меняются, базовая схема, которую вы использовали в предыдущих практических занятиях этого урока, остаётся неизменной. Знание этих схем значительно ускоряет написание кода, поскольку вы можете повторно использовать код из существующих приложений. Вот некоторые из схем, использованных в этом курсе:

  • Создайте ViewModel и ViewModelFactory и настройте источник данных.
  • Активируйте навигацию. Чтобы разделить задачи, поместите обработчик щелчков в модель представления, а навигацию — во фрагмент.
  • Используйте инкапсуляцию с LiveData для отслеживания изменений состояния и реагирования на них.
  • Используйте преобразования с LiveData .
  • Создайте одноэлементную базу данных.
  • Настройка сопрограмм для операций с базой данных.

Запуск навигации

Возможные пути навигации между фрагментами определяются в навигационном файле. Существует несколько способов инициировать навигацию между фрагментами. Вот некоторые из них:

  • Определите обработчики onClick для запуска навигации к целевому фрагменту.
  • Альтернативно, чтобы включить навигацию от одного фрагмента к другому:
  • Определите значение LiveData для записи, если должна выполняться навигация.
  • Присоедините наблюдателя к этому значению LiveData .
  • Затем ваш код изменяет это значение всякий раз, когда навигация должна быть запущена или завершена.

Установка атрибута android:enabled

  • Атрибут android:enabled определен в TextView и наследуется всеми подклассами, включая Button .
  • Атрибут android:enabled определяет, включено ли View . Значение «включено» различается в зависимости от подкласса. Например, отключенный EditText запрещает пользователю редактировать содержащийся в нём текст, а отключенный Button запрещает пользователю нажимать на кнопку.
  • Атрибут enabled не совпадает с атрибутом visibility .
  • Карты преобразования можно использовать для установки значения enabled атрибута кнопок на основе состояния другого объекта или переменной.

Другие вопросы, рассматриваемые в этой лабораторной работе:

  • Для запуска уведомлений пользователю можно использовать тот же метод, который используется для запуска навигации.
  • Для уведомления пользователя можно использовать Snackbar .

Курс Udacity:

Документация для разработчиков Android:

В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:

  • При необходимости задавайте домашнее задание.
  • Объясните учащимся, как следует сдавать домашние задания.
  • Оцените домашние задания.

Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.

Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.

Ответьте на эти вопросы

Вопрос 1

Один из способов разрешить приложению запускать навигацию от одного фрагмента к другому — использовать значение LiveData для указания того, следует ли запускать навигацию.

Каковы шаги по использованию значения LiveData с именем gotoBlueFragment для запуска навигации от красного фрагмента к синему? Выберите все подходящие варианты:

  • В ViewModel определите значение LiveData gotoBlueFragment .
  • В RedFragment обратите внимание на значение gotoBlueFragment . Реализуйте код observe{} для перехода к BlueFragment при необходимости, а затем сбросьте значение gotoBlueFragment , чтобы обозначить завершение перехода.
  • Убедитесь, что ваш код устанавливает для переменной gotoBlueFragment значение, которое запускает навигацию всякий раз, когда приложению необходимо перейти от RedFragment к BlueFragment .
  • Убедитесь, что ваш код определяет обработчик onClick для View , который пользователь щелкает для перехода к BlueFragment , где обработчик onClick отслеживает значение goToBlueFragment .

Вопрос 2

Вы можете изменить статус Button (доступность для нажатия) с помощью LiveData . Как гарантировать, что ваше приложение изменит кнопку UpdateNumber так, чтобы:

  • Кнопка доступна, если myNumber имеет значение больше 5.
  • Кнопка неактивна, если myNumber равен или меньше 5.

Предположим, что макет, содержащий кнопку UpdateNumber , включает переменную <data> для NumbersViewModel , как показано здесь:

<data>
   <variable
       name="NumbersViewModel"
       type="com.example.android.numbersapp.NumbersViewModel" />
</data>

Предположим, что идентификатор кнопки в файле макета следующий:

android:id="@+id/update_number_button"

Что ещё вам нужно сделать? Выберите всё подходящее.

  • В классе NumbersViewModel определите переменную LiveData myNumber , представляющую число. Также определите переменную, значение которой задаётся вызовом метода Transform.map() для переменной myNumber , который возвращает логическое значение, указывающее, больше ли число 5.

    В частности, в ViewModel добавьте следующий код:
val myNumber: LiveData<Int>

val enableUpdateNumberButton = Transformations.map(myNumber) {
   myNumber > 5
}
  • В макете XML установите для атрибута android:enabled update_number_button button значение NumberViewModel.enableUpdateNumbersButton .
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
  • Во Fragment , использующем класс NumbersViewModel , добавьте наблюдателя к атрибуту enabled кнопки.

    В частности, в Fragment добавьте следующий код:
// Observer for the enabled attribute
viewModel.enabled.observe(this, Observer<Boolean> { isEnabled ->
   myNumber > 5
})
  • В файле макета установите атрибут android:enabled update_number_button button в значение "Observable" .

Перейти к следующему уроку: 7.1 Основы RecyclerView

Ссылки на другие практические занятия по этому курсу см. на целевой странице практических занятий по основам Android Kotlin .