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 elementemAdapter
,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ą
- Pobierz z GitHuba kod RecyclerViewClickHandler-Starter i otwórz projekt w Android Studio.
- 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.
- Nawet jeśli nadal korzystasz z obecnej aplikacji, pobierz z GitHuba kod RecyclerViewClickHandler-Starter, który pozwoli Ci skopiować pliki.
- Skopiuj wszystkie pliki w pakiecie
sleepdetail
. - W folderze
layout
skopiuj plikfragment_sleep_detail.xml
. - Skopiuj zaktualizowaną treść elementu
navigation.xml
, która zawiera nawigację dlasleep_detail_fragment
. - W pakiecie
database
w elemencieSleepDatabaseDao
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>
- W
res/values/strings
dodaj ten zasób ciągu znaków:
<string name="close">Close</string>
- 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:
- 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. - W pakiecie
sleepdetail
otwórz i sprawdź kodSleepDetailViewModel
. Ten model widoku korzysta z kluczaSleepNight
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 elementuSleepTrackerFragment
po naciśnięciu przycisku Zamknij.
FunkcjagetNightWithId()
zwraca wartośćLiveData<SleepNight>
i jest określona w elemencieSleepDatabaseDao
(w pakieciedatabase
). - W pakiecie
sleepdetail
otwórz i sprawdź kodSleepDetailFragment
. Zwróć uwagę na konfigurację wiązania danych, model widoku i obserwatora na potrzeby nawigacji. - W pakiecie
sleepdetail
otwórz i sprawdź kod dla pakietuSleepDetailViewModelFactory
. - 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 orazButton
aby zamknąć fragment szczegółów. - Otwórz plik
navigation.xml
.sleep_tracker_fragment
to nowe działaniesleep_detail_fragment
.
Nowe działanieaction_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
, boViewModel
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
- W folderze
sleeptracker
otwórz plik SleepNightAdapter.kt. - Na końcu pliku utwórz nową klasę słuchacza
SleepNightListener
.
class SleepNightListener() {
}
- 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() =
}
- Dodaj argument funkcji
night
typuSleepNight
doonClick()
. 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) =
}
- Aby określić, co robi
onClick()
, zwróć wywołanie zwrotneclickListener
w konstruktorzeSleepNightListener
i przypisz je doonClick()
.
Nadanie lambdzie nazwy kliknięciaclickListener
pomaga śledzić to zdarzenie w miarę przechodzenia między zajęciami. Wywołanie zwrotneclickListener
potrzebuje dostępu tylko donight.nightId
, by uzyskać dostęp do danych z bazy danych. Ukończone zajęcia wSleepNightListener
powinny wyglądać tak jak poniżej.
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
fun onClick(night: SleepNight) = clickListener(night.nightId)
}
- Otwórz plik list_item_sleep_night.xml.
- W bloku
data
dodaj nową zmienną, aby udostępnić klasęSleepNightListener
za pomocą wiązania danych. Nadaj nowemu<variable>
właściwościname
o wartościclickListener.
Ustaw wartośćtype
na pełną nazwę klasycom.example.android.trackmysleepquality.sleeptracker.SleepNightListener
, jak pokazano poniżej. FunkcjaonClick()
jest teraz dostępna wSleepNightListener
z tego układu.
<variable
name="clickListener"
type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
- Aby śledzić kliknięcia dowolnej części tego elementu listy, dodaj atrybut
android:onClick
doConstraintLayout
.
Ustaw atrybut naclickListener: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
- Otwórz SleepNightAdapter.kt.
- Zmodyfikuj konstruktor klasy
SleepNightAdapter
, aby otrzymaćval clickListener: SleepNightListener
. Gdy adapter wiąże się z elementemViewHolder
, musi go przekazać do tego detektora kliknięć.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
- W
onBindViewHolder()
zaktualizuj wywołanie doholder.bind()
, by przesłać też detektor kliknięć doViewHolder
. Błąd kompilatora wynika z dodania parametru do wywołania funkcji.
holder.bind(getItem(position)!!, clickListener)
- Dodaj parametr
clickListener
do elementubind()
. Aby to zrobić, umieść kursor na błędzie i naciśnijAlt+Enter
(Windows) lubOption+Enter
(Mac) przy błędzie, tak jak widać na zrzucie ekranu poniżej.
- W obrębie funkcji
ViewHolder
przypisz w obrębie funkcjibind()
detektor kliknięć do obiektubinding
. Pojawia się błąd, ponieważ musisz zaktualizować obiekt powiązania.
binding.clickListener = clickListener
- 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.
- Otwórz plik SleepTrackerFragment.kt.
- W
onCreateView()
znajdź zmiennąadapter
. Zwróć uwagę na błąd, ponieważ oczekuje on parametru detektor kliknięć. - Określ detektor kliknięć, przesyłając lambdę do
SleepNightAdapter
. W tej prostej lambdzie wyświetla się tost nanightId
, 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()
})
- Uruchom aplikację, kliknij wybrany element i sprawdź, czy wyświetla się prawidłowy komunikat
nightId
. Elementy mają rosnące wartościnightId
, 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ęć:
- Otwórz plik SleepTrackerViewModel.kt.
- Na końcu parametru
SleepTrackerViewModel
zdefiniuj funkcję modułu obsługi kliknięćonSleepNightClicked()
.
fun onSleepNightClicked(id: Long) {
}
- W sekcji
onSleepNightClicked()
uruchom nawigację, ustawiając wartość_navigateToSleepDetail
naid
klikniętej nocy snu.
fun onSleepNightClicked(id: Long) {
_navigateToSleepDetail.value = id
}
- Użyj mechanizmu
_navigateToSleepDetail
. Tak jak poprzednio, ustawprivate MutableLiveData
jako stan nawigacji. I publiczny publiczny interfejsval
, który możesz pobrać.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
get() = _navigateToSleepDetail
- Określ metodę wywołania metody po zakończeniu nawigacji w aplikacji. Nazwij go
onSleepDetailNavigated()
i ustaw jego wartość nanull
.
fun onSleepDetailNavigated() {
_navigateToSleepDetail.value = null
}
Dodaj kod wywołujący moduł obsługi kliknięć:
- 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()
})
- Dodaj ten kod poniżej, aby wywołać moduł obsługi kliknięć:
onSleepNighClicked()
w elemenciesleepTrackerViewModel
po kliknięciu elementu. Migaj modelnightId
, by model widoku wiedział, ile śpisz. Zdarza się błąd, ponieważ nie zdefiniowano jeszczeonSleepNightClicked()
. Zachowaj, skomentuj lub usuń toast.
sleepTrackerViewModel.onSleepNightClicked(nightId)
Dodaj kod, aby obserwować kliknięcia:
- Otwórz plik SleepTrackerFragment.kt.
- W narzędziu
onCreateView()
, tuż nad deklaracją tagumanager
, dodaj kod, aby obserwować nowynavigateToSleepDetail
LiveData
. GdynavigateToSleepDetail
się zmieni, przejdź doSleepDetailFragment
, przejeżdżając przeznight
, a następnie wywołajonSleepDetailNavigated()
. 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()
}
})
- Uruchom kod, kliknij element i aplikacja ulegnie awarii.
Obsługa pustych wartości w adapterach wiązania:
- 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.
- W
BindingUtils.kt
dla każdego adaptera wiązania zmień typ argumentuitem
na wartość null i ujmij treść witem?.let{...}
. Tak będzie wyglądał na przykład adaptersleepQualityString
. Zmień też pozostałe adaptery.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
item?.let {
text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
}
- 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ę: