Android Kotlin Fundamentals 07.4: Interakcja z elementami RecyclerView

Ten moduł Codelab jest częścią kursu Android Kotlin Fundamentals. Najwięcej korzyści przyniesie Ci ukończenie wszystkich ćwiczeń w kolejności. Wszystkie ćwiczenia z tego kursu znajdziesz na stronie docelowej kursu Android Kotlin Fundamentals.

Wprowadzenie

Większość aplikacji, które korzystają z list i siatek wyświetlających elementy, umożliwia użytkownikom interakcję z tymi elementami. Kliknięcie elementu na liście i wyświetlenie jego szczegółów to bardzo częsty przypadek użycia tego typu interakcji. Aby to zrobić, możesz dodać odbiorniki kliknięć, które reagują na kliknięcia użytkownika, wyświetlając widok szczegółowy.

W tym ćwiczeniu z programowania dodasz interakcję do aplikacji RecyclerView, rozwijając rozszerzoną wersję aplikacji do śledzenia snu z poprzedniej serii ćwiczeń z programowania.

Co warto wiedzieć

  • Tworzenie podstawowego interfejsu użytkownika za pomocą aktywności, fragmentów i widoków.
  • przechodzenie między fragmentami i używanie safeArgs do przekazywania danych między fragmentami;
  • Wyświetlanie modeli, fabryk modeli, przekształceń i LiveData oraz ich obserwatorów.
  • Jak utworzyć bazę danych Room, obiekt dostępu do danych (DAO) i zdefiniować encje.
  • Jak używać korutyn do obsługi baz danych i innych długotrwałych zadań.
  • Jak wdrożyć podstawowy RecyclerView z Adapter, ViewHolder i układem elementów.
  • Jak zaimplementować wiązanie danych w przypadku RecyclerView.
  • Jak tworzyć i używać adapterów powiązań do przekształcania danych.
  • Instrukcje korzystania z GridLayoutManager.

Czego się nauczysz

  • Jak sprawić, aby elementy w RecyclerView były klikalne. Zaimplementuj detektor kliknięć, aby po kliknięciu elementu przejść do widoku szczegółów.

Jakie zadania wykonasz

  • Skorzystaj z rozszerzonej wersji aplikacji TrackMySleepQuality z poprzedniego laboratorium w tej serii.
  • Dodaj do listy odbiornik kliknięć i zacznij nasłuchiwać interakcji użytkownika. Gdy użytkownik kliknie element listy, nastąpi przejście do fragmentu ze szczegółami klikniętego elementu. Kod początkowy zawiera kod fragmentu szczegółów oraz kod nawigacji.

Aplikacja do śledzenia snu ma 2 ekrany reprezentowane przez fragmenty, jak pokazano na ilustracji poniżej.

Pierwszy ekran, widoczny po lewej stronie, zawiera przyciski rozpoczynania i zatrzymywania śledzenia. Na ekranie wyświetlają się niektóre dane dotyczące snu użytkownika. Przycisk Wyczyść trwale usuwa wszystkie dane zebrane przez aplikację na temat użytkownika. Drugi ekran, widoczny po prawej stronie, służy do wybierania oceny jakości snu.

Ta aplikacja korzysta z uproszczonej architektury z kontrolerem interfejsu, modelem widoku i LiveData oraz bazą danych Room do przechowywania danych o śnie.

W tym laboratorium dodasz możliwość reagowania na kliknięcie elementu w siatce przez użytkownika, co spowoduje wyświetlenie ekranu szczegółów podobnego do tego poniżej. Kod tego ekranu (fragment, model widoku i nawigacja) jest dostępny w aplikacji startowej. Ty musisz zaimplementować mechanizm obsługi kliknięć.

Krok 1. Pobierz aplikację startową

  1. Pobierz kod początkowy RecyclerViewClickHandler z GitHuba i otwórz projekt w Android Studio.
  2. Skompiluj i uruchom aplikację do śledzenia snu.

[Opcjonalnie] Zaktualizuj aplikację, jeśli chcesz używać aplikacji z poprzedniego ćwiczenia z programowania

Jeśli w tym laboratorium chcesz korzystać z aplikacji startowej udostępnionej w GitHubie, przejdź do następnego kroku.

Jeśli chcesz nadal używać własnej aplikacji do śledzenia snu, którą utworzono w poprzednim laboratorium, postępuj zgodnie z poniższymi instrukcjami, aby zaktualizować istniejącą aplikację i dodać do niej kod fragmentu ekranu szczegółów.

  1. Nawet jeśli kontynuujesz pracę nad istniejącą aplikacją, pobierz z GitHuba kod początkowy RecyclerViewClickHandler, aby móc skopiować pliki.
  2. Skopiuj wszystkie pliki z pakietu sleepdetail.
  3. W folderze layout skopiuj plik fragment_sleep_detail.xml.
  4. Skopiuj zaktualizowaną zawartość pliku navigation.xml, która dodaje nawigację dla sleep_detail_fragment.
  5. database pakiecie w SleepDatabaseDao dodaj nową metodę 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. W pliku res/values/strings dodaj ten zasób ciągu znaków:
<string name="close">Close</string>
  1. Wyczyść i ponownie skompiluj aplikację, aby zaktualizować wiązanie danych.

Krok 2. Sprawdź kod ekranu szczegółów snu

W tym laboratorium kodowania zaimplementujesz moduł obsługi kliknięć, który przechodzi do fragmentu zawierającego szczegóły klikniętej nocy snu. Kod początkowy zawiera już fragment i graf nawigacji dla tego SleepDetailFragment, ponieważ jest to dość dużo kodu, a fragmenty i nawigacja nie są częścią tego laboratorium. Zapoznaj się z tym kodem:

  1. W aplikacji znajdź pakiet sleepdetail. Ten pakiet zawiera fragment, model widoku i fabrykę modelu widoku dla fragmentu, który wyświetla szczegóły dotyczące jednej nocy snu.

  2. W pakiecie sleepdetail otwórz i sprawdź kod SleepDetailViewModel. Ten model widoku przyjmuje klucz dla SleepNight i obiekt DAO w konstruktorze.

    W treści klasy znajduje się kod, który pobiera SleepNight dla danego klucza, oraz zmienna navigateToSleepTracker, która umożliwia powrót do SleepTrackerFragment po naciśnięciu przycisku Zamknij.

    Funkcja getNightWithId() zwraca wartość LiveData<SleepNight> i jest zdefiniowana w SleepDatabaseDao (w pakiecie database).

  3. W pakiecie sleepdetail otwórz i sprawdź kod SleepDetailFragment. Zwróć uwagę na konfigurację powiązania danych, model widoku i obserwatora nawigacji.

  4. W pakiecie sleepdetail otwórz i sprawdź kod SleepDetailViewModelFactory.

  5. W folderze układu sprawdź fragment_sleep_detail.xml. Zwróć uwagę na zmienną sleepDetailViewModel zdefiniowaną w tagu <data>, aby uzyskać dane do wyświetlenia w każdym widoku z modelu widoku.

    Układ zawiera element ConstraintLayout, który zawiera element ImageView dla jakości snu, element TextView dla oceny jakości, element TextView dla długości snu i element Button do zamknięcia fragmentu szczegółów.

  6. Otwórz plik navigation.xml. Zwróć uwagę na nowe działanie w przypadku sleep_detail_fragmentsleep_tracker_fragment.

    Nowe działanie, action_sleep_tracker_fragment_to_sleepDetailFragment, to przejście z fragmentu śledzenia snu na ekran szczegółów.

W tym zadaniu zaktualizujesz RecyclerView, aby reagował na kliknięcia użytkownika, wyświetlając ekran szczegółów klikniętego elementu.

Odbieranie kliknięć i ich obsługa to zadanie dwuetapowe: najpierw musisz nasłuchiwać i odbierać kliknięcia oraz określać, który element został kliknięty. Następnie musisz zareagować na kliknięcie, wykonując działanie.

Gdzie najlepiej dodać odbiornik kliknięć w tej aplikacji?

  • SleepTrackerFragment zawiera wiele widoków, więc nasłuchiwanie zdarzeń kliknięcia na poziomie fragmentu nie powie Ci, który element został kliknięty. Nie powie Ci nawet, czy kliknięto element, czy jeden z innych elementów interfejsu.
  • Słuchając na poziomie RecyclerView, trudno jest dokładnie określić, który element na liście kliknął użytkownik.
  • Najlepszym sposobem na uzyskanie informacji o klikniętym elemencie jest użycie obiektu ViewHolder, ponieważ reprezentuje on jeden element listy.

Chociaż ViewHolder to świetne miejsce do nasłuchiwania kliknięć, zwykle nie jest to odpowiednie miejsce do ich obsługi. Gdzie najlepiej obsługiwać kliknięcia?

  • Adapter wyświetla elementy danych w widokach, dzięki czemu możesz obsługiwać kliknięcia w adapterze. Zadaniem adaptera jest jednak dostosowywanie danych do wyświetlania, a nie obsługa logiki aplikacji.
  • Kliknięcia należy zwykle obsługiwać w ViewModel, ponieważ ViewModel ma dostęp do danych i logiki, które pozwalają określić, co ma się stać w odpowiedzi na kliknięcie.

Krok 1. Utwórz odbiornik kliknięć i wywołaj go z układu elementu

  1. W folderze sleeptracker otwórz plik SleepNightAdapter.kt.
  2. Na końcu pliku, na najwyższym poziomie, utwórz nową klasę odbiornika SleepNightListener.
class SleepNightListener() {
    
}
  1. W klasie SleepNightListener dodaj funkcję onClick(). Gdy klikniesz widok, który wyświetla element listy, wywoła on tę funkcję onClick(). (Właściwość android:onClick widoku danych ustawisz później na tę funkcję).
class SleepNightListener() {
    fun onClick() = 
}
  1. Dodaj argument funkcji night typu SleepNight do onClick(). Widok wie, który element wyświetla, i musi przekazać te informacje, aby obsłużyć kliknięcie.
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. Aby zdefiniować działanie onClick(), podaj wywołanie zwrotne clickListener w konstruktorze SleepNightListener i przypisz je do onClick().

    Nadanie wyrażeniu lambda obsługującemu kliknięcie nazwy clickListener ułatwia śledzenie go podczas przekazywania między klasami. Wywołanie zwrotne clickListener potrzebuje tylko night.nightId, aby uzyskać dostęp do danych z bazy danych. Ukończona klasa SleepNightListener powinna wyglądać jak poniższy kod.
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. Otwórz plik list_item_sleep_night.xml..
  2. W bloku data dodaj nową zmienną, aby udostępnić klasę SleepNightListener za pomocą powiązania danych. Nadaj nowemu <variable> name o wartości clickListener.. Ustaw type na pełną nazwę klasy com.example.android.trackmysleepquality.sleeptracker.SleepNightListener, jak pokazano poniżej. W tym układzie możesz teraz uzyskać dostęp do funkcji onClick()SleepNightListener.
<variable
            name="clickListener"
            type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. Aby nasłuchiwać kliknięć w dowolnej części elementu listy, dodaj atrybut android:onClick do elementu ConstraintLayout.

    Ustaw atrybut na clickListener:onClick(sleep) za pomocą lambdy wiązania danych, jak pokazano poniżej:
android:onClick="@{() -> clickListener.onClick(sleep)}"

Krok 2. Przekaż odbiornik kliknięć do uchwytu widoku i obiektu powiązania

  1. Otwórz plik SleepNightAdapter.kt.
  2. Zmodyfikuj konstruktor klasy SleepNightAdapter, aby otrzymywał obiekt val clickListener: SleepNightListener. Gdy adapter powiąże ViewHolder, będzie musiał przekazać mu ten odbiornik kliknięć.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. W pliku onBindViewHolder() zaktualizuj wywołanie funkcji holder.bind(), aby przekazywać detektor kliknięć do funkcji ViewHolder. Otrzymasz błąd kompilatora, ponieważ do wywołania funkcji został dodany parametr.
holder.bind(getItem(position)!!, clickListener)
  1. Dodaj parametr clickListener do bind(). Aby to zrobić, umieść kursor na błędzie i naciśnij Alt+Enter (Windows) lub Option+Enter (Mac), jak pokazano na zrzucie ekranu poniżej.

  1. W klasie ViewHolder, w funkcji bind() przypisz detektor kliknięć do obiektu binding. Wyświetla się błąd, ponieważ musisz zaktualizować obiekt powiązania.
binding.clickListener = clickListener
  1. Aby zaktualizować powiązanie danych, wyczyśćponownie skompiluj projekt. (Może być też konieczne unieważnienie pamięci podręcznych). Pobierasz więc odbiornik kliknięć z konstruktora adaptera i przekazujesz go do obiektu widoku i obiektu powiązania.

Krok 3. Wyświetlaj powiadomienie po dotknięciu elementu

Masz już kod, który rejestruje kliknięcie, ale nie masz jeszcze kodu, który określa, co się stanie po kliknięciu elementu listy. Najprostsza odpowiedź to wyświetlenie powiadomienia z symbolem nightId po kliknięciu elementu. Dzięki temu sprawdzisz, czy po kliknięciu elementu listy rejestrowany i przekazywany jest prawidłowy nightId.

  1. Otwórz plik SleepTrackerFragment.kt.
  2. W sekcji onCreateView() znajdź zmienną adapter. Zwróć uwagę, że wyświetla błąd, ponieważ oczekuje teraz parametru odbiornika kliknięć.
  3. Zdefiniuj odbiornik kliknięć, przekazując lambdę do funkcji SleepNightAdapter. Ta prosta lambda wyświetla tylko powiadomienie z wartością nightId, jak pokazano poniżej. Musisz zaimportować Toast. Poniżej znajdziesz pełną, zaktualizowaną definicję.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Uruchom aplikację, kliknij elementy i sprawdź, czy wyświetla się komunikat z prawidłowym nightId. Ponieważ elementy mają rosnące wartości nightId, a aplikacja wyświetla najpierw najnowszą noc, element z najniższą wartością nightId znajduje się na dole listy.

W tym zadaniu zmienisz zachowanie po kliknięciu elementu w RecyclerView, tak aby zamiast wyświetlać powiadomienie, aplikacja przechodziła do fragmentu ze szczegółami, który zawiera więcej informacji o klikniętej nocy.

Krok 1. Przejdź do kliknięcia

W tym kroku zamiast wyświetlać tylko powiadomienie, zmienisz lambdę odbiornika kliknięć w pliku onCreateView() elementu SleepTrackerFragment, aby przekazywać nightId do SleepTrackerViewModel i wywoływać nawigację do SleepDetailFragment.

Zdefiniuj funkcję obsługi kliknięcia:

  1. Otwórz plik SleepTrackerViewModel.kt.
  2. W obszarze SleepTrackerViewModel, pod koniec, zdefiniuj funkcję obsługi kliknięcia onSleepNightClicked().
fun onSleepNightClicked(id: Long) {

}
  1. onSleepNightClicked() wywołaj nawigację, ustawiając _navigateToSleepDetail na przekazany id klikniętej nocy snu.
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. Użyj mechanizmu _navigateToSleepDetail. Podobnie jak wcześniej zdefiniuj private MutableLiveData dla stanu nawigacji. oraz publiczny getter val.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. Określ metodę wywoływaną po zakończeniu nawigacji w aplikacji. Nazwij go onSleepDetailNavigated() i ustaw jego wartość na null.
fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

Dodaj kod, aby wywołać funkcję obsługi kliknięć:

  1. Otwórz plik SleepTrackerFragment.kt i przewiń w dół do kodu, który tworzy adapter i definiuje SleepNightListener, aby wyświetlić powiadomienie.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Dodaj poniższy kod pod powiadomieniem, aby wywołać procedurę obsługi kliknięć onSleepNighClicked()sleepTrackerViewModel po kliknięciu elementu. Przekaż w nightId, aby model widoku wiedział, którą noc snu ma pobrać. W takim przypadku pojawi się błąd, ponieważ nie zdefiniowano jeszcze zmiennej onSleepNightClicked(). Możesz zachować, wykomentować lub usunąć powiadomienie.
sleepTrackerViewModel.onSleepNightClicked(nightId)

Dodaj kod, aby obserwować kliknięcia:

  1. Otwórz plik SleepTrackerFragment.kt.
  2. onCreateView(), tuż nad deklaracją manager, dodaj kod, aby obserwować nowy navigateToSleepDetail LiveData. Gdy zmieni się navigateToSleepDetail, przejdź do SleepDetailFragment, przekazując night, a następnie wywołaj onSleepDetailNavigated(). Ponieważ robiliśmy to już w poprzednich ćwiczeniach, oto kod:
sleepTrackerViewModel.navigateToSleepDetail.observe(this, Observer { night ->
            night?.let {
              this.findNavController().navigate(
                        SleepTrackerFragmentDirections
                                .actionSleepTrackerFragmentToSleepDetailFragment(night))
               sleepTrackerViewModel.onSleepDetailNavigated()
            }
        })
  1. Uruchamiasz kod, klikasz element i … aplikacja się zawiesza.

Obsługa wartości null w adapterach powiązań:

  1. Uruchom ponownie aplikację w trybie debugowania. Kliknij element i odfiltruj logi, aby wyświetlić błędy. Wyświetli ślad stosu, który będzie zawierać informacje podobne do tych poniżej.
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item

Niestety zrzut stosu nie wskazuje, gdzie występuje ten błąd. Jedną z wad wiązania danych jest to, że może ono utrudniać debugowanie kodu. Aplikacja ulega awarii po kliknięciu elementu, a jedyny nowy kod służy do obsługi kliknięcia.

Okazuje się jednak, że dzięki temu nowemu mechanizmowi obsługi kliknięć adaptery powiązań mogą być wywoływane z wartością null dla item. W szczególności po uruchomieniu aplikacji zmienna LiveData ma wartość null, więc do każdego z adapterów musisz dodać sprawdzanie wartości null.

  1. W przypadku każdego adaptera powiązania w BindingUtils.kt zmień typ argumentu item na dopuszczający wartość null i otocz treść znakiem item?.let{...}. Na przykład adapter do sleepQualityString będzie wyglądać tak: Podobnie zmień pozostałe adaptery.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. Uruchom aplikację. Kliknij element, aby otworzyć widok szczegółów.

Projekt Android Studio: RecyclerViewClickHandler.

Aby elementy w RecyclerView reagowały na kliknięcia, dołącz do nich w ViewHolder detektory kliknięć i obsługuj kliknięcia w ViewModel.

Aby elementy w RecyclerView reagowały na kliknięcia, musisz wykonać te czynności:

  • Utwórz klasę detektora, która przyjmuje wyrażenie lambda i przypisuje je do funkcji onClick().
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  • Ustaw odbiornik kliknięć w widoku.
android:onClick="@{() -> clickListener.onClick(sleep)}"
  • Przekaż odbiornik kliknięć do konstruktora adaptera, do elementu View Holder i dodaj go do obiektu powiązania.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()
holder.bind(getItem(position)!!, clickListener)
binding.clickListener = clickListener
  • W fragmencie, który wyświetla widok cykliczny, w miejscu tworzenia adaptera zdefiniuj detektor kliknięć, przekazując do adaptera wyrażenie lambda.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
      sleepTrackerViewModel.onSleepNightClicked(nightId)
})
  • Zaimplementuj moduł obsługi kliknięć w modelu widoku. W przypadku kliknięć elementów listy zwykle powoduje to przejście do fragmentu ze szczegółami.

Kurs Udacity:

Dokumentacja dla deweloperów aplikacji na Androida:

W tej sekcji znajdziesz listę możliwych zadań domowych dla uczniów, którzy wykonują ten moduł w ramach kursu prowadzonego przez instruktora. Nauczyciel musi:

  • W razie potrzeby przypisz pracę domową.
  • Poinformuj uczniów, jak przesyłać projekty.
  • Oceń zadania domowe.

Instruktorzy mogą korzystać z tych sugestii w dowolnym zakresie i mogą zadawać inne zadania domowe, które uznają za odpowiednie.

Jeśli wykonujesz ten kurs samodzielnie, możesz użyć tych zadań domowych, aby sprawdzić swoją wiedzę.

Odpowiedz na te pytania

Pytanie 1

Załóżmy, że Twoja aplikacja zawiera RecyclerView, która wyświetla produkty na liście zakupów. Aplikacja definiuje też klasę odbiornika kliknięć:

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

Jak udostępnić ShoppingListItemListener na potrzeby powiązania danych? Wybierz jedną z opcji.

▢ W pliku układu, który zawiera element RecyclerView wyświetlający listę zakupów, dodaj zmienną <data> dla elementu ShoppingListItemListener.

▢ W pliku układu, który definiuje układ pojedynczego wiersza na liście zakupów, dodaj zmienną <data> dla ShoppingListItemListener.

▢ W klasie ShoppingListItemListener dodaj funkcję, która włączy wiązanie danych:

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

▢ W klasie ShoppingListItemListener, wewnątrz funkcji onClick(), dodaj wywołanie, aby włączyć wiązanie danych:

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

Pytanie 2

Gdzie należy dodać atrybut android:onClick, aby elementy w RecyclerView reagowały na kliknięcia? Zaznacz wszystkie pasujące opcje.

▢ W pliku układu, który wyświetla RecyclerView, dodaj go do <androidx.recyclerview.widget.RecyclerView>.

▢ Dodaj go do pliku układu elementu w wierszu. Jeśli chcesz, aby cały element był klikalny, dodaj go do widoku nadrzędnego, który zawiera elementy w wierszu.

▢ Dodaj go do pliku układu elementu w wierszu. Jeśli chcesz, aby pojedynczy znak TextView w elemencie był klikalny, dodaj go do elementu <TextView>.

▢ Zawsze dodawaj go do pliku układu dla MainActivity.

Otwórz następną lekcję: 7.5. Nagłówki w widoku RecyclerView