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
Z tego ćwiczenia dowiesz się, jak dodać nagłówek obejmujący szerokość listy wyświetlanej w narzędziu RecyclerView
. Korzystasz z aplikacji do monitorowania snu z poprzednich ćwiczeń.
Co musisz wiedzieć
- Jak utworzyć podstawowy interfejs, używając aktywności, fragmentów i widoków danych.
- Jak poruszać się między fragmentami i jak używać
safeArgs
do przekazywania danych między fragmentami. - Wyświetlanie modeli, wyświetlanie fabryk modeli, przekształceń oraz
LiveData
i ich obserwatorów. - Jak utworzyć bazę danych
Room
, utworzyć DAO i określić encje. - Sposób używania schematów na potrzeby interakcji z bazą danych lub 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
. - Jak przechwytywać kliknięcia produktów i zarządzać nimi w:
RecyclerView.
Czego się nauczysz
- Używanie kilku elementów
ViewHolder
z elementemRecyclerView
w celu dodania elementów o innym układzie. W szczególności używaj drugiego atrybutuViewHolder
, aby dodawać nagłówek nad elementami wyświetlanymi w elemencieRecyclerView
.
Jakie zadania wykonasz:
- Skorzystaj z aplikacji TrackMySleepQuality z poprzedniego ćwiczenia z tej serii.
- Dodaj nagłówek, który rozciąga się na szerokość ekranu nad nocami wyświetlanymi w narzędziu
RecyclerView
.
Aplikacja do monitorowania snu ma początkowo 3 ekrany reprezentowane fragmentami, jak 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 (środkowym) wybierasz ocenę jakości snu. Trzeci ekran to widok szczegółowy otwierany, gdy użytkownik kliknie element na siatce.
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 dodajesz nagłówek do siatki wyświetlanych elementów. Końcowy ekran główny będzie wyglądał tak:
Z tego modułu ćwiczeń dowiesz się, jak dodawać elementy o różnych układach w RecyclerView
. Przykładem może być nagłówek na liście lub na siatce. Lista może mieć jeden nagłówek opisujący zawartość elementu. Lista może też zawierać wiele nagłówków, które pozwalają grupować i rozdzielać elementy w ramach jednej listy.
RecyclerView
nie ma żadnych informacji o Twoich danych ani o układzie każdego elementu. LayoutManager
umieszcza elementy na ekranie, ale adapter dostosowuje wyświetlane dane i przekazuje je do RecyclerView
. Aby więc utworzyć nagłówki w adapterze, musisz dodać kod.
Dwa sposoby dodawania nagłówków
W RecyclerView
każdy element na liście odpowiada numerowi indeksującemu od 0. Przykład:
[Rzeczywiste dane] -> [Widoki adaptera]
[0: Sen snu] -> [0: nocna]
[1: Sennoc] -> [1: Sennoc]
[2: Sennoc] -> [2: Sennoc]
Dobrym sposobem na dodanie nagłówków do listy jest zmodyfikowanie adaptera tak, by używał innego atrybutu ViewHolder
, sprawdzając indeksy w miejscu, w którym ma być wyświetlany nagłówek. Adapter
jest odpowiedzialny za śledzenie nagłówka. Aby na przykład wyświetlać nagłówek u góry tabeli, musisz zwrócić inny nagłówek ViewHolder
podczas układania elementu z indeksem zero. Następnie pozostałe elementy zostaną zmapowane za pomocą przesunięcia nagłówka, jak pokazano poniżej.
[Rzeczywiste dane] -> [Widoki adaptera]
[0: Nagłówek]
[0: Sen.noc] -> [1: Sen.noc]
[1: Sennoc] -> [2: Sennoc]
[2: Sen snu] -> [3: Sen.
Innym sposobem dodawania nagłówków jest zmodyfikowanie zbioru danych backendu dla siatki danych. Wszystkie dane, które mają być wyświetlane, są przechowywane na liście, dlatego możesz ją zmodyfikować, aby zawierała elementy reprezentujące nagłówek. Jest to nieco łatwiejsze do zrozumienia, ale wymaga przemyślenia sposobu projektowania obiektów, dzięki czemu możesz połączyć różne typy elementów w jedną listę. W ten sposób adapter wyświetli elementy, które zostaną do niego przesłane. Element na pozycji 0 jest nagłówkiem, a element na pozycji 1 to SleepNight
, który mapuje bezpośrednio na to, co widać na ekranie.
[Rzeczywiste dane] -> [Widoki adaptera]
[0: Nagłówek] -> [0: Nagłówek]
[1: Sennoc] -> [1: Sennoc]
[2: Sennoc] -> [2: Sennoc]
[3: Sennoc] -> [3: Sennoc]
Każda metoda ma zalety i wady. Zmiana zbioru danych nie spowoduje wprowadzenia znaczących zmian w pozostałej części kodu adaptera. Możesz dodać logikę nagłówka, manipulując listą danych. Z drugiej strony użycie różnych nagłówków ViewHolder
przez sprawdzenie indeksów daje więcej swobody w określaniu układu nagłówków. Umożliwia też adapterowi dostosowywanie danych w widoku danych bez konieczności modyfikowania tych danych.
W ćwiczeniach z programowania zaktualizujesz RecyclerView
, aby wyświetlał nagłówek na początku listy. W tym przypadku aplikacja będzie używać innego nagłówka ViewHolder
niż elementu danych. Aplikacja sprawdzi indeks listy, aby określić, którego ViewHolder
ma używać.
Krok 1. Utwórz klasę DataItem
Aby abstraktować typ elementu, a adapter po prostu radzić sobie z &;quot;items", możesz utworzyć klasę właściciela danych, która reprezentuje SleepNight
lub Header
. Zbiór danych będzie wówczas zawierał listę elementów należących do właściciela danych.
Możesz pobrać aplikację startową z GitHuba lub dalej używać aplikacji SleepTracker utworzonej w ramach ćwiczeń z programowania.
- Pobierz kod RecyclerViewHeaders-Starter z GitHuba. Katalog RecyclerViewHeaders-Starter zawiera początkową wersję aplikacji SleepTracker niezbędną do udziału w tym ćwiczeniu. Jeśli wolisz, możesz też ukończyć gotową aplikację z poprzedniego ćwiczenia z programowania.
- Otwórz 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 w tym pliku muszą być zdefiniowane wszystkie podkategorieDataItem
. W rezultacie liczba podklas jest znana kompilatorowi. W innej części kodu nie można określić nowego typu atrybutuDataItem
, który może spowodować uszkodzenie adaptera.
sealed class DataItem {
}
- W treści klasy
DataItem
określ dwie klasy, które reprezentują różne typy elementów danych. Pierwsze toSleepNightItem
, czyli kod, który łączy ok.SleepNight
i pobiera pojedynczą wartość o nazwiesleepNight
. Aby stać się częścią przedmiotu klasy szczelności, przedłuż go:DataItem
.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- Druga klasa to
Header
, która reprezentuje nagłówek. Nagłówek nie zawiera danych, więc możesz go zadeklarować jakoobject
. Oznacza to, że elementHeader
nigdy nie wystąpi. Przedłuż go oDataItem
.
object Header: DataItem()
- W klasie
DataItem
określ na poziomie klasy właściwośćabstract
Long
o nazwieid
. Gdy adapter używa atrybutuDiffUtil
do określenia, czy i w jaki sposób nastąpiła zmiana,DiffItemCallback
musi znać identyfikator każdego elementu. Zobaczysz błąd, ponieważSleepNightItem
iHeader
muszą zastąpić abstrakcyjną właściwość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
, co jest bardzo małą (czyli -2 do potęgi 63). Oznacza to, że nigdy nie będą sprzeczne z istniejącym elementemnightId
.
override val id = Long.MIN_VALUE
- Gotowy kod powinien wyglądać podobnie do tego, a aplikacja powinna kompilować się 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 nagłówek ViewHolder dla nagłówka
- Utwórz układ nagłówka w nowym pliku zasobów o nazwie header.xml, który wyświetla
TextView
. Nie ma tu nic ekscytującego, więc oto 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 ciągu znaków i nazwij goheader_text
.
<string name="header_text">Sleep Results</string>
- W pliku SleepNightAdapter.kt w obrębie
SleepNightAdapter
, nad klasąViewHolder
, utwórz nową klasęTextViewHolder
. Ta klasa zwiększa układ textview.xml i zwraca instancjęTextViewHolder
. Ponieważ masz to już za sobą, oto kod, który 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ę SleepNightAdapter
. Zamiast obsługiwać tylko jeden typ ViewHolder
, musi on mieć możliwość użycia dowolnego typu właściciela widoku danych.
Zdefiniuj typy elementów
- W
SleepNightAdapter.kt
na najwyższym poziomie, poniżej instrukcjiimport
i powyżejSleepNightAdapter
, zdefiniuj dwie stałe dla widoków danych.
Aby elementy zamówienia mogły prawidłowo przypisywać do nich udział w wyświetleniach, obiektRecyclerView
musi rozróżniać każdy typ widoku danych.
private val ITEM_VIEW_TYPE_HEADER = 0
private val ITEM_VIEW_TYPE_ITEM = 1
- W sekcji
SleepNightAdapter
utwórz funkcję zastępowaniagetItemViewType()
w celu zwracania stałego nagłówka lub stałej pozycji 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
}
}
Zaktualizuj definicję SleepNightAdapter
- W definicji
SleepNightAdapter
zaktualizuj pierwszy argumentListAdapter
zSleepNight
naDataItem
. - W definicji
SleepNightAdapter
zmień drugi argument ogólny dlaListAdapter
zSleepNightAdapter.ViewHolder
naRecyclerView.ViewHolder
. Podczas dokonywania niezbędnych aktualizacji zobaczysz błędy, a nagłówek klasy powinien wyglądać jak poniżej.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {
Aktualizacja onCreateViewHolder()
- Zmień podpis
onCreateViewHolder()
, by zwracałRecyclerView.ViewHolder
.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
- Rozwiń implementację metody
onCreateViewHolder()
, by przetestować w przypadku każdego typu elementu identyfikator użytkownika odpowiedni do wyświetlania. Zaktualizowana metoda powinna wyglądać jak 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 onBindViewHolder()
- Zmień typ parametru
onBindViewHolder()
zViewHolder
naRecyclerView.ViewHolder
.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
- Dodaj warunek, by przypisywać dane do użytkownika tylko wtedy, gdy ma on typ
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
- Prześlij typ obiektu zwracany przez
getItem()
naDataItem.SleepNightItem
. Twoja 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)
}
}
}
Aktualizacja wywołań zwrotnych diffUtil
- Zmień metody w
SleepNightDiffCallback
, aby używać nowych zajęćDataItem
, a nieSleepNight
. Pomiń ostrzeżenie o linczach, jak widać w poniższym kodzie.
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
- W obrębie funkcji
SleepNightAdapter
, poniżejonCreateViewHolder()
, zdefiniuj funkcjęaddHeaderAndSubmitList()
w podany niżej sposób. Ta funkcja pobiera listęSleepNight
. Zamiast przesyłać tekstsubmitList()
podany przezListAdapter
, do przesłania listy możesz użyć tej funkcji, by dodać nagłówek, a następnie przesłać listę.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
- Jeśli na liście
addHeaderAndSubmitList()
podana jest wartośćnull
, zwróć tylko nagłówek, a w przeciwnym razie dołącz nagłówek do nagłówka listy i prześlij ją.
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 na
submitList()
doaddHeaderAndSubmitList()
.
- Uruchom aplikację i sprawdź, jak nagłówek wyświetla się jako pierwszy element na liście.
Musisz rozwiązać dwie problemy dotyczące tej aplikacji. Jedna jest widoczna, a druga nie.
- Nagłówek wyświetla się w lewym górnym rogu i jest trudny do odróżnienia.
- W przypadku krótkiej listy z 1 nagłówkiem nie ma to znaczenia, ale w wątku interfejsu nie należy wykonywać operacji na liście w języku
addHeaderAndSubmitList()
. Wyobraź sobie listę z setkami elementów, wieloma nagłówkami i logiką, by zdecydować, gdzie wstawić elementy. To zadanie należy do współprogramu.
Aby używać współprogramów, zmień addHeaderAndSubmitList()
:
- Na najwyższym poziomie klasy
SleepNightAdapter
określ właściwośćCoroutineScope
za pomocą właściwościDispatchers.Default
.
private val adapterScope = CoroutineScope(Dispatchers.Default)
- W
addHeaderAndSubmitList()
uruchom grupę 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 być tworzony i uruchomiony, więc nie zobaczysz żadnych różnic.
Obecnie nagłówek ma taką samą szerokość jak inne elementy siatki, zajmując jedną rozpiętość w poziomie i w pionie. Cała siatka mieści się w trzech elementach o szerokości po jednej szerokości, więc nagłówek powinien mieć trzy rozpiętości w poziomie.
Aby poprawić szerokość nagłówka, musisz określić w polu GridLayoutManager
zakres danych we wszystkich kolumnach. Aby to zrobić, skonfiguruj SpanSizeLookup
na urządzeniu GridLayoutManager
. To jest obiekt konfiguracji, którego GridLayoutManager
używa do określenia liczby rozpiętości dla każdego elementu na liście.
- Otwórz plik SleepTrackerFragment.kt.
- Znajdź kod, w którym zdefiniujesz kod
manager
, który znajduje się pod koniec tego adresu:onCreateView()
.
val manager = GridLayoutManager(activity, 3)
- Poniżej
manager
określmanager.spanSizeLookup
tak, jak to pokazano. Musisz zrobićobject
, bosetSpanSizeLookup
nie bierze lambdy. Aby utworzyćobject
w Kotlinie, wpiszobject : classname
, w tym przypadkuGridLayoutManager.SpanSizeLookup
.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- Możesz wywołać błąd kompilatora wywołujący konstruktor. Jeśli tak, otwórz menu intencji, używając
Option+Enter
(Mac) lubAlt+Enter
(Windows) i zastosuj wywołanie konstruktora.
- Następnie na stronie
object
pojawi się komunikat 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()
zwracaj odpowiedni rozmiar rozpiętości dla każdej pozycji. Pozycja 0 ma rozpiętość 3, a drugie – rozpiętość 1. Gotowy kod powinien wyglądać tak jak poniżej:
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = when (position) {
0 -> 3
else -> 1
}
}
- Aby ulepszyć 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ę. Powinno wyglądać na zrzucie ekranu poniżej.
Gratulacje! To już koniec.
Projekt Android Studio: RecyclerViewHeaders
- Nagłówek to zwykle element, który rozciąga się i znajduje się na liście lub pełni funkcję tytułu lub separatora. Lista może mieć jeden nagłówek opisujący treść elementu lub wiele nagłówków do grupowania elementów i oddzielania się od siebie.
RecyclerView
może wykorzystywać wiele elementów widoku danych, by uwzględnić heterogeniczny zbiór elementów, np. nagłówki i elementy listy.- Jednym ze sposobów dodania nagłówków jest zmodyfikowanie adaptera tak, by używał innego atrybutu
ViewHolder
, sprawdzając indeksy w miejscu, w którym ma być wyświetlany nagłówek.Adapter
odpowiada za śledzenie nagłówka. - Innym sposobem dodawania nagłówków jest zmodyfikowanie backendu zbioru danych (listy) w siatce danych, co zostało zrobione w tym ćwiczeniu z programowania.
Oto główne etapy dodawania nagłówka:
- Streszczenia danych na liście: utwórz
DataItem
, który może zawierać nagłówek lub dane. - Utwórz ładowarkę z układem nagłówka w adapterze.
- Zaktualizuj adapter i metody, aby używać dowolnego typu znaczników
RecyclerView.ViewHolder
. - W
onCreateViewHolder()
zwracaj prawidłowy typ właściciela danych dla elementu danych. - Aby pracować z klasą
DataItem
, zaktualizujSleepNightDiffCallback
. - Utwórz funkcję
addHeaderAndSubmitList()
, która za pomocą algorytmów dodaje nagłówek do zbioru danych i wywołuje metodęsubmitList()
. - Zaimplementuj
GridLayoutManager.SpanSizeLookup()
, aby szeroki nagłówek miał tylko trzy pasy.
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
Które z tych stwierdzeń na temat miejsca ViewHolder
jest prawdziwe?
▢ Adapter może używać nagłówków klasy ViewHolder
do przechowywania nagłówków i różnych typów danych.
▢ Możesz mieć tylko jeden widok danych dla danych i jeden nagłówek dla nagłówka.
▢ RecyclerView
obsługuje wiele typów nagłówków, ale dane muszą być jednolite.
▢ Dodając nagłówek, wybierasz podkategorię RecyclerView
, aby wstawić go w odpowiednim miejscu.
Pytanie 2
Kiedy należy używać współprac z RecyclerView
? Zaznacz wszystkie stwierdzenia, które są prawdziwe.
▢ Nigdy. Element RecyclerView
jest elementem interfejsu i nie powinien używać strategii.
▢ Używaj algorytmów do wykonywania długotrwałych zadań, które mogą spowolnić interfejs użytkownika.
▢ Manipulowanie listami może zająć dużo czasu, więc zawsze należy je wykonywać przy użyciu algorytmów.
▢ Używaj reguł z funkcjami zawieszania, by uniknąć zablokowania głównego wątku.
Pytanie 3
Które z tych czynności NIE musisz wykonywać, gdy używasz więcej niż jednej usługi ViewHolder
?
▢ ViewHolder
: dołącz wiele plików układu, by w razie potrzeby zwiększyć rozmiar pliku.
▢ onCreateViewHolder()
– zwracaj właściwy typ właściciela elementu danych.
▢ W onBindViewHolder()
wiązaj dane tylko wtedy, gdy są one właściwym typem widoku danych w przypadku tego elementu.
▢ Ogólnie podpis klasy adaptera, aby zaakceptować dowolny RecyclerView.ViewHolder
.
Rozpocznij następną lekcję:
Linki do innych ćwiczeń z programowania w tym kursie znajdziesz na stronie docelowej z ćwiczeniami z podstaw Androida Kotlin.