Эта практическая работа входит в курс «Основы Android Kotlin». Вы получите максимальную пользу от этого курса, если будете выполнять практические работы последовательно. Все практические работы курса перечислены на целевой странице практической работы «Основы Android Kotlin» .
Введение
В этой практической работе мы рассмотрим совместное использование ViewModel
и фрагментов для реализации навигации. Помните, что цель — реализовать логику навигации во ViewModel
, а пути — во фрагментах и файле навигации. Для достижения этой цели используются модели представлений, фрагменты, LiveData
и наблюдатели.
В заключение практической работы демонстрируется умный способ отслеживания состояний кнопок с минимальным количеством кода, чтобы каждая кнопка была включена и нажималась только тогда, когда пользователю имеет смысл нажать эту кнопку.
Что вам уже следует знать
Вам должно быть знакомо:
- Создание базового пользовательского интерфейса (UI) с использованием активности, фрагментов и представлений.
- Навигация между фрагментами и использование
safeArgs
для передачи данных между фрагментами. - Просмотр моделей, просмотр фабрик моделей, преобразований, а также
LiveData
и их наблюдателей. - Как создать базу данных
Room
, создать объект доступа к данным (DAO) и определить сущности. - Как использовать сопрограммы для взаимодействия с базой данных и других длительных задач.
Чему вы научитесь
- Как обновить существующую запись о качестве сна в базе данных.
- Как использовать
LiveData
для отслеживания состояний кнопок. - Как отобразить снэк-бар в ответ на событие.
Что ты будешь делать?
- Расширьте возможности приложения TrackMySleepQuality для сбора рейтинга качества, добавления рейтинга в базу данных и отображения результата.
- Используйте
LiveData
для отображения снэк-бара. - Используйте
LiveData
для включения и отключения кнопок.
В этой лабораторной работе вы создадите запись качества сна и окончательный пользовательский интерфейс приложения TrackMySleepQuality.
Приложение имеет два экрана, представленных фрагментами, как показано на рисунке ниже.
На первом экране, показанном слева, есть кнопки для запуска и остановки отслеживания. На экране отображаются все данные о сне пользователя. Кнопка «Очистить» безвозвратно удаляет все данные, собранные приложением для пользователя.
Второй экран, показанный справа, предназначен для выбора оценки качества сна. В приложении оценка представлена в числовом виде. В целях разработки приложение отображает как значки лиц, так и их числовые эквиваленты.
Поток действий пользователя выглядит следующим образом:
- Пользователь открывает приложение и видит экран отслеживания сна.
- Пользователь нажимает кнопку «Старт» . Время начала фиксируется и отображается на экране. Кнопка «Старт» отключается, а кнопка «Стоп» активируется.
- Пользователь нажимает кнопку «Стоп» . Записывается время окончания и открывается экран качества сна.
- Пользователь выбирает значок качества сна. Экран закрывается, и на экране отслеживания отображаются время окончания сна и качество сна. Кнопка «Стоп» отключается, а кнопка «Старт» активируется. Приложение готово к следующей ночи.
- Кнопка «Очистить» активна, когда в базе данных есть данные. При нажатии кнопки «Очистить » все данные пользователя удаляются без возможности восстановления — сообщение «Вы уверены?» не появляется.
Это приложение использует упрощённую архитектуру, как показано ниже в контексте полной архитектуры. Приложение использует только следующие компоненты:
- Контроллер пользовательского интерфейса
- Просмотреть модель и
LiveData
- База данных A Room
В этой лабораторной работе предполагается, что вы знаете, как реализовать навигацию с помощью фрагментов и навигационного файла. Для экономии времени мы предоставляем значительную часть этого кода.
Шаг 1: Проверьте код
- Чтобы начать, продолжите работу с собственным кодом из конца последней лабораторной работы или загрузите стартовый код .
- В стартовом коде проверьте
SleepQualityFragment
. Этот класс расширяет макет, получает приложение и возвращаетbinding.root
. - Откройте файл navigation.xml в редакторе дизайна. Вы увидите навигационный путь от
SleepTrackerFragment
кSleepQualityFragment
и обратно отSleepQualityFragment
кSleepTrackerFragment
. - Проверьте код navigation.xml . В частности, найдите
<argument>
с именемsleepNightKey
.
Когда пользователь переходит отSleepTrackerFragment
кSleepQualityFragment,
приложение передаетsleepNightKey
вSleepQualityFragment
для ночи, которую необходимо обновить.
Шаг 2: Добавьте навигацию для отслеживания качества сна
Граф навигации уже включает пути от SleepTrackerFragment
к SleepQualityFragment
и обратно. Однако обработчики щелчков, реализующие навигацию от одного фрагмента к другому, пока не написаны. Этот код нужно добавить сейчас в ViewModel
.
В обработчике щелчков вы устанавливаете переменную LiveData
, которая изменяется, когда приложение должно перейти к другому пункту назначения. Фрагмент отслеживает эту LiveData
. При изменении данных фрагмент переходит к пункту назначения и сообщает модели представления о завершении, что сбрасывает переменную состояния.
- Откройте
SleepTrackerViewModel
. Вам нужно добавить навигацию, чтобы при нажатии пользователем кнопки «Стоп » приложение переходило к фрагментуSleepQualityFragment
для сбора оценки качества. - В
SleepTrackerViewModel
создайте объектLiveData
, который будет изменяться, когда приложение должно переходить к фрагментуSleepQualityFragment
. Используйте инкапсуляцию, чтобы предоставитьViewModel
только доступную версиюLiveData
.
Вы можете поместить этот код в любое место на верхнем уровне тела класса.
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()
val navigateToSleepQuality: LiveData<SleepNight>
get() = _navigateToSleepQuality
- Добавьте функцию
doneNavigating()
, которая сбрасывает переменную, запускающую навигацию.
fun doneNavigating() {
_navigateToSleepQuality.value = null
}
- В обработчике щелчков кнопки «Стоп»
onStopTracking()
активируйте переход к фрагментуSleepQualityFragment
. Установите переменную _navigateToSleepQuality
в конце функции, как последнюю в блокеlaunch{}
. Обратите внимание, что эта переменная имеет значениеnight
. Если эта переменная имеет значение, приложение переходит к фрагментуSleepQualityFragment
, передавая ему значение night.
_navigateToSleepQuality.value = oldNight
- Фрагмент
SleepTrackerFragment
должен наблюдать за _navigateToSleepQuality
, чтобы приложение знало, когда осуществлять навигацию. ВonCreateView()
объектаSleepTrackerFragment
добавьте наблюдателя дляnavigateToSleepQuality()
. Обратите внимание, что импорт для этого неоднозначен, и вам необходимо импортироватьandroidx.lifecycle.Observer
.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
- Внутри блока observer выполните навигацию и передайте идентификатор текущей ночи, а затем вызовите
doneNavigating()
. Если ваш импорт неоднозначен, импортируйтеandroidx.navigation.fragment.findNavController
.
night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
sleepTrackerViewModel.doneNavigating()
}
- Создайте и запустите приложение. Нажмите «Пуск» , затем нажмите «Стоп» , чтобы открыть экран
SleepQualityFragment
. Чтобы вернуться назад, используйте системную кнопку «Назад».
В этой задаче вы регистрируете качество сна и возвращаетесь к фрагменту трекера сна. Дисплей должен автоматически обновляться, чтобы отображать пользователю обновлённое значение. Вам необходимо создать ViewModel
и ViewModelFactory
, а также обновить SleepQualityFragment
.
Шаг 1: Создайте ViewModel и ViewModelFactory
- В пакете
sleepquality
создайте или откройте SleepQualityViewModel.kt. - Создайте класс
SleepQualityViewModel
, принимающий в качестве аргументовsleepNightKey
и базу данных. Как и дляSleepTrackerViewModel
, необходимо передатьdatabase
из фабрики. Также необходимо передатьsleepNightKey
из навигации.
class SleepQualityViewModel(
private val sleepNightKey: Long = 0L,
val database: SleepDatabaseDao) : ViewModel() {
}
- Внутри класса
SleepQualityViewModel
определитеJob
иuiScope
и переопределитеonCleared()
.
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
- To navigate back to the
SleepTrackerFragment
using the same pattern as above, declare_navigateToSleepTracker
. ImplementnavigateToSleepTracker
anddoneNavigating()
.
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()
val navigateToSleepTracker: LiveData<Boolean?>
get() = _navigateToSleepTracker
fun doneNavigating() {
_navigateToSleepTracker.value = null
}
- Создайте обработчик одного щелчка
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
}
}
- В пакете
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
- Откройте
SleepQualityFragment.kt
. - В
onCreateView()
после полученияapplication
необходимо получитьarguments
, передаваемые вместе с навигацией. Эти аргументы находятся вSleepQualityFragmentArgs
. Их необходимо извлечь из пакета.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
- Далее получаем
dataSource
.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- Создайте фабрику, передав ей
dataSource
иsleepNightKey
.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
- Получить ссылку на
ViewModel
.
val sleepQualityViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepQualityViewModel::class.java)
- Добавьте
ViewModel
к объекту привязки. (Если вы видите ошибку с объектом привязки, пока проигнорируйте ее.)
binding.sleepQualityViewModel = sleepQualityViewModel
- Добавьте наблюдателя. При появлении запроса импортируйте
androidx.lifecycle.Observer
.
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
if (it == true) { // Observed state is true.
this.findNavController().navigate(
SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
sleepQualityViewModel.doneNavigating()
}
})
Шаг 3: Обновите файл макета и запустите приложение.
- Откройте файл макета
fragment_sleep_quality.xml
. В блоке<data>
добавьте переменную дляSleepQualityViewModel
.
<data>
<variable
name="sleepQualityViewModel"
type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
</data>
- Для каждого из шести изображений, демонстрирующих качество сна, добавьте обработчик щелчков, как показано ниже. Сопоставьте оценку качества с изображением.
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
- Очистите и пересоберите проект. Это должно устранить все ошибки, связанные с объектом привязки. В противном случае очистите кэш ( Файл > Недействительные кэши / Перезапустить ) и пересоберите приложение.
Поздравляем! Вы только что создали полноценное приложение базы данных Room
с использованием сопрограмм.
Теперь ваше приложение работает отлично. Пользователь может нажимать кнопки «Старт» и «Стоп» столько раз, сколько захочет. Нажатие кнопки «Стоп» позволяет выбрать качество сна. Нажатие кнопки «Очистить» автоматически удаляет все данные в фоновом режиме. Однако все кнопки всегда активны и доступны для нажатия, что не нарушает работу приложения, но позволяет пользователям создавать неполные ночи сна.
В этом последнем задании вы узнаете, как использовать карты преобразований для управления видимостью кнопок, чтобы пользователи могли сделать только правильный выбор. Аналогичный метод можно использовать для отображения дружественного сообщения после очистки всех данных.
Шаг 1: Обновите состояния кнопок
Идея состоит в том, чтобы установить состояние кнопки таким образом, чтобы в самом начале была активна только кнопка «Пуск» , то есть ее можно было бы нажать.
После нажатия кнопки «Старт» кнопка «Стоп» становится активной, а кнопка «Старт» — нет. Кнопка «Очистить» активна только при наличии данных в базе данных.
- Откройте файл макета
fragment_sleep_tracker.xml
. - Добавьте свойство
android:enabled
к каждой кнопке. Свойствоandroid:enabled
— это логическое значение, указывающее, включена ли кнопка. ( Включенную кнопку можно нажать, а выключенную — нет.) Присвойте свойству значение переменной состояния, которую вы определите чуть позже.
start_button
:
android:enabled="@{sleepTrackerViewModel.startButtonVisible}"
stop_button
:
android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"
clear_button
:
android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
- Откройте
SleepTrackerViewModel
и создайте три соответствующие переменные. Назначьте каждой переменной преобразование, которое её проверяет.
- Кнопка «Пуск» должна быть включена, когда
tonight
—null
. - Кнопка «Стоп» должна быть включена, когда значение
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()
}
- Запустите приложение и поэкспериментируйте с кнопками.
Шаг 2: Используйте снэк-бар для уведомления пользователя
После очистки базы данных покажите пользователю подтверждение с помощью виджета Snackbar
. Снэк-бар предоставляет краткую обратную связь об операции в виде сообщения в нижней части экрана. Снэк-бар исчезает по истечении времени ожидания, после взаимодействия пользователя с другим элементом экрана или после того, как пользователь смахивает его с экрана.
Отображение снэк-бара — это задача пользовательского интерфейса, и она должна выполняться во фрагменте. Решение о необходимости отображения снэк-бара принимается во ViewModel
. Для настройки и запуска снэк-бара при очистке данных можно использовать тот же метод, что и для запуска навигации.
- В
SleepTrackerViewModel
создайте инкапсулированное событие.
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
- Затем реализуйте
doneShowingSnackbar()
.
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
- В
SleepTrackerFragment
, вonCreateView()
, добавьте наблюдателя:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
- Внутри блока наблюдателя отобразите снэк-бар и немедленно сбросьте событие.
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()
}
- В
SleepTrackerViewModel
вызовите событие в методеonClear()
. Для этого установите значение событияtrue
внутри блокаlaunch
:
_showSnackbarEvent.value = true
- Создайте и запустите свое приложение!
Проект 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"
.
Перейти к следующему уроку:
Ссылки на другие практические занятия по этому курсу см. на целевой странице практических занятий по основам Android Kotlin .