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
Z tego samouczka dowiesz się, jak dodać nagłówek, który obejmuje całą szerokość listy wyświetlanej w RecyclerView
. Będziesz pracować nad aplikacją do śledzenia snu z poprzednich ćwiczeń.
Co warto wiedzieć
- Jak utworzyć podstawowy interfejs użytkownika za pomocą aktywności, fragmentów i widoków.
- Jak przechodzić między fragmentami i jak używać
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
, utworzyć DAO i zdefiniować encje. - Jak używać korutyn do interakcji z bazą danych i innych długotrwałych zadań.
- Jak wdrożyć podstawowy
RecyclerView
zAdapter
,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
. - Jak rejestrować i obsługiwać kliknięcia elementów w
RecyclerView.
Czego się nauczysz
- Jak używać więcej niż jednego przycisku
ViewHolder
z przyciskiemRecyclerView
, aby dodawać elementy o innym układzie. W szczególności jak użyć drugiego taguViewHolder
, aby dodać nagłówek nad elementami wyświetlanymi w taguRecyclerView
.
Jakie zadania wykonasz
- Skorzystaj z aplikacji TrackMySleepQuality z poprzedniego samouczka z tej serii.
- Dodaj nagłówek, który będzie obejmował całą szerokość ekranu nad nocami snu wyświetlanymi w sekcji
RecyclerView
.
Aplikacja do śledzenia snu, od której zaczniesz, ma 3 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środku, służy do wyboru oceny jakości snu. Trzeci ekran to widok szczegółowy, który otwiera się, gdy użytkownik kliknie element w siatce.
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 nagłówek do wyświetlanej siatki elementów. Ostateczny ekran główny będzie wyglądać tak:
W tym laboratorium dowiesz się, jak w RecyclerView
umieszczać elementy korzystające z różnych układów. Częstym przykładem jest umieszczenie nagłówków na liście lub w siatce. Lista może mieć jeden nagłówek opisujący zawartość elementu. Lista może też zawierać wiele nagłówków, które grupują i rozdzielają elementy na jednej liście.
RecyclerView
nie ma żadnych informacji o Twoich danych ani o tym, jaki układ ma każdy element. LayoutManager
rozmieszcza elementy na ekranie, ale adapter dostosowuje dane do wyświetlania i przekazuje uchwyty widoku do RecyclerView
. Dlatego dodasz kod, który tworzy nagłówki w adapterze.
Dwa sposoby dodawania nagłówków
W RecyclerView
każdy element listy odpowiada numerowi indeksu zaczynającemu się od 0. Na przykład:
[Actual Data] -> [Adapter Views]
[0: SleepNight] -> [0: SleepNight]
[1: SleepNight] -> [1: SleepNight]
[2: SleepNight] -> [2: SleepNight]
Jednym ze sposobów dodania nagłówków do listy jest zmodyfikowanie adaptera, aby używał innego ViewHolder
przez sprawdzanie indeksów, w których ma się wyświetlać nagłówek. Adapter
będzie odpowiedzialny za śledzenie nagłówka. Jeśli na przykład chcesz wyświetlić nagłówek u góry tabeli, musisz zwrócić inny ViewHolder
dla nagłówka podczas układania elementu indeksowanego od zera. Wszystkie pozostałe elementy zostaną zmapowane z przesunięciem nagłówka, jak pokazano poniżej.
[Actual Data] -> [Adapter Views]
[0: Header]
[0: SleepNight] -> [1: SleepNight]
[1: SleepNight] -> [2: SleepNight]
[2: SleepNight] -> [3: SleepNight.
Innym sposobem dodawania nagłówków jest zmodyfikowanie zbioru danych, na którym opiera się siatka danych. Wszystkie dane, które mają być wyświetlane, są przechowywane na liście, więc możesz ją zmodyfikować, aby uwzględnić elementy reprezentujące nagłówek. Jest to nieco prostsze, ale wymaga zastanowienia się nad tym, jak projektujesz obiekty, aby można było połączyć różne typy elementów w jedną listę. W takim przypadku adapter będzie wyświetlać przekazane do niego elementy. Element na pozycji 0 to nagłówek, a element na pozycji 1 to SleepNight
, co odpowiada temu, co jest wyświetlane na ekranie.
[Actual Data] -> [Adapter Views]
[0: Header] -> [0: Header]
[1: SleepNight] -> [1: SleepNight]
[2: SleepNight] -> [2: SleepNight]
[3: SleepNight] -> [3: SleepNight]
Każda metodologia ma zalety i wady. Zmiana zbioru danych nie powoduje większych zmian w pozostałej części kodu adaptera. Logikę nagłówka możesz dodać, manipulując listą danych. Z drugiej strony użycie innego ViewHolder
przez sprawdzenie indeksów nagłówków daje większą swobodę w układzie nagłówka. Umożliwia też dostosowywanie danych do widoku bez modyfikowania danych źródłowych.
W tym ćwiczeniu zaktualizujesz RecyclerView
, aby wyświetlać nagłówek na początku listy. W takim przypadku aplikacja będzie używać innego znaku ViewHolder
w nagłówku niż w elementach danych. Aplikacja sprawdzi indeks listy, aby określić, którego ViewHolder
użyć.
Krok 1. Utwórz klasę DataItem
Aby wyodrębnić typ elementu i umożliwić adapterowi obsługę tylko „elementów”, możesz utworzyć klasę przechowującą dane, która reprezentuje SleepNight
lub Header
. Twój zbiór danych będzie wtedy listą elementów podmiotu przechowującego dane.
Możesz pobrać aplikację startową z GitHuba lub nadal używać aplikacji SleepTracker utworzonej w poprzednim laboratorium.
- Pobierz kod RecyclerViewHeaders-Starter z GitHuba. Katalog RecyclerViewHeaders-Starter zawiera wersję początkową aplikacji SleepTracker potrzebną do tego ćwiczenia. Możesz też kontynuować pracę nad ukończoną aplikacją z poprzedniego laboratorium, jeśli wolisz.
- Otwórz plik SleepNightAdapter.kt.
- Pod klasą
SleepNightListener
na najwyższym poziomie zdefiniuj klasęsealed
o nazwieDataItem
, która reprezentuje element danych.
Klasasealed
definiuje typ zamknięty, co oznacza, że wszystkie podklasyDataItem
muszą być zdefiniowane w tym pliku. Dzięki temu kompilator zna liczbę podklas. Inna część kodu nie może zdefiniować nowego typuDataItem
, który mógłby uszkodzić adapter.
sealed class DataItem {
}
- W treści klasy
DataItem
zdefiniuj 2 klasy reprezentujące różne typy elementów danych. Pierwszy toSleepNightItem
, który jest otoczkąSleepNight
, więc przyjmuje pojedynczą wartość o nazwiesleepNight
. Aby uczynić ją częścią klasy zamkniętej, rozszerz ją oDataItem
.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- Druga klasa to
Header
, która reprezentuje nagłówek. Ponieważ nagłówek nie zawiera rzeczywistych danych, możesz zadeklarować go jakoobject
. Oznacza to, że zawsze będzie tylko jedno wystąpienie elementuHeader
. Ponownie rozszerz go oDataItem
.
object Header: DataItem()
- Wewnątrz elementu
DataItem
na poziomie zajęć zdefiniuj właściwośćabstract
Long
o nazwieid
. Gdy adapter używa parametruDiffUtil
, aby określić, czy i jak zmienił się element, parametrDiffItemCallback
musi znać identyfikator każdego elementu. Pojawi się błąd, ponieważSleepNightItem
iHeader
muszą zastąpić właściwość abstrakcyjnąid
.
abstract val id: Long
- W
SleepNightItem
zastąpid
, aby zwrócićnightId
.
override val id = sleepNight.nightId
- W
Header
zastąpid
, aby zwrócićLong.MIN_VALUE
, czyli bardzo małą liczbę (dosłownie -2 do potęgi 63). Dlatego nigdy nie będzie on kolidować z żadnym istniejącymnightId
.
override val id = Long.MIN_VALUE
- Gotowy kod powinien wyglądać tak, a aplikacja powinna się kompilować bez błędów.
sealed class DataItem {
abstract val id: Long
data class SleepNightItem(val sleepNight: SleepNight): DataItem() {
override val id = sleepNight.nightId
}
object Header: DataItem() {
override val id = Long.MIN_VALUE
}
}
Krok 2. Utwórz element ViewHolder dla nagłówka
- Utwórz układ nagłówka w nowym pliku zasobu układu o nazwie header.xml , który wyświetla
TextView
. Nie ma w tym nic ekscytującego, więc podaję kod.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Sleep Results"
android:padding="8dp" />
- Wyodrębnij
"Sleep Results"
do zasobu tekstowego i nadaj mu nazwęheader_text
.
<string name="header_text">Sleep Results</string>
- W pliku SleepNightAdapter.kt wewnątrz funkcji
SleepNightAdapter
, powyżej klasyViewHolder
, utwórz nową klasęTextViewHolder
. Ta klasa rozszerza układ textview.xml i zwraca instancjęTextViewHolder
. Ponieważ już to robiliśmy, podaję kod. Musisz zaimportowaćView
iR
:
class TextViewHolder(view: View): RecyclerView.ViewHolder(view) {
companion object {
fun from(parent: ViewGroup): TextViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater.inflate(R.layout.header, parent, false)
return TextViewHolder(view)
}
}
}
Krok 3. Zaktualizuj SleepNightAdapter
Następnie musisz zaktualizować deklarację dotyczącą SleepNightAdapter
. Zamiast obsługiwać tylko jeden typ ViewHolder
, musi być w stanie używać dowolnego typu widoku.
Określ typy produktów
- W
SleepNightAdapter.kt
na najwyższym poziomie, poniżej instrukcjiimport
i powyżejSleepNightAdapter
, zdefiniuj 2 stałe dla typów widoków.RecyclerView
musi rozróżniać typ widoku każdego elementu, aby móc prawidłowo przypisać do niego uchwyt widoku.
private val ITEM_VIEW_TYPE_HEADER = 0
private val ITEM_VIEW_TYPE_ITEM = 1
- W
SleepNightAdapter
utwórz funkcję, która zastąpigetItemViewType()
, aby zwracać odpowiednią stałą nagłówka lub elementu w zależności od typu bieżącego elementu.
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DataItem.Header -> ITEM_VIEW_TYPE_HEADER
is DataItem.SleepNightItem -> ITEM_VIEW_TYPE_ITEM
}
}
Aktualizowanie definicji SleepNightAdapter
- W definicji
SleepNightAdapter
zmień pierwszy argument funkcjiListAdapter
zSleepNight
naDataItem
. - W definicji
SleepNightAdapter
zmień drugi argument ogólny dla funkcjiListAdapter
zSleepNightAdapter.ViewHolder
naRecyclerView.ViewHolder
. Pojawią się błędy dotyczące niezbędnych aktualizacji, a nagłówek zajęć powinien wyglądać jak poniżej.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {
Aktualizacja metody onCreateViewHolder()
- Zmień sygnaturę funkcji
onCreateViewHolder()
, aby zwracała wartośćRecyclerView.ViewHolder
.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
- Rozwiń implementację metody
onCreateViewHolder()
, aby testować i zwracać odpowiedni uchwyt widoku dla każdego typu elementu. Zaktualizowana metoda powinna wyglądać tak, jak pokazano poniżej.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_HEADER -> TextViewHolder.from(parent)
ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
else -> throw ClassCastException("Unknown viewType ${viewType}")
}
}
Aktualizacja metody onBindViewHolder()
- Zmień typ parametru
onBindViewHolder()
zViewHolder
naRecyclerView.ViewHolder
.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
- Dodaj warunek, aby przypisywać dane do właściciela widoku tylko wtedy, gdy jest on
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
- Rzutuj typ obiektu zwracany przez
getItem()
naDataItem.SleepNightItem
. Gotowa funkcjaonBindViewHolder()
powinna wyglądać tak.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
val nightItem = getItem(position) as DataItem.SleepNightItem
holder.bind(nightItem.sleepNight, clickListener)
}
}
}
Aktualizowanie wywołań zwrotnych diffUtil
- Zmień metody w
SleepNightDiffCallback
, aby używać nowej klasyDataItem
zamiastSleepNight
. Wyłącz ostrzeżenie narzędzia lint, jak pokazano w kodzie poniżej.
class SleepNightDiffCallback : DiffUtil.ItemCallback<DataItem>() {
override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem.id == newItem.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem == newItem
}
}
Dodawanie i przesyłanie nagłówka
- Wewnątrz
SleepNightAdapter
, podonCreateViewHolder()
, zdefiniuj funkcjęaddHeaderAndSubmitList()
, jak pokazano poniżej. Ta funkcja przyjmuje listęSleepNight
. Zamiast używać funkcjisubmitList()
udostępnianej przezListAdapter
do przesyłania listy, użyjesz tej funkcji, aby dodać nagłówek, a następnie przesłać listę.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
- Wewnątrz
addHeaderAndSubmitList()
, jeśli przekazana lista tonull
, zwróć tylko nagłówek. W przeciwnym razie dołącz nagłówek na początku listy, a następnie prześlij listę.
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
submitList(items)
- Otwórz plik SleepTrackerFragment.kt i zmień wywołanie funkcji
submitList()
naaddHeaderAndSubmitList()
.
- Uruchom aplikację i sprawdź, jak nagłówek wyświetla się jako pierwszy element na liście elementów snu.
W tej aplikacji trzeba rozwiązać 2 problemy. Jeden z nich jest widoczny, a drugi nie.
- Nagłówek pojawia się w lewym górnym rogu i nie jest łatwo rozpoznawalny.
- W przypadku krótkiej listy z jednym nagłówkiem nie ma to większego znaczenia, ale nie należy manipulować listą w
addHeaderAndSubmitList()
w wątku interfejsu. Wyobraź sobie listę z setkami elementów, wieloma nagłówkami i logiką decydującą o tym, gdzie należy wstawić poszczególne elementy. Ta praca należy do korutyny.
Zmień addHeaderAndSubmitList()
, aby używać korutyn:
- Na najwyższym poziomie w klasie
SleepNightAdapter
zdefiniujCoroutineScope
za pomocąDispatchers.Default
.
private val adapterScope = CoroutineScope(Dispatchers.Default)
- W
addHeaderAndSubmitList()
uruchom korutynę wadapterScope
, aby manipulować listą. Następnie przełącz się na kontekstDispatchers.Main
, aby przesłać listę, jak pokazano w poniższym kodzie.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {
adapterScope.launch {
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
withContext(Dispatchers.Main) {
submitList(items)
}
}
}
- Kod powinien się skompilować i uruchomić, a Ty nie zauważysz żadnej różnicy.
Obecnie nagłówek ma taką samą szerokość jak inne elementy siatki i zajmuje 1 kolumnę w poziomie i w pionie. W całej siatce mieszczą się 3 elementy o szerokości 1 kolumny, więc nagłówek powinien zajmować 3 kolumny.
Aby naprawić szerokość nagłówka, musisz określić, kiedy GridLayoutManager
ma rozciągać dane na wszystkie kolumny. Możesz to zrobić, konfigurując SpanSizeLookup
na GridLayoutManager
. Jest to obiekt konfiguracji, którego GridLayoutManager
używa do określania liczby kolumn dla każdego elementu na liście.
- Otwórz plik SleepTrackerFragment.kt.
- Znajdź kod, w którym definiujesz
manager
, pod konieconCreateView()
.
val manager = GridLayoutManager(activity, 3)
- Pod
manager
zdefiniujmanager.spanSizeLookup
, jak pokazano poniżej. Musisz utworzyćobject
, ponieważsetSpanSizeLookup
nie przyjmuje wartości lambda. Aby w języku Kotlin utworzyć znakobject
, wpiszobject : classname
, w tym przypadkuGridLayoutManager.SpanSizeLookup
.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- Może pojawić się błąd kompilatora, który uniemożliwi wywołanie konstruktora. Jeśli tak, otwórz menu intencji za pomocą klawisza
Option+Enter
(Mac) lubAlt+Enter
(Windows), aby zastosować wywołanie konstruktora.
- W
object
pojawi się błąd informujący o konieczności zastąpienia metod. Umieść kursor na ikonieobject
, naciśnijOption+Enter
(Mac) lubAlt+Enter
(Windows), aby otworzyć menu intencji, a następnie zastąp metodęgetSpanSize()
.
- W treści
getSpanSize()
zwróć odpowiedni rozmiar zakresu dla każdej pozycji. Pozycja 0 ma rozmiar zakresu 3, a pozostałe pozycje mają rozmiar zakresu 1. Gotowy kod powinien wyglądać tak:
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = when (position) {
0 -> 3
else -> 1
}
}
- Aby poprawić wygląd nagłówka, otwórz plik header.xml i dodaj ten kod do pliku układu header.xml.
android:textColor="@color/white_text_color"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@color/colorAccent"
- Uruchom aplikację. Powinna wyglądać jak na zrzucie ekranu poniżej.
Gratulacje! To już koniec.
Projekt Android Studio: RecyclerViewHeaders
- Nagłówek to zwykle element, który zajmuje całą szerokość listy i pełni funkcję tytułu lub separatora. Lista może mieć jeden nagłówek opisujący zawartość elementu lub wiele nagłówków grupujących elementy i oddzielających je od siebie.
RecyclerView
może używać wielu uchwytów widoku, aby pomieścić niejednorodny zestaw elementów, np. nagłówki i elementy listy.- Jednym ze sposobów dodawania nagłówków jest zmodyfikowanie adaptera, aby używał innego
ViewHolder
przez sprawdzanie indeksów, w których ma się wyświetlać nagłówek. Za śledzenie nagłówka odpowiadaAdapter
. - Innym sposobem dodawania nagłówków jest zmodyfikowanie zbioru danych (listy) stanowiącego podstawę siatki danych, co zostało zrobione w tych ćwiczeniach z programowania.
Oto główne etapy dodawania nagłówka:
- Wyodrębnij dane z listy, tworząc
DataItem
, które może zawierać nagłówek lub dane. - Utwórz w adapterze uchwyt widoku z układem nagłówka.
- Zaktualizuj adapter i jego metody, aby używać dowolnego rodzaju
RecyclerView.ViewHolder
. - W
onCreateViewHolder()
zwróć prawidłowy typ uchwytu widoku dla elementu danych. - Zaktualizuj aplikację
SleepNightDiffCallback
, aby współpracowała z klasąDataItem
. - Utwórz funkcję
addHeaderAndSubmitList()
, która używa korutyn do dodania nagłówka do zbioru danych, a następnie wywołuje funkcjęsubmitList()
. - Zastosuj
GridLayoutManager.SpanSizeLookup()
, aby nagłówek zajmował tylko 3 kolumny.
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
Które z tych stwierdzeń dotyczących ViewHolder
jest prawdziwe?
▢ Adapter może używać wielu klas ViewHolder
do przechowywania nagłówków i różnych typów danych.
▢ Możesz mieć dokładnie 1 obiekt wyświetlający dane i 1 obiekt wyświetlający nagłówek.
▢ RecyclerView
obsługuje wiele typów nagłówków, ale dane muszą być jednolite.
▢ Podczas dodawania nagłówka użyj klasy podrzędnej RecyclerView
, aby wstawić nagłówek w odpowiednim miejscu.
Pytanie 2
Kiedy należy używać korutyn z RecyclerView
? Zaznacz wszystkie prawdziwe stwierdzenia.
▢ Nigdy. RecyclerView
to element interfejsu, który nie powinien używać korutyn.
▢ Używaj korutyn do długotrwałych zadań, które mogą spowolnić interfejs.
▢ Manipulowanie listami może zająć dużo czasu, dlatego zawsze należy to robić za pomocą korutyn.
▢ Używaj współprogramów z funkcjami zawieszania, aby uniknąć blokowania wątku głównego.
Pytanie 3
Której z tych czynności NIE musisz wykonywać, gdy używasz więcej niż jednego ViewHolder
?
▢ W ViewHolder
podaj kilka plików układu, które w razie potrzeby można rozwinąć.
▢ W onCreateViewHolder()
zwróć prawidłowy typ obiektu wyświetlającego dla elementu danych.
▢ W onBindViewHolder()
wiąż dane tylko wtedy, gdy uchwyt widoku jest odpowiednim typem uchwytu widoku dla elementu danych.
▢ Uogólnij sygnaturę klasy adaptera, aby akceptowała dowolny typ RecyclerView.ViewHolder
.
Rozpocznij kolejną lekcję:
Linki do innych ćwiczeń z tego kursu znajdziesz na stronie docelowej ćwiczeń z podstaw języka Kotlin na Androidzie.