Android Kotlin Fundamentals 07.5: Nagłówki w 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

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 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.
  • Jak rejestrować i obsługiwać kliknięcia elementów w RecyclerView.

Czego się nauczysz

  • Jak używać więcej niż jednego przycisku ViewHolder z przyciskiem RecyclerView, aby dodawać elementy o innym układzie. W szczególności jak użyć drugiego tagu ViewHolder, aby dodać nagłówek nad elementami wyświetlanymi w tagu RecyclerView.

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.

  1. 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.
  2. Otwórz plik SleepNightAdapter.kt.
  3. Pod klasą SleepNightListener na najwyższym poziomie zdefiniuj klasę sealed o nazwie DataItem, która reprezentuje element danych.

    Klasa sealed definiuje typ zamknięty, co oznacza, że wszystkie podklasy DataItem muszą być zdefiniowane w tym pliku. Dzięki temu kompilator zna liczbę podklas. Inna część kodu nie może zdefiniować nowego typu DataItem, który mógłby uszkodzić adapter.
sealed class DataItem {

 }
  1. W treści klasy DataItem zdefiniuj 2 klasy reprezentujące różne typy elementów danych. Pierwszy to SleepNightItem, który jest otoczką SleepNight, więc przyjmuje pojedynczą wartość o nazwie sleepNight. Aby uczynić ją częścią klasy zamkniętej, rozszerz ją o DataItem.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
  1. Druga klasa to Header, która reprezentuje nagłówek. Ponieważ nagłówek nie zawiera rzeczywistych danych, możesz zadeklarować go jako object. Oznacza to, że zawsze będzie tylko jedno wystąpienie elementu Header. Ponownie rozszerz go o DataItem.
object Header: DataItem()
  1. Wewnątrz elementu DataItem na poziomie zajęć zdefiniuj właściwość abstract Long o nazwie id. Gdy adapter używa parametru DiffUtil, aby określić, czy i jak zmienił się element, parametr DiffItemCallback musi znać identyfikator każdego elementu. Pojawi się błąd, ponieważ SleepNightItemHeader muszą zastąpić właściwość abstrakcyjną id.
abstract val id: Long
  1. SleepNightItem zastąp id, aby zwrócić nightId.
override val id = sleepNight.nightId
  1. Header zastąp id, 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ącym nightId.
override val id = Long.MIN_VALUE
  1. 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

  1. 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" />
  1. Wyodrębnij "Sleep Results" do zasobu tekstowego i nadaj mu nazwę header_text.
<string name="header_text">Sleep Results</string>
  1. W pliku SleepNightAdapter.kt wewnątrz funkcji SleepNightAdapter, powyżej klasy ViewHolder, 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ć ViewR:
    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

  1. SleepNightAdapter.kt na najwyższym poziomie, poniżej instrukcji import i powyżej SleepNightAdapter, 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
  1. SleepNightAdapter utwórz funkcję, która zastąpi getItemViewType(), 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

  1. W definicji SleepNightAdapter zmień pierwszy argument funkcji ListAdapter z SleepNight na DataItem.
  2. W definicji SleepNightAdapter zmień drugi argument ogólny dla funkcji ListAdapter z SleepNightAdapter.ViewHolder na RecyclerView.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()

  1. Zmień sygnaturę funkcji onCreateViewHolder(), aby zwracała wartość RecyclerView.ViewHolder.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
  1. 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()

  1. Zmień typ parametru onBindViewHolder() z ViewHolder na RecyclerView.ViewHolder.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
  1. Dodaj warunek, aby przypisywać dane do właściciela widoku tylko wtedy, gdy jest on ViewHolder.
        when (holder) {
            is ViewHolder -> {...}
  1. Rzutuj typ obiektu zwracany przez getItem() na DataItem.SleepNightItem. Gotowa funkcja onBindViewHolder() 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

  1. Zmień metody w SleepNightDiffCallback, aby używać nowej klasy DataItem zamiast SleepNight. 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

  1. Wewnątrz SleepNightAdapter, pod onCreateViewHolder(), zdefiniuj funkcję addHeaderAndSubmitList(), jak pokazano poniżej. Ta funkcja przyjmuje listę SleepNight. Zamiast używać funkcji submitList() udostępnianej przez ListAdapter do przesyłania listy, użyjesz tej funkcji, aby dodać nagłówek, a następnie przesłać listę.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
  1. Wewnątrz addHeaderAndSubmitList(), jeśli przekazana lista to null, 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)
  1. Otwórz plik SleepTrackerFragment.kt i zmień wywołanie funkcji submitList() na addHeaderAndSubmitList().
  1. 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:

  1. Na najwyższym poziomie w klasie SleepNightAdapter zdefiniuj CoroutineScope za pomocą Dispatchers.Default.
private val adapterScope = CoroutineScope(Dispatchers.Default)
  1. addHeaderAndSubmitList() uruchom korutynę w adapterScope, aby manipulować listą. Następnie przełącz się na kontekst Dispatchers.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)
            }
        }
    }
  1. 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.

  1. Otwórz plik SleepTrackerFragment.kt.
  2. Znajdź kod, w którym definiujesz manager, pod koniec onCreateView().
val manager = GridLayoutManager(activity, 3)
  1. Pod manager zdefiniuj manager.spanSizeLookup, jak pokazano poniżej. Musisz utworzyć object, ponieważ setSpanSizeLookup nie przyjmuje wartości lambda. Aby w języku Kotlin utworzyć znak object, wpisz object : classname, w tym przypadku GridLayoutManager.SpanSizeLookup.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
  1. 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) lub Alt+Enter (Windows), aby zastosować wywołanie konstruktora.
  1. object pojawi się błąd informujący o konieczności zastąpienia metod. Umieść kursor na ikonie object, naciśnij Option+Enter (Mac) lub Alt+Enter (Windows), aby otworzyć menu intencji, a następnie zastąp metodę getSpanSize().
  1. 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
            }
        }
  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"
  1. 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 odpowiada Adapter.
  • 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.
  • 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ę: 8.1 Pobieranie danych z internetu

Linki do innych ćwiczeń z tego kursu znajdziesz na stronie docelowej ćwiczeń z podstaw języka Kotlin na Androidzie.