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

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

Введение

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

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

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

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

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

Что вы узнаете

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

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

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

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

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

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

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

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

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

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

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

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

Шаг 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 , передавая ночь.
_navigateToSleepQuality.value = oldNight
  1. SleepTrackerFragment должен отслеживать _ navigateToSleepQuality , чтобы приложение знало, когда следует переходить. В SleepTrackerFragment в onCreateView() добавьте наблюдателя для navigationToSleepQuality navigateToSleepQuality() . Обратите внимание, что импорт для этого неоднозначен, и вам нужно импортировать androidx.lifecycle.Observer .
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})

  1. Внутри блока наблюдателя перейдите и передайте идентификатор текущей ночи, а затем вызовите doneNavigating() . Если ваш импорт неоднозначен, импортируйте androidx.navigation.fragment.findNavController .
night ->
night?.let {
   this.findNavController().navigate(
           SleepTrackerFragmentDirections
                   .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
   sleepTrackerViewModel.doneNavigating()
}
  1. Создайте и запустите свое приложение. Коснитесь Start , затем коснитесь Stop , чтобы перейти к экрану 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. Чтобы вернуться к SleepTrackerFragment используя тот же шаблон, что и выше, объявите _navigateToSleepTracker . navigateToSleepTracker navigationToSleepTracker и 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. Очистите и перестройте свой проект. Это должно устранить любые ошибки с объектом привязки. В противном случае очистите кеш ( File > Invalidate Caches / Restart ) и перестройте приложение.

Поздравляем! Вы только что создали полное приложение базы данных 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 и создайте три соответствующие переменные. Назначьте каждой переменной преобразование, которое ее проверяет.
  • Кнопка Start должна быть включена, когда tonight равна null .
  • Кнопка « Стоп » должна быть включена, если tonight не равно null .
  • Кнопку « Очистить » следует активировать только в том случае, если 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 для уведомления пользователя.

Удасити курс:

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

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

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

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

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

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

Вопрос 1

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

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

  • В ViewModel определите значение LiveData gotoBlueFragment .
  • В RedFragment внимание на значение gotoBlueFragment . Внедрите кодObserve 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 update_number_button значение NumberViewModel.enableUpdateNumbersButton .
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
  • Во Fragment , использующем класс NumbersViewModel , добавьте наблюдателя к enabled enable кнопки.

    В частности, в 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 Fundamentals .