Основы Android Kotlin 07.4: Взаимодействие с RecyclerСмотреть элементы

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

Введение

Большинство приложений, использующих списки и сетки для отображения элементов, позволяют пользователям взаимодействовать с ними. Нажатие на элемент списка и просмотр подробной информации об элементе — очень распространённый вариант такого взаимодействия. Для этого можно добавить обработчики щелчков, которые реагируют на нажатия пользователем на элементы, отображая подробную информацию.

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

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

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

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

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

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

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

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

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

Это приложение использует упрощенную архитектуру с контроллером пользовательского интерфейса, моделью представления и LiveData , а также базу данных Room для сохранения данных о сне.

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

Шаг 1: Получите стартовое приложение

  1. Загрузите код RecyclerViewClickHandler-Starter с GitHub и откройте проект в Android Studio.
  2. Создайте и запустите базовое приложение для отслеживания сна.

[Необязательно] Обновите приложение, если хотите использовать приложение из предыдущей лабораторной работы.

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

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

  1. Даже если вы продолжаете использовать существующее приложение, получите код RecyclerViewClickHandler-Starter с GitHub, чтобы можно было скопировать файлы.
  2. Скопируйте все файлы в пакет sleepdetail .
  3. В папку layout скопируйте файл fragment_sleep_detail.xml .
  4. Скопируйте обновленное содержимое navigation.xml , которое добавляет навигацию для sleep_detail_fragment .
  5. В пакете database , в SleepDatabaseDao , добавьте новый метод getNightWithId() :
/**
 * Selects and returns the night with given nightId.
*/
@Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
fun getNightWithId(key: Long): LiveData<SleepNight>
  1. В res/values/strings добавьте следующий строковый ресурс:
<string name="close">Close</string>
  1. Очистите и перестройте свое приложение, чтобы обновить привязку данных.

Шаг 2: Проверьте код экрана сведений о сне.

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

  1. В вашем приложении найдите пакет sleepdetail . Этот пакет содержит фрагмент, модель представления и фабрику моделей представления для фрагмента, отображающего сведения об одной ночи сна.

  2. В пакете sleepdetail откройте и изучите код SleepDetailViewModel . Эта модель представления принимает ключ SleepNight и DAO в конструкторе.

    Тело класса содержит код для получения SleepNight для заданного ключа и переменную navigateToSleepTracker для управления навигацией обратно к SleepTrackerFragment при нажатии кнопки «Закрыть» .

    Функция getNightWithId() возвращает LiveData<SleepNight> и определена в SleepDatabaseDao (в пакете database ).

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

  4. В пакете sleepdetail откройте и проверьте код SleepDetailViewModelFactory .

  5. В папке макета проверьте файл fragment_sleep_detail.xml . Обратите внимание на переменную sleepDetailViewModel , определённую в теге <data> , которая позволяет получать данные для отображения в каждом представлении из модели представления.

    Макет содержит ConstraintLayout , который содержит ImageView для качества сна, TextView для оценки качества, TextView для продолжительности сна и Button для закрытия фрагмента сведений.

  6. Откройте файл navigation.xml . Для sleep_tracker_fragment обратите внимание на новое действие sleep_detail_fragment .

    Новое действие action_sleep_tracker_fragment_to_sleepDetailFragment — это навигация от фрагмента трекера сна к экрану подробностей.

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

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

Итак, где лучше всего добавить прослушиватель кликов для этого приложения?

  • SleepTrackerFragment содержит множество представлений, поэтому отслеживание событий щелчков на уровне фрагмента не покажет, какой именно элемент был нажат. Вы даже не узнаете, был ли нажат элемент или один из других элементов пользовательского интерфейса.
  • Прослушивая данные на уровне RecyclerView , сложно определить, на какой именно элемент списка нажал пользователь.
  • Лучший способ получить информацию об одном выбранном элементе — это использовать объект ViewHolder , поскольку он представляет один элемент списка.

Хотя ViewHolder — отличное место для отслеживания щелчков, он обычно не подходит для их обработки. Итак, какое место лучше всего подходит для обработки щелчков?

  • Adapter отображает элементы данных в представлениях, что позволяет обрабатывать щелчки в адаптере. Однако задача адаптера — адаптировать данные для отображения, а не обрабатывать логику приложения.
  • Обычно обрабатывать щелчки следует в ViewModel , поскольку ViewModel имеет доступ к данным и логике для определения того, что должно произойти в ответ на щелчок.

Шаг 1: Создайте прослушиватель щелчков и активируйте его из макета элемента.

  1. В папке sleeptracker откройте SleepNightAdapter.kt .
  2. В конце файла, на верхнем уровне, создайте новый класс слушателя SleepNightListener .
class SleepNightListener() {
    
}
  1. В классе SleepNightListener добавьте функцию onClick() . При щелчке по представлению, отображающему элемент списка, оно вызывает эту функцию onClick() . (Позже вы назначите этой функции свойство android:onClick представления.)
class SleepNightListener() {
    fun onClick() = 
}
  1. Добавьте аргумент функции night типа SleepNight к onClick() . Представление знает, какой элемент оно отображает, и эту информацию необходимо передать для обработки щелчка.
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. Чтобы определить, что делает onClick() , предоставьте обратный вызов clickListener в конструкторе SleepNightListener и назначьте его onClick() .

    Присвоение имени лямбда-функции, обрабатывающей щелчок, clickListener помогает отслеживать его при передаче между классами. Обратному вызову clickListener требуется только night.nightId для доступа к данным из базы данных. Готовый класс SleepNightListener должен выглядеть так, как показано ниже.
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. Откройте list_item_sleep_night.xml.
  2. Внутри блока data добавьте новую переменную, чтобы сделать класс SleepNightListener доступным через привязку данных. Присвойте новой <variable> name clickListener. Задайте type , соответствующий полному имени класса com.example.android.trackmysleepquality.sleeptracker.SleepNightListener , как показано ниже. Теперь вы можете получить доступ к функции onClick() в SleepNightListener из этого макета.
<variable
            name="clickListener"
            type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. Чтобы отслеживать нажатия на любую часть этого элемента списка, добавьте атрибут android:onClick к ConstraintLayout .

    Установите атрибут clickListener:onClick(sleep) с помощью лямбда-функции привязки данных, как показано ниже:
android:onClick="@{() -> clickListener.onClick(sleep)}"

Шаг 2: Передайте прослушиватель щелчков держателю представления и объекту привязки.

  1. Откройте SleepNightAdapter.kt.
  2. Измените конструктор класса SleepNightAdapter так, чтобы он принимал val clickListener: SleepNightListener . Когда адаптер привязывает ViewHolder , ему необходимо будет предоставить этот прослушиватель щелчков.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. В onBindViewHolder() обновите вызов holder.bind() , чтобы также передать прослушиватель щелчков ViewHolder . Вы получите ошибку компиляции, поскольку вы добавили параметр в вызов функции.
holder.bind(getItem(position)!!, clickListener)
  1. Добавьте параметр clickListener в метод bind() . Для этого наведите курсор на ошибку и нажмите Alt+Enter (Windows) или Option+Enter (Mac), как показано на снимке экрана ниже.

  1. Внутри класса ViewHolder , в функции bind() , назначьте прослушиватель щелчков объекту binding . Вы увидите ошибку, поскольку необходимо обновить объект привязки.
binding.clickListener = clickListener
  1. Чтобы обновить привязку данных, очистите и перестройте свой проект. (Возможно, вам также потребуется сделать кэши недействительными.) Итак, вы взяли прослушиватель щелчков из конструктора адаптера и передали его полностью держателю представления и в объект привязки.

Шаг 3: Отображение уведомления при нажатии на элемент

Теперь у вас есть код для захвата нажатия, но вы не реализовали реакцию на нажатие элемента списка. Простейший способ — отобразить уведомление с nightId при щелчке по элементу. Это подтверждает, что при щелчке по элементу списка фиксируется и передаётся правильный nightId .

  1. Откройте SleepTrackerFragment.kt.
  2. В onCreateView() найдите переменную adapter . Обратите внимание, что она выдаёт ошибку, поскольку теперь ожидает параметр прослушивателя кликов.
  3. Определите прослушиватель кликов, передав лямбда-выражение в SleepNightAdapter . Эта простая лямбда-функция просто выводит уведомление с nightId , как показано ниже. Вам потребуется импортировать Toast . Ниже приведено полное обновлённое определение.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Запустите приложение, нажмите на элементы и убедитесь, что они отображают уведомление с правильным nightId . Поскольку значения nightId элементов увеличиваются, а приложение сначала отображает последнюю ночь, элемент с наименьшим nightId находится внизу списка.

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

Шаг 1: Переход по щелчку

На этом этапе вместо того, чтобы просто отображать уведомление, вы изменяете лямбда-функцию прослушивателя кликов в onCreateView() объекта SleepTrackerFragment для передачи nightId в SleepTrackerViewModel и запускаете навигацию к SleepDetailFragment .

Определите функцию обработчика щелчков:

  1. Откройте SleepTrackerViewModel.kt .
  2. Внутри SleepTrackerViewModel , ближе к концу, определите функцию обработчика щелчков onSleepNightClicked() .
fun onSleepNightClicked(id: Long) {

}
  1. Внутри onSleepNightClicked() активируйте навигацию, установив _navigateToSleepDetail на переданный id нажатой ночи сна.
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. Реализуйте _navigateToSleepDetail . Как и раньше, определите private MutableLiveData для состояния навигации и общедоступный val для него.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. Определите метод, который будет вызван после завершения навигации. Вызовите его onSleepDetailNavigated() и установите для него значение null .
fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

Добавьте код для вызова обработчика кликов:

  1. Откройте SleepTrackerFragment.kt и прокрутите вниз до кода, который создает адаптер и определяет SleepNightListener , чтобы отобразить уведомление.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Добавьте следующий код под уведомлением, чтобы вызвать обработчик щелчков onSleepNighClicked() в sleepTrackerViewModel при касании элемента. Передайте nightId , чтобы модель представления знала, какую ночь сна выбрать. Это приведёт к ошибке, поскольку метод onSleepNightClicked() ещё не определён. Вы можете сохранить уведомление, закомментировать или удалить его по своему усмотрению.
sleepTrackerViewModel.onSleepNightClicked(nightId)

Добавьте код для наблюдения за кликами:

  1. Откройте SleepTrackerFragment.kt .
  2. В onCreateView() , прямо над объявлением manager , добавьте код для отслеживания нового navigateToSleepDetail LiveData . При изменении navigateToSleepDetail перейдите к SleepDetailFragment , передав ему значение night , а затем вызовите onSleepDetailNavigated() . Поскольку вы уже делали это в предыдущей лабораторной работе, вот код:
sleepTrackerViewModel.navigateToSleepDetail.observe(this, Observer { night ->
            night?.let {
              this.findNavController().navigate(
                        SleepTrackerFragmentDirections
                                .actionSleepTrackerFragmentToSleepDetailFragment(night))
               sleepTrackerViewModel.onSleepDetailNavigated()
            }
        })
  1. Запустите свой код, нажмите на элемент и... приложение аварийно завершит работу.

Обработка нулевых значений в адаптерах привязки:

  1. Запустите приложение ещё раз в режиме отладки. Нажмите на нужный элемент и отфильтруйте журналы, чтобы увидеть ошибки. Будет показана трассировка стека, включающая что-то похожее на то, что показано ниже.
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item

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

Однако оказывается, что с этим новым механизмом обработки кликов адаптеры привязки теперь могут вызываться со значением null для item . В частности, при запуске приложения LiveData запускается со null , поэтому необходимо добавить проверки на null для каждого адаптера.

  1. В BindingUtils.kt для каждого адаптера привязки измените тип аргумента item на nullable и заключите тело в item?.let{...} . Например, ваш адаптер для sleepQualityString будет выглядеть так. Измените остальные адаптеры аналогичным образом.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. Запустите приложение. Нажмите на элемент, и откроется подробное представление.

Проект Android Studio: RecyclerViewClickHandler .

Чтобы элементы в RecyclerView реагировали на щелчки, прикрепите прослушиватели щелчков к элементам списка в ViewHolder и обрабатывайте щелчки в ViewModel .

Чтобы элементы в RecyclerView реагировали на клики, необходимо сделать следующее:

  • Создайте класс прослушивателя, который принимает лямбда-выражение и назначает его функции onClick() .
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  • Установите прослушиватель щелчков на представление.
android:onClick="@{() -> clickListener.onClick(sleep)}"
  • Передайте прослушиватель щелчков конструктору адаптера, в держатель представления и добавьте его к объекту привязки.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()
holder.bind(getItem(position)!!, clickListener)
binding.clickListener = clickListener
  • Во фрагменте, демонстрирующем представление recycler, где вы создаете адаптер, определите прослушиватель щелчков, передав лямбда-выражение адаптеру.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
      sleepTrackerViewModel.onSleepNightClicked(nightId)
})
  • Реализуйте обработчик щелчков в модели представления. При щелчках по элементам списка это обычно приводит к переходу к фрагменту с подробностями.

Курс Udacity:

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

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

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

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

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

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

Вопрос 1

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

class ShoppingListItemListener(val clickListener: (itemId: Long) -> Unit) {
    fun onClick(cartItem: CartItem) = clickListener(cartItem.itemId)
}

Как сделать ShoppingListItemListener доступным для привязки данных? Выберите один из вариантов.

▢ В файле макета, содержащем RecyclerView , отображающий список покупок, добавьте переменную <data> для ShoppingListItemListener .

▢ В файле макета, который определяет макет для отдельной строки в списке покупок, добавьте переменную <data> для ShoppingListItemListener .

▢ В классе ShoppingListItemListener добавьте функцию для включения привязки данных:

fun onBinding (cartItem: CartItem) {dataBindingEnable(true)}

▢ В классе ShoppingListItemListener внутри функции onClick() добавьте вызов для включения привязки данных:

fun onClick(cartItem: CartItem) = { 
    clickListener(cartItem.itemId)
    dataBindingEnable(true)
}

Вопрос 2

Куда добавить атрибут android:onClick , чтобы элементы в RecyclerView реагировали на нажатия? Выберите все подходящие варианты.

▢ В файле макета, который отображает RecyclerView , добавьте его в <androidx.recyclerview.widget.RecyclerView>

▢ Добавьте его в файл макета для элемента в строке. Если вы хотите, чтобы весь элемент был кликабельным, добавьте его в родительское представление, содержащее элементы в строке.

▢ Добавьте его в файл макета для элемента в строке. Если вы хотите, чтобы один TextView в элементе был кликабельным, добавьте его в <TextView> .

▢ Всегда добавляйте его в файл макета для MainActivity .

Начните следующий урок: 7.5: Заголовки в RecyclerView