Android Kotlin Fundamentals 07.4: Interakcja z elementami RecyclerView

Te ćwiczenia są częścią kursu Android Kotlin Fundamentals. Skorzystaj z tego kursu, jeśli będziesz wykonywać kolejno kilka ćwiczeń z programowania. Wszystkie ćwiczenia z kursu są wymienione na stronie docelowej ćwiczeń z programowania na temat Kotlin.

Wprowadzenie

Większość aplikacji korzystających z list i siatek wyświetlających elementy umożliwia użytkownikom interakcję z nimi. W przypadku tego typu interakcji bardzo często stosuje się element z listy i widzisz jego szczegóły. Aby to osiągnąć, możesz dodać detektory kliknięć, które reagują na dotknięcia elementu przez wyświetlenie widoku szczegółowego.

W ramach tego ćwiczenia dodajesz interakcję do urządzenia RecyclerView na podstawie rozszerzonej wersji aplikacji do monitorowania snu z poprzedniej serii ćwiczeń.

Co musisz wiedzieć

  • Tworzenie podstawowego interfejsu użytkownika za pomocą aktywności, fragmentów i widoków danych.
  • Przechodzenie między fragmentami i przekazywanie danych między nimi za pomocą safeArgs.
  • Wyświetlanie modeli, wyświetlanie fabryk modeli, przekształceń oraz LiveData i ich obserwatorów.
  • Jak utworzyć bazę danych Room, utworzyć obiekt dostępu do danych (DAO) i określić encje.
  • Jak używać algorytmów baz danych i innych długotrwałych zadań.
  • Jak wdrożyć podstawowy element RecyclerView z elementem Adapter, ViewHolder i układem elementu.
  • Jak wdrożyć wiązanie danych dla RecyclerView.
  • Tworzenie i używanie adapterów wiązań do przekształcania danych.
  • Jak korzystać z GridLayoutManager.

Czego się nauczysz

  • Jak sprawić, aby elementy w RecyclerView można było klikać. Zaimplementuj detektor kliknięć, by przejść do widoku szczegółowego po kliknięciu produktu.

Jakie zadania wykonasz:

  • Skorzystaj z rozszerzonej wersji aplikacji TrackMySleepQuality z poprzedniego ćwiczenia z tej serii.
  • Dodaj detektor kliknięć do listy i zacznij nasłuchiwać interakcji użytkownika. Kliknięcie elementu listy powoduje przejście do fragmentu ze szczegółowymi informacjami o klikniętym elemencie. Kod początkowy zawiera zarówno fragment szczegółów, jak i element nawigacyjny.

Pierwsza aplikacja do monitorowania snu ma 2 ekrany reprezentowane przez fragmenty, co widać na rysunku poniżej.

Na pierwszym ekranie po lewej stronie znajdują się przyciski rozpoczynające i zatrzymujące śledzenie. Na ekranie pojawią się niektóre dane dotyczące snu użytkownika. Kliknięcie przycisku Wyczyść powoduje trwałe usunięcie wszystkich danych użytkownika zbieranych przez aplikację. Na drugim ekranie (po prawej) możesz wybrać ocenę jakości snu.

Ta aplikacja ma uproszczoną architekturę z kontrolerem interfejsu, modelem wyświetlania i LiveData, a także bazę danych Room do przechowywania danych o śnie.

W ramach tego ćwiczenia możesz dodać odpowiedź, gdy użytkownik kliknie element w siatce. Pojawi się ekran szczegółów podobny do tego poniżej. Kod tego ekranu (fragment, model widoku i nawigacja) jest dostarczany z aplikacją uruchamiającą, a Ty wdrożysz mechanizm obsługi kliknięć.

Krok 1. Pobierz aplikację startową

  1. Pobierz z GitHuba kod RecyclerViewClickHandler-Starter i otwórz projekt w Android Studio.
  2. Utwórz i uruchom aplikację startową monitorowania snu.

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

Jeśli chcesz wykonać tę czynność z poziomu aplikacji startowej udostępnionej w GitHubie, przejdź do następnego kroku.

Jeśli chcesz nadal korzystać z własnej aplikacji do monitorowania snu, która została opracowana w poprzednim ćwiczeniu z programowania, wykonaj poniższe instrukcje, aby zaktualizować dotychczasową aplikację i umieścić w niej kod fragmentu ekranu szczegółów.

  1. Nawet jeśli nadal korzystasz z obecnej aplikacji, pobierz z GitHuba kod RecyclerViewClickHandler-Starter, który pozwoli Ci skopiować pliki.
  2. Skopiuj wszystkie pliki w pakiecie sleepdetail.
  3. W folderze layout skopiuj plik fragment_sleep_detail.xml.
  4. Skopiuj zaktualizowaną treść elementu navigation.xml, która zawiera nawigację dla sleep_detail_fragment.
  5. W pakiecie database w elemencie 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 res/values/strings dodaj ten zasób ciągu znaków:
<string name="close">Close</string>
  1. Wyczyść i odbuduj aplikację, aby zaktualizować wiązanie danych.

Krok 2. Sprawdź kod na ekranie szczegółów snu

W tym ćwiczeniu implementujesz moduł do obsługi kliknięć, który przekierowuje Cię do fragmentu zawierającego szczegółowe informacje o klikniętej nocy snu. Twój kod startowy zawiera już fragment i wykres nawigacji dla: SleepDetailFragment, bo zawiera on sporo kodu, a fragmenty i elementy nawigacyjne nie są częścią tego ćwiczenia. Zapoznaj się z tym kodem:

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

  2. W pakiecie sleepdetail otwórz i sprawdź kod SleepDetailViewModel. Ten model widoku korzysta z klucza SleepNight i DAO w konstruktorze.

    Treść klasy zawiera kod, który pozwala pobrać SleepNight dla danego klucza oraz zmienną navigateToSleepTracker, która pozwala powrócić do elementu SleepTrackerFragment po naciśnięciu przycisku Zamknij.

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

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

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

  5. Sprawdź folder fragment_sleep_detail.xml w folderze układu. Zwróć uwagę na zmienną sleepDetailViewModel w tagu <data>, która powoduje wyświetlanie danych w każdym widoku danych w modelu widoku danych.

    Układ zawiera wartość ConstraintLayout zawierającą ImageView w przypadku jakości snu, TextView jako ocenę jakości, TextView w przypadku długości snu oraz Button aby zamknąć fragment szczegółów.

  6. Otwórz plik navigation.xml. sleep_tracker_fragment to nowe działanie sleep_detail_fragment.

    Nowe działanie action_sleep_tracker_fragment_to_sleepDetailFragment to przejście z elementu śledzącego sen do ekranu z informacjami.

W tym zadaniu aktualizujesz RecyclerView, aby reagować na dotknięcia ekranu, pokazując ekran szczegółów klikniętego elementu.

Odbieranie i obsługa kliknięć to dwuczęściowe zadanie. Najpierw musisz posłuchać i otrzymać kliknięcie, by określić, który element został kliknięty. Następnie musisz zareagować na kliknięcie.

Gdzie najlepiej dodać detektor kliknięć do tej aplikacji?

  • SleepTrackerFragment hostuje wiele widoków, więc słuchanie zdarzeń kliknięć na poziomie fragmentu nie powie, który element został kliknięty. Nie dowiesz się nawet, czy chodzi o kliknięty element czy inny element interfejsu.
  • Na poziomie RecyclerView trudno jest stwierdzić, który element na liście kliknął użytkownik.
  • Najlepiej jest umieścić informacje o 1 klikniętym obiekcie w obiekcie ViewHolder, bo reprezentuje on 1 element listy.

ViewHolder to doskonałe miejsce, by poznać liczbę kliknięć. Nie zawsze jest to jednak właściwe miejsce. Jakie jest więc najlepsze miejsce na obsługę kliknięć?

  • Element Adapter wyświetla elementy danych w widokach danych, więc możesz obsłużyć kliknięcia w adapcie. Jednak zadaniem adaptera jest dostosowywanie danych do wyświetlania, a nie racjonalizowanie logiki aplikacji.
  • Kliknięcia zwykle należy wykonywać w ViewModel, bo ViewModel ma dostęp do danych i logiki określającej, co ma się stać w odpowiedzi na kliknięcie.

Krok 1. Utwórz detektor kliknięć i wyzwalaj go na podstawie układu elementu

  1. W folderze sleeptracker otwórz plik SleepNightAdapter.kt.
  2. Na końcu pliku utwórz nową klasę słuchacza SleepNightListener.
class SleepNightListener() {
    
}
  1. W ramach klasy SleepNightListener dodaj funkcję onClick(). Kliknięcie widoku listy z widokiem listy wywołuje funkcję onClick(). Właściwość android:onClick tego widoku ustawisz później na tę funkcję.
class SleepNightListener() {
    fun onClick() = 
}
  1. Dodaj argument funkcji night typu SleepNight do onClick(). Widok określa, który element wyświetla się na ekranie, i takie informacje muszą zostać przekazane do obsługi kliknięcia.
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. Aby określić, co robi onClick(), zwróć wywołanie zwrotne clickListener w konstruktorze SleepNightListener i przypisz je do onClick().

    Nadanie lambdzie nazwy kliknięcia clickListener pomaga śledzić to zdarzenie w miarę przechodzenia między zajęciami. Wywołanie zwrotne clickListener potrzebuje dostępu tylko do night.nightId, by uzyskać dostęp do danych z bazy danych. Ukończone zajęcia w SleepNightListener powinny wyglądać tak jak poniżej.
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ą wiązania danych. Nadaj nowemu <variable> właściwości name o wartości clickListener. Ustaw wartość type na pełną nazwę klasy com.example.android.trackmysleepquality.sleeptracker.SleepNightListener, jak pokazano poniżej. Funkcja onClick() jest teraz dostępna w SleepNightListener z tego układu.
<variable
            name="clickListener"
            type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. Aby śledzić kliknięcia dowolnej części tego elementu listy, dodaj atrybut android:onClick do ConstraintLayout.

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

Krok 2. Przekaż detektor kliknięć do użytkownika widoku i obiektu wiązania

  1. Otwórz SleepNightAdapter.kt.
  2. Zmodyfikuj konstruktor klasy SleepNightAdapter, aby otrzymać val clickListener: SleepNightListener. Gdy adapter wiąże się z elementem ViewHolder, musi go przekazać do tego detektora kliknięć.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. W onBindViewHolder() zaktualizuj wywołanie do holder.bind(), by przesłać też detektor kliknięć do ViewHolder. Błąd kompilatora wynika z dodania parametru do wywołania funkcji.
holder.bind(getItem(position)!!, clickListener)
  1. Dodaj parametr clickListener do elementu bind(). Aby to zrobić, umieść kursor na błędzie i naciśnij Alt+Enter (Windows) lub Option+Enter (Mac) przy błędzie, tak jak widać na zrzucie ekranu poniżej.

  1. W obrębie funkcji ViewHolder przypisz w obrębie funkcji bind() detektor kliknięć do obiektu binding. Pojawia się błąd, ponieważ musisz zaktualizować obiekt powiązania.
binding.clickListener = clickListener
  1. Aby zaktualizować wiązanie danych, wyczyść i odbuduj projekt. Może być też konieczne unieważnienie pamięci podręcznej. Masz więc detektor kliknięć utworzony w konstruktorze adaptera i przekazujesz go do właściciela widoku oraz do obiektu wiązania.

Krok 3. Wyświetl toast po dotknięciu elementu

Masz już kod służący do rejestrowania kliknięć, ale nie masz jeszcze zaimplementowanego efektu kliknięcia elementu listy. Najprostsza odpowiedź to wyświetlanie tego komunikatu w chwili kliknięcia nightId. Sprawdza to, czy po kliknięciu elementu listy przechwytywany jest prawidłowy element nightId i czy jest on przekazywany.

  1. Otwórz plik SleepTrackerFragment.kt.
  2. W onCreateView() znajdź zmienną adapter. Zwróć uwagę na błąd, ponieważ oczekuje on parametru detektor kliknięć.
  3. Określ detektor kliknięć, przesyłając lambdę do SleepNightAdapter. W tej prostej lambdzie wyświetla się tost na nightId, jak widać 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 wybrany element i sprawdź, czy wyświetla się prawidłowy komunikat nightId. Elementy mają rosnące wartości nightId, a aplikacja wyświetla się w ostatnich dniach od razu, dlatego na dole listy znajduje się element z najniższą wartością nightId.

W tym zadaniu zmienisz zachowanie po kliknięciu elementu w RecyclerView, tak więc zamiast wyświetlania tosty, aplikacja przejdzie do fragmentu z informacjami na temat klikniętej nocy.

Krok 1. Przejdź po kliknięciu

W tym kroku zamiast wyświetlać toast, zmieniasz lambdę detektora kliknięć w onCreateView() z SleepTrackerFragment, by przekazywać nightId do SleepTrackerViewModel i uruchomić nawigację do SleepDetailFragment.

Zdefiniuj funkcję modułu obsługi kliknięć:

  1. Otwórz plik SleepTrackerViewModel.kt.
  2. Na końcu parametru SleepTrackerViewModel zdefiniuj funkcję modułu obsługi kliknięć onSleepNightClicked().
fun onSleepNightClicked(id: Long) {

}
  1. W sekcji onSleepNightClicked() uruchom nawigację, ustawiając wartość _navigateToSleepDetail na id klikniętej nocy snu.
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. Użyj mechanizmu _navigateToSleepDetail. Tak jak poprzednio, ustaw private MutableLiveData jako stan nawigacji. I publiczny publiczny interfejs val, który możesz pobrać.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. Określ metodę wywołania metody po zakończeniu nawigacji w aplikacji. Nazwij go onSleepDetailNavigated() i ustaw jego wartość na null.
fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

Dodaj kod wywołujący moduł obsługi kliknięć:

  1. Otwórz plik SleepTrackerFragment.kt i przewiń w dół do kodu, który tworzy adapter i określa SleepNightListener, aby tosta.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Dodaj ten kod poniżej, aby wywołać moduł obsługi kliknięć: onSleepNighClicked() w elemencie sleepTrackerViewModel po kliknięciu elementu. Migaj model nightId, by model widoku wiedział, ile śpisz. Zdarza się błąd, ponieważ nie zdefiniowano jeszcze onSleepNightClicked(). Zachowaj, skomentuj lub usuń toast.
sleepTrackerViewModel.onSleepNightClicked(nightId)

Dodaj kod, aby obserwować kliknięcia:

  1. Otwórz plik SleepTrackerFragment.kt.
  2. W narzędziu onCreateView(), tuż nad deklaracją tagu manager, dodaj kod, aby obserwować nowy navigateToSleepDetail LiveData. Gdy navigateToSleepDetail się zmieni, przejdź do SleepDetailFragment, przejeżdżając przez night, a następnie wywołaj onSleepDetailNavigated(). Ponieważ już to robiłeś podczas poprzedniego ćwiczenia z programowania, oto kod:
sleepTrackerViewModel.navigateToSleepDetail.observe(this, Observer { night ->
            night?.let {
              this.findNavController().navigate(
                        SleepTrackerFragmentDirections
                                .actionSleepTrackerFragmentToSleepDetailFragment(night))
               sleepTrackerViewModel.onSleepDetailNavigated()
            }
        })
  1. Uruchom kod, kliknij element i aplikacja ulegnie awarii.

Obsługa pustych wartości w adapterach wiązania:

  1. Uruchom ponownie aplikację w trybie debugowania. Kliknij element i przefiltruj dzienniki, by wyświetlić błędy. Wyświetli się zrzut stosu z tym przykładem:
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 wyraźnie, gdzie wystąpił ten błąd. Wadą funkcji wiązania danych jest to, że może ona utrudniać debugowanie kodu. Aplikacja ulega awarii, kiedy klikniesz element. Jedynym nowym kodem do obsługi kliknięcia.

Okazuje się jednak, że dzięki nowemu mechanizmowi obsługi kliknięć możliwe jest wywołanie adaptera wiązania z wartością null dla elementu item. W szczególności po uruchomieniu aplikacji LiveData zaczyna się od null, więc musisz dodać dowolne pakiety do każdego adaptera.

  1. W BindingUtils.kt dla każdego adaptera wiązania zmień typ argumentu item na wartość null i ujmij treść w item?.let{...}. Tak będzie wyglądał na przykład adapter sleepQualityString. Zmień też pozostałe adaptery.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. Uruchom aplikację. Kliknij element, a otworzy się widok szczegółowy.

Projekt na Android Studio: RecyclerViewClickHandler.

Aby elementy w elemencie RecyclerView odpowiadały na kliknięcia, dołącz detektory kliknięć do listy elementów w elemencie ViewHolder i obsługuj kliknięcia w narzędziu ViewModel.

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

  • Utwórz klasę słuchacza, która wykorzystuje lambdę i przypisze ją do funkcji onClick().
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  • Ustaw w widoku detektor kliknięć.
android:onClick="@{() -> clickListener.onClick(sleep)}"
  • Przekaż detektor kliknięć do konstruktora adaptera, do uchwytu widoku 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 kodu zawierającym widok recyklingu, w którym tworzysz adapter, zdefiniuj detektor kliknięć, przesyłając lambdę do adaptera.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
      sleepTrackerViewModel.onSleepNightClicked(nightId)
})
  • Zaimplementuj moduł obsługi kliknięć w modelu widoku danych. W przypadku kliknięć elementów na liście zwykle powoduje to przejście do fragmentu szczegółowego.

Kurs Udacity:

Dokumentacja dla programistów Androida:

Ta sekcja zawiera listę możliwych zadań domowych dla uczniów, którzy pracują w ramach tego ćwiczenia w ramach kursu prowadzonego przez nauczyciela. To nauczyciel może wykonać te czynności:

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

Nauczyciele mogą wykorzystać te sugestie tak długo, jak chcą lub chcą, i mogą przypisać dowolne zadanie domowe.

Jeśli samodzielnie wykonujesz te ćwiczenia z programowania, możesz sprawdzić swoją wiedzę w tych zadaniach domowych.

Odpowiedz na te pytania

Pytanie 1

Załóżmy, że Twoja aplikacja zawiera element RecyclerView, który wyświetla produkty na liście zakupów. W aplikacji jest też definicja klasy słuchacza kliknięć:

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

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

▢ W pliku układu zawierającym RecyclerView, który wyświetla listę zakupów, dodaj zmienną <data> dla: ShoppingListItemListener.

▢ W pliku układu definiującym pojedynczy wiersz 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 w wywołaniu funkcji onClick() dodaj wywołanie włączające dane:

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

Pytanie 2

Gdzie dodajesz atrybut android:onClick, by elementy w aplikacji RecyclerView odpowiadały na kliknięcia? Wybierz wszystkie pasujące odpowiedzi.

▢ W pliku układu zawierającym plik RecyclerView dodaj go do <androidx.recyclerview.widget.RecyclerView>

▢ Dodaj do pliku układu elementu w wierszu. Jeśli chcesz kliknąć cały element, dodaj go do widoku nadrzędnego zawierającego elementy w wierszu.

▢ Dodaj do pliku układu elementu w wierszu. Jeśli chcesz, aby pojedynczy element TextView można było kliknąć, dodaj go do elementu <TextView>.

▢ Zawsze dodawaj plik szablonu dla MainActivity.

Rozpocznij następną lekcję: 7.5: Nagłówki w aplikacji RecyclerView