Úlohy pre 4. cvičenie

  • 1. Vytvorenie skrolovateľného zoznamu položiek pomocou RecyclerView.
  • 2. Použitie ViewModelu a LiveData pre asynchrónne úlohy a uchovávanie údajov.
  • 3. Kotlin Coroutines.
  • 4. Komunikácia s REST webservisom pomocou Retrofitu a Gson na parsovanie JSONU.

Hodnotenie

Za splnenie prvých 3 úloh celkovo 3 body.

Za splnenie úlohy 4. celkovo 2 body.

1. Vytvorenie skrolovateľného zoznamu položiek pomocou RecyclerView

Krok 1: Vytvorenie XML layout pre RecyclerView

Tento XML kód je určený pre layout v Android aplikácii a obsahuje dva hlavné komponenty: BottomBar a RecyclerView.

RecyclerView je flexibilná komponenta zobrazenia, ktorá je optimalizovaná pre veľké súbory dát, ktoré sa menia v čase. Tento konkrétny RecyclerView je nastavený tak, aby vyplnil všetok dostupný priestor medzi vrchom rodičovského view a spodnou časťou BottomBar.

V súbore fragment_example.xml:
<eu.mcomputing.mobv.mobvzadanie.BottomBar
    android:id="@+id/bottom_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

<androidx.recyclerview.widget.RecyclerView
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toTopOf="@id/bottom_bar"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    android:layout_height="0dp"
    android:layout_width="match_parent"/>

Šírka je nastavená na match_parent, čo znamená, že RecyclerView bude mať rovnakú šírku ako jeho rodičovský view. Výška je nastavená na 0dp, čo v kombinácii s hornými a dolnými obmedzeniami zabezpečuje, že RecyclerView sa rozťahuje, aby vyplnila dostupný vertikálny priestor.

Krok 2: Vytvorenie XML layout pre riadok v zozname

Tento XML kód definuje layout pre jednotlivé riadky zobrazené v rámci komponentu RecyclerView. Skladá sa z ConstraintLayout, ktorý obsahuje ImageView a TextView.

ConstraintLayout je použitý ako kontajner pre riadok, čo umožňuje flexibilné rozmiestnenie a veľkosť komponentov vo vnútri. Tento layout sa rozširuje na celú šírku rodičovského view a obaluje obsah pre výšku.

Okrem toho, margin definovaný v android:layout_margin poskytuje odsadenie od okrajov rodičovského view.

ImageView je použitý pre zobrazenie obrázka v riadku. Jeho šírka a výška sú nastavené na wrap_content, čo znamená, že view si zmení svoje rozmery podľa veľkosti obrázka.

Použitie layout_constraint* atribútov zabezpečuje, že tento view je ukotvený na vrch a začiatok rodičovského view v rámci ConstraintLayout.

TextView je použitý pre zobrazenie textu v riadku. Jeho šírka a výška sú nastavené na wrap_content, čo znamená, že view si zmení svoje rozmery podľa obsahu textu.

Text je posunutý od začiatku obrázka s definovaným odsadením a je ukotvený na vrch rodičovského view.

Vytvorte súbor feed_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="@dimen/activity_horizontal_margin"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <ImageView
        android:id="@+id/item_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />

    <TextView
        android:id="@+id/item_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toEndOf="@id/item_image"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginStart="@dimen/activity_horizontal_margin"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

Krok 3: Implementácia adaptéra

RecyclerView.Adapter je kľúčovou súčasťou práce s RecyclerView v Android aplikáciách. Tento adaptér je zodpovedný za poskytovanie viewholderov, ktoré reprezentujú položky dát, a tiež za väzbu dát k týmto viewholderom keď sú tieto položky zobrazené na obrazovke.

Pred implementáciou adaptéra musíte najprv definovať ViewHolder. Táto vnútorná trieda rozširuje RecyclerView.ViewHolder a uchováva informácie o jednotlivých položkách zoznamu.


class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val imageView: ImageView = view.findViewById(R.id.item_image)
    val textView: TextView = view.findViewById(R.id.item_text)
}
        

V tomto príklade, CustomViewHolder obsahuje referencie na ImageView a TextView, ktoré sa používajú na zobrazenie dát pre jednu položku v RecyclerView.

Adaptér potrebuje prepísať tri metódy: onCreateViewHolder(), onBindViewHolder(), a getItemCount().


class CustomAdapter(private val data: List<DataType>) : RecyclerView.Adapter<CustomViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
        return CustomViewHolder(view)
    }

    override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
        val item = data[position]
        // Bind data to ViewHolder
    }

    override fun getItemCount(): Int = data.size
}
        

V metóde onCreateViewHolder(), adaptér inflates the layout pre každú položku a inicializuje ViewHolder. V metóde onBindViewHolder(), adaptér priradí dáta (získané z vašej dátovej sady) k elementom view udržiavaným ViewHolder-om. Metóda getItemCount() jednoducho vráti počet položiek v dátovej sade.

Nakoniec, po vytvorení adaptéra, musíte ho priradiť k inštancii RecyclerView.


val recyclerView: RecyclerView = findViewById(R.id.my_recycler_view)
recyclerView.adapter = CustomAdapter(myDataList)
recyclerView.layoutManager = LinearLayoutManager(this)
        

Toto priradenie zabezpečuje, že RecyclerView bude vedieť, ako vytvárať a napĺňať jednotlivé riadky na základe dát poskytnutých adaptérom.

Príklad adaptéra: FeedAdapter

Nižšie je príklad adaptéra pre RecyclerView s vlastným layoutom, ktorý obsahuje ImageView a TextView:

Vytvorte nový Kotlin súbor s názvom FeedAdapter.kt a implementujte adaptér.

package eu.mcomputing.mobv.mobvzadanie

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

data class MyItem(val imageResource: Int, val text: String)

class FeedAdapter : RecyclerView.Adapter<FeedAdapter.FeedViewHolder>() {
    private var items: List<MyItem> = listOf()

    // ViewHolder poskytuje odkazy na zobrazenia v každej položke
    class FeedViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    // Táto metóda vytvára nový ViewHolder
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.feed_item, parent, false)
        return FeedViewHolder(view)
    }

    // Táto metóda prepojí dáta s ViewHolderom
    override fun onBindViewHolder(holder: FeedViewHolder, position: Int) {
        holder.itemView.findViewById<ImageView>(R.id.item_image).setImageResource(items[position].imageResource)
        holder.itemView.findViewById<TextView>(R.id.item_text).text = items[position].text
    }

    // Vracia počet položiek v zozname
    override fun getItemCount() = items.size

    fun updateItems(newItems: List<MyItem>) {
        items = newItems
        notifyDataSetChanged()
    }
}

Tento kód demonštruje, ako vytvoriť adaptér pre RecyclerView, ktorý obsahuje zoznam položiek typu MyItem. Každá položka MyItem má dva atribúty: imageResource pre ukladanie referencie na obrázok a text pre ukladanie textového reťazca.

data class MyItem(val imageResource: Int, val text: String): Definuje dátovú triedu v Kotlin, ktorá reprezentuje položku so dvoma vlastnosťami: obrázok a text.

class FeedAdapter : RecyclerView.Adapter<FeedAdapter.FeedViewHolder>(): Definuje adaptér dedičný od RecyclerView.Adapter s vnorenou triedou FeedViewHolder.

class FeedViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView): Táto vnorená trieda rozširuje RecyclerView.ViewHolder a poskytuje referencie na zobrazenia pre každú položku v RecyclerView.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedViewHolder: Táto metóda inflates (načíta) layout pre každú položku v RecyclerView a inicializuje FeedViewHolder.

override fun onBindViewHolder(holder: FeedViewHolder, position: Int): Táto metóda prepojí dáta s ViewHolderom. Konkrétne nastavuje obrázok a text pre zobrazenia v ViewHolderi na základe dát na aktuálnej pozícii.

override fun getItemCount() = items.size: Táto metóda vracia celkový počet položiek v dátovom zozname.

fun updateItems(newItems: List<MyItem>): Táto verejná metóda umožňuje aktualizovať zoznam položiek v adaptéri a informovať RecyclerView o zmene dát prostredníctvom volania notifyDataSetChanged().

Kód je dobre štrukturovaný a každá časť má svoju zodpovednú úlohu pri zobrazovaní dát v RecyclerView. Metóda updateItems je obzvlášť užitočná, pretože umožňuje adaptéru pracovať s najnovšími dátami.

Krok 4: Inicializácia RecyclerView v Fragment

V súbore fragmentu, inicializujte RecyclerView:
val recyclerView = view.findViewById<RecyclerView>(R.id.feed_recyclerview)
recyclerView.layoutManager = LinearLayoutManager(context)
val feedAdapter = FeedAdapter()
recyclerView.adapter = feedAdapter
feedAdapter.updateItems(listOf(
    MyItem(R.drawable.baseline_feed_24,"Prvy"),
    MyItem(R.drawable.baseline_map_24,"Druhy"),
    MyItem(R.drawable.baseline_account_box_24,"Treti"),
))

Tento úsek kódu ilustruje, ako sa inicializuje RecyclerView a nastavuje sa jeho adaptér a správca rozloženia v kontexte Android aplikácie. Tu je podrobný opis krokov:

  • val recyclerView = view.findViewById<RecyclerView>(R.id.feed_recyclerview): Táto línia kódu vyhľadáva RecyclerView v aktuálnom zobrazení/layoute pomocou jeho ID. Následne priradzuje referenciu na tento RecyclerView do premennej recyclerView.
  • recyclerView.layoutManager = LinearLayoutManager(context): Nastavuje správcu rozloženia pre RecyclerView. LinearLayoutManager zobrazuje položky v lineárnom zozname, čo je štandardný spôsob zobrazenia položiek v zozname alebo mriežke v Android aplikáciách.
  • val feedAdapter = FeedAdapter(): Vytvára novú inštanciu adaptéra FeedAdapter, ktorý bol predtým definovaný. Tento adaptér bude zodpovedný za dodávanie zobrazení položiek pre RecyclerView.
  • recyclerView.adapter = feedAdapter: Priradzuje adaptér feedAdapter k RecyclerView. Týmto sa zabezpečí, že každá položka v zozname bude mať správne zobrazenie, a bude správne zobrazovaná v RecyclerView.
  • feedAdapter.updateItems(listOf( MyItem(R.drawable.baseline_feed_24,"Prvy"), MyItem(R.drawable.baseline_map_24,"Druhy"), MyItem(R.drawable.baseline_account_box_24,"Treti"), )): Tento kód aktualizuje dáta v adaptéri s novým zoznamom položiek. Každá položka je reprezentovaná inštanciou MyItem, ktorá obsahuje obrázok a text. updateItems je metóda definovaná v našom FeedAdapter, ktorá aktualizuje zoznam položiek a informuje adaptér, že dáta sa zmenili.

Tento kód je kľúčový pri inicializácii a nastavení RecyclerView v Android aplikácii, aby sa zobrazovali položky v zozname pomocou custom adaptéra.

LinearLayoutManager je správca rozloženia, ktorý usporiada položky v lineárnej sekvencii. Je jedným z najčastejšie používaných správcov rozloženia v `RecyclerView` a poskytuje jednoduché zobrazenie položiek, buď horizontálne alebo vertikálne. Jeho hlavným cieľom je zobraziť položky v jednom stĺpci alebo riadku, podobne ako tradičný zoznam.

GridLayoutManager umožňuje zobraziť položky v mriežkovom formáte. V závislosti od orientácie a definovaného počtu "spanov" môže tento správca rozloženia usporiadať položky vo viacerých stĺpcoch (vertikálna orientácia) alebo riadkoch (horizontálna orientácia). Je ideálny pre situácie, keď chcete zobraziť položky v mriežkovom formáte, ako sú galérie obrázkov alebo aplikácie s dlaždicovým rozhraním.

Všeobecné vlastnosti

  • Flexibilita: Oba správcovia rozloženia sú flexibilné a môžu byť prispôsobené rôznym potrebám zobrazenia. Ich orientácia, rozmer a ďalšie vlastnosti môžu byť nastavené podľa požiadaviek aplikácie.
  • Optimalizácia: Ako súčasť knižnice RecyclerView, oba správcovia rozloženia sú navrhnuté tak, aby boli výkonné a optimalizované pre veľké súbory dát. Položky, ktoré nie sú viditeľné, sa opätovne použijú, čo znižuje záťaž na systém pri posúvaní.
  • Pripojenie s adaptérom: Oba správcovia rozloženia pracujú v tesnej spolupráci s adaptérom, ktorý poskytuje dáta a definuje, ako sú jednotlivé položky zobrazené.

LinearLayoutManager

LinearLayoutManager je jedným z manažérov rozloženia poskytovaných v rámci knižnice RecyclerView v Androide. Jeho hlavným účelom je zobraziť súbor položiek v lineárnom rozložení, čo môže byť vertikálne alebo horizontálne. Tu je niekoľko kľúčových bodov, ktoré je dôležité vedieť o LinearLayoutManager:

  • Základné Rozloženie: LinearLayoutManager umožňuje položkám v RecyclerView zobrazovať sa v priamke alebo lineárnej sekvencii, podobne ako LinearLayout v klasických Android rozloženiach.
  • Vertikálne alebo Horizontálne: Môžete nastaviť orientáciu LinearLayoutManagera na vertikálnu alebo horizontálnu. Vertikálna orientácia zobrazuje položky zhora nadol, kým horizontálna orientácia zobrazuje položky zľava doprava.
  • Práca s RecyclerView: Aby RecyclerView mohol správne fungovať, potrebuje manažéra rozloženia. LinearLayoutManager je často používaný, keď chcete zobraziť položky ako jednoduchý jeden stĺpec alebo riadok.
  • Účinnosť: LinearLayoutManager recykluje iba tie položky, ktoré momentálne nie sú viditeľné na obrazovke, čím zvyšuje výkon pri posúvaní tým, že znovu používa zobrazenia.
  • Prispôsobivosť: Napriek svojej jednoduchosti je LinearLayoutManager veľmi prispôsobivý. Môže byť použitý v rôznych situáciách, kde je potrebné zobraziť zoznam položiek lineárne.

LinearLayoutManager je dôležitou súčasťou mnohých Android aplikácií, ktoré vyžadujú zobrazenie zoznamu položiek v konzistentnom a účinnom spôsobe. Jeho použitie zjednodušuje správu rozloženia položiek v RecyclerView.

Príklad nastavenia LinearLayoutManager:


val layoutManager = LinearLayoutManager(context)
recyclerView.layoutManager = layoutManager
        

Vertikálna a horizontálna orientácia:

Ak chcete zmeniť orientáciu na horizontálnu, môžete to urobiť takto:


layoutManager.orientation = LinearLayoutManager.HORIZONTAL
        

GridLayoutManager

GridLayoutManager je ďalší manažér rozloženia poskytovaný v rámci knižnice RecyclerView v Androide, ktorý zobrazuje položky v mriežkovom rozložení. Položky sú usporiadané v stĺpcoch a riadkoch, čo poskytuje vizuálne bohatší dojem ako lineárne rozloženie. Tu je niekoľko dôležitých aspektov, ktoré je dobré poznať o GridLayoutManager:

  • Mriežkové Rozloženie: GridLayoutManager rozdeľuje súbor položiek do mriežky, ktorá pozostáva zo stĺpcov a riadkov. Každý riadok obsahuje fixný počet stĺpcov.
  • Prispôsobiteľný Počet Stĺpcov: Môžete nastaviť počet stĺpcov v mriežke, čo je užitočné pre rôzne veľkosti displejov alebo orientácie zariadenia (na stojato alebo na ležato).
  • Integrácia s RecyclerView: Podobne ako LinearLayoutManager, aj GridLayoutManager je používaný s RecyclerView pre zobrazenie položiek, ale poskytuje mriežkové rozloženie namiesto lineárneho.
  • Recyklácia Zobrazení: GridLayoutManager tiež recykluje zobrazenia tým, že opätovne používa položky, ktoré sa stali neviditeľnými v dôsledku posúvania.
  • Flexibilita: Je možné prispôsobiť veľkosti položiek, meniť orientáciu rozloženia a dokonca umožniť rôzne typy zobrazení v rámci jednej mriežky s použitím GridLayoutManager.

GridLayoutManager je ideálny pre aplikácie, ktoré potrebujú zobraziť položky v mriežkovom formáte, ako sú galérie obrázkov alebo komplexnejšie rozhrania. Poskytuje flexibilitu a vysoký výkon pre interaktívne a vizuálne prvky aplikácie.

Príklad nastavenia GridLayoutManager s 3 stĺpcami:


val gridLayoutManager = GridLayoutManager(context, 3)
recyclerView.layoutManager = gridLayoutManager
        

Ak chcete zmeniť orientáciu na horizontálnu a určiť počet riadkov, môžete to urobiť takto:


gridLayoutManager.orientation = GridLayoutManager.HORIZONTAL
// Uvedený počet riadkov je len príklad
gridLayoutManager.spanCount = 2
        

Rozdiely medzi LinearLayoutManager a GridLayoutManager

  • Usporiadanie: LinearLayoutManager usporiada položky vertikálne alebo horizontálne v jednom stĺpci alebo riadku. Na druhej strane, GridLayoutManager môže usporiadať položky vo viacerých stĺpcoch alebo riadkoch.
  • Vzhľad: Ak chcete zobraziť položky v klasickom zozname, použite LinearLayoutManager. Ak chcete zobraziť položky v mriežkovom formáte, použite GridLayoutManager.
  • Flexibilita: GridLayoutManager je flexibilnejší v tom, že umožňuje zobrazenie položiek v mriežke s rôznym počtom stĺpcov alebo riadkov v závislosti od orientácie.

Vyberanie medzi LinearLayoutManager a GridLayoutManager závisí od špecifických potrieb vašej aplikácie a typu dát, ktoré chcete zobraziť. Zatiaľ čo LinearLayoutManager je jednoduchší a ľahší na implementáciu, GridLayoutManager poskytuje väčšiu vizuálnu flexibilitu a je lepší pre komplexnejšie rozvrhnutia.

Krok 5: Naplnenie dát do RecyclerView

Naplnenie dát do RecyclerView je jednoduché prostredníctvom adaptéra. Pridajte dáta do zoznamu a oznámte zmeny adaptéru:

myAdapter.updateItems(newData)
        

Aktualizácia zoznamu dát vo vašej aplikácii je bežnou požiadavkou, keď dáta podliehajú zmene alebo sú dynamicky načítané. V RecyclerView v Androide môžeme aktualizovať adaptér s novými položkami pomocou metódy, ako je myAdapter.updateItems(newData). Tu je podrobný opis tohto procesu:

  • Definícia Metódy: V našom adaptéri RecyclerView definujeme metódu updateItems, ktorá prijíma zoznam nových dát. Táto metóda nahradí staré dáta novými.
  • Nahradenie Dát: V metóde updateItems nastavíme aktuálne dáta adaptéra na nový zoznam, ktorý je prenesený ako parameter. Toto prepíše existujúce dáta.
  • Notifikácia o Zmene Dát: Po aktualizácii dát je dôležité zavolať metódu notifyDataSetChanged(), aby bol adaptér informovaný o zmene dát a aby mohol obnoviť zobrazené položky.
  • Vizualizácia Aktualizácií: Keď je metóda notifyDataSetChanged() zavolaná, RecyclerView znovu vyvolá metódu onBindViewHolder pre položky, ktoré sú viditeľné, čím sa aktualizuje obsah zobrazený užívateľovi.
  • Optimalizácia Výkonu: Metóda notifyDataSetChanged() môže byť náročná na výkon, pretože spôsobuje prekreslenie všetkých položiek v RecyclerView. Na optimalizáciu výkonu môžete použiť metódy, ako sú notifyItemInserted(), notifyItemRemoved(), atď., ktoré oznámia zmene iba konkrétnych položiek.

Metóda updateItems(newData) je esenciálna pre udržanie údajov v RecyclerView aktuálnymi. Správna implementácia tejto metódy a efektívne oznámenie zmien adaptéru zabezpečia hladkú a reaktívnu užívateľskú skúsenosť.

Metódy `notifyDataSetChanged()` a ich variácie

1. notifyDataSetChanged()

Informuje `RecyclerView` o tom, že sa zmenili údaje v celom datasete a môže spôsobiť znovu vykreslenie celého zoznamu. Táto metóda je považovaná za výkonnostne náročnú, pretože môže invalidovať všetky položky a znovu ich vykresliť, aj keď sa zmenila iba jedna položka.

2. notifyItemChanged(int position)

Táto metóda informuje `RecyclerView` o zmene v jednej položke na špecifikovanej pozícii. Je efektívnejšia ako `notifyDataSetChanged()`, pretože aktualizuje iba danú položku, a nie celý zoznam.

3. notifyItemInserted(int position)

Informuje `RecyclerView` o tom, že bola na špecifikovanej pozícii vložená nová položka. Používa sa, keď chceme pridať novú položku do datasetu.

4. notifyItemRemoved(int position)

Informuje `RecyclerView` o tom, že položka na špecifikovanej pozícii bola odstránená. Používa sa, keď chceme odstrániť položku z datasetu.

5. notifyItemRangeChanged(int positionStart, int itemCount)

Táto metóda informuje `RecyclerView` o zmene v určenom rozsahu položiek. Je užitočná, keď sa naraz mení viac položiek.

Význam a využitie

Metódy `notify...` sú dôležité pre správnu aktualizáciu zobrazenia v `RecyclerView` po zmene údajov. Zabezpečujú, že používateľ vidí aktuálne údaje a zároveň optimalizujú výkonnosť tým, že obmedzujú vykresľovanie iba na zmenené položky. Odporúča sa vždy používať najšpecifikovanejšiu metódu, ktorá najlepšie zodpovedá zmene v údajoch, aby sa predišlo zbytočnému vykresľovaniu a zlepšila sa výkonnosť aplikácie.

6. Použitie DiffUtil.ItemCallback s RecyclerView.Adapter pre optimalizované aktualizácie zoznamu

Pre tých, ktorí chcú optimalizovať aktualizácie zoznamu v `RecyclerView.Adapter`, je možné použiť `DiffUtil`. Táto trieda vypočíta rozdiely medzi starým a novým zoznamom a automaticky aktualizuje adaptér s príslušnými animáciami.

DiffUtil je nástroj v knižnici RecyclerView, ktorý pomáha optimalizovať aktualizáciu dát v adaptéri zlepšením výkonu a efektívnosti. Konkrétne DiffUtil.ItemCallback sa používa na porovnanie položiek v zozname a identifikáciu zmien, ktoré treba aplikovať. Tu je podrobný opis, ako to funguje s ilustračným kódom:

  • Vytvorenie DiffCallback: Vytvoríme triedu, ktorá rozširuje DiffUtil.Callback, ako je ukázané vo MyItemDiffCallback. Táto trieda definuje logiku porovnávania položiek.
  • Metódy Porovnávania: Implementujeme metódy getOldListSize, getNewListSize, areItemsTheSame a areContentsTheSame pre porovnanie starej a novej kolekcie položiek.
  • Vypočítanie Rozdielov: Používame DiffUtil.calculateDiff na vypočítanie najefektívnejšieho súboru operácií, ktoré prevedú starý zoznam na nový.
  • Aktualizácia Adaptéra: Po vypočítaní rozdielov aktualizujeme dáta v adaptéri a použijeme dispatchUpdatesTo na aplikovanie týchto zmien na adaptér.

Vďaka použitiu DiffUtil, adaptér vie presne, ktoré položky boli pridané, odstránené, alebo zmenené. To znižuje množstvo zbytočných operácií a zlepšuje výkon zobrazenia zoznamu, čím sa zároveň zvyšuje hladkosť a reaktívnosť užívateľského rozhrania.

  • areItemsTheSame: Táto metóda kontroluje, či sú dve položky rovnaké. Je to prvý krok pri porovnávaní dvoch objektov. Metóda porovnáva, či je identifikátor alebo nejaký iný jedinečný ukazovateľ rovnaký pre oba objekty. Ak sú identifikátory rovnaké, znamená to, že ide o rovnakú položku.
  • areContentsTheSame: Ak metóda areItemsTheSame vráti true, DiffUtil potom volá areContentsTheSame, aby zistil, či majú rovnaké položky aj rovnaké údaje. Táto metóda skúma, či sa obsah položiek zmenil. Ak sa obsah nezmenil, metóda by mala vrátiť true; v opačnom prípade vráti false.

V praxi, areItemsTheSame môže porovnávať identifikátory (alebo niektoré ine unikátne kľúče) položiek, zatiaľ čo areContentsTheSame môže porovnávať skutočný obsah. To umožňuje DiffUtil efektívne identifikovať a spracovať pridané, odstránené alebo zmenené položky.

Príklad použitia `DiffUtil` v `RecyclerView.Adapter`:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
    private var items: List<MyItem> = listOf()

    fun updateItems(newItems: List<MyItem>) {
        val diffCallback = MyItemDiffCallback(items, newItems)
        val diffResult = DiffUtil.calculateDiff(diffCallback)
        
        items = newItems
        diffResult.dispatchUpdatesTo(this)
    }

    // ... Zvyšok vášho kódu adaptéra ...
}

class MyItemDiffCallback(
    private val oldList: List<MyItem>, 
    private val newList: List<MyItem>
) : DiffUtil.Callback() {
    override fun getOldListSize() = oldList.size
    override fun getNewListSize() = newList.size
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition] == newList[newItemPosition]
    }
}
        

Použitím `DiffUtil`, môžete získať výhody automatických animácií a optimalizovaných aktualizácií, aj keď používate štandardný `RecyclerView.Adapter` namiesto `ListAdapter`.

Nezabudnite, ze je nevyhnutne upravit aj triedu MyItem v adaptri.

data class MyItem(val id: Int, val imageResource: Int, val text: String) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as MyItem

        if (id != other.id) return false
        if (imageResource != other.imageResource) return false
        if (text != other.text) return false

        return true
    }

    override fun hashCode(): Int {
        var result = id
        result = 31 * result + imageResource
        result = 31 * result + text.hashCode()
        return result
    }
}

2. Použitie ViewModelu a LiveData pre asynchrónne úlohy a uchovávanie údajov.

Pridanie závislostí do projektu

ViewModel je súčasťou knižnice architektúry Android Jetpack a je navrhnutý tak, aby uchovával a spravoval dáta súvisiace s UI (užívateľským rozhraním) nezávisle od životného cyklu aktivít a fragmentov. To znamená, že dáta uchované v ViewModel zostanú nezmenené pri zmene konfigurácie, ako je napríklad otočenie obrazovky.

  • Oddelenie logiky: ViewModel pomáha oddeliť logiku pre prácu s dátami od UI, čím uľahčuje testovanie a udržiavanie kódu.
  • Uchovávanie dát: Umožňuje uchovávať dáta, ktoré sú nezávislé od zmien životného cyklu komponentov UI, ako sú aktivita alebo fragment.
  • Spolupráca s LiveData: ViewModel často pracuje s LiveData, čo je iná súčasť Jetpacku. LiveData je pozorovateľný dátový držiak, ktorý je vedomý životného cyklu komponentov. To znamená, že môže automaticky aktualizovať dáta v UI v pravý čas.

Na používanie LiveData a ViewModel v projekte je potrebné pridať nasledujúce závislosti do súboru `build.gradle`:

dependencies {
    // ViewModel a LiveData
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.x.x"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.x.x"

}
        

Nezabudnite nahradiť "2.x.x" aktuálnou verziou knižnice. Môžete skontrolovať najnovšie verzie na oficiálnom Maven repozitári alebo na Android developer stránke.

1. Nastavenie LiveData s reťazcom (String) a jeho použitie vo Fragmente

Na začiatok si ukážeme, ako nastaviť LiveData s jednoduchým reťazcom (String) a ako túto hodnotu pozorovať a reagovať na jej zmeny vo Fragment.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class FeedViewModel : ViewModel() {
    private val _sampleString = MutableLiveData<String>()
    val sampleString: LiveData<String> get() = _sampleString

    fun updateString(value: String) {
        _sampleString.value = value
    }
}

V tomto príklade je LiveData `_sampleString` privátne, a je z neho vystavené iba čítanie cez `sampleString`. Metóda `updateString` umožňuje aktualizovať hodnotu LiveData.

Rozdiel medzi `.value` a `.postValue()`:

  • .value: Používa sa na nastavenie hodnoty LiveData z hlavného vlákna. Pri pokuse o nastavenie hodnoty z vedľajšieho vlákna môže spôsobiť chybu. Keď je hodnota nastavená pomocou `.value`, informuje všetkých aktívnych pozorovateľov o zmene hodnoty.
  • .postValue(): Používa sa na nastavenie hodnoty LiveData z vedľajšieho vlákna. Aktualizuje hodnotu asynchrónne a informuje všetkých aktívnych pozorovateľov o zmene, keď je to vykonané na hlavnom vlákne.

ViewModel sa nevytvára priamo v aktivite alebo vo fragmente. Namiesto toho sa používa ViewModelProvider, ktorý zabezpečí, že ViewModel nie je zbytočne re-created pri každej zmene konfigurácie a môže sa používať na zdieľanie dát medzi rôznymi fragmentmi a aktivitami.

Ak chcete túto hodnotu pozorovať vo Fragment, môžete to urobiť nasledovne:

class FeedFragment : Fragment(R.layout.fragment_feed) {
    private lateinit var viewModel: FeedViewModel

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel = ViewModelProvider(this)[FeedViewModel::class.java]
        // Pozorovanie zmeny hodnoty
        viewModel.sampleString.observe(viewLifecycleOwner, Observer { stringValue ->
            // Tu môžete aktualizovať UI podľa hodnoty stringValue
            Log.d("FeedFragment", "novy text: $stringValue")
        })

        viewModel.updateString("zmena textu")
    }
}

V príklade vyššie inicializujeme `viewModel` vo Fragment. Používame metódu `observe` na `sampleString`, aby sme mohli sledovať zmeny v hodnote a aktualizovať UI (napr. TextView) podľa tejto hodnoty.

2. Nastavenie LiveData s zoznamom položiek

Nasledujúci príklad ukazuje, ako nastaviť LiveData s Listom položiek. Predpokladáme, že máme definovanú triedu `MyItem` v adapteri pre položky v RecyclerView.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class FeedViewModel : ViewModel() {
    private val _feed_items = MutableLiveData<List<MyItem>>()
    val feed_items: LiveData<List<MyItem>> get() = _feed_items

    fun updateItems(items: List<MyItem>) {
        _feed_items.value = items
    }
}

Podobne ako v prvom príklade, máme privátnu MutableLiveData `_itemList` a vystavenú LiveData `itemList` na čítanie. Metóda `updateItemList` slúži na aktualizáciu zoznamu položiek.

3. Pozorovanie zmeny zoznamu a aktualizácia dát v RecyclerView

Ak chcete reagovať na zmeny v LiveData a aktualizovať RecyclerView, môžete to dosiahnuť takto:

class FeedFragment : Fragment(R.layout.fragment_feed) {
    private lateinit var viewModel: FeedViewModel

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        view.findViewById<BottomBar>(R.id.bottom_bar).setActive(BottomBar.FEED)

        // Inicializácia ViewModel
        viewModel = ViewModelProvider(this)[FeedViewModel::class.java]

        val recyclerView = view.findViewById<RecyclerView>(R.id.feed_recyclerview)
        recyclerView.layoutManager = LinearLayoutManager(context)
        val feedAdapter = FeedAdapter()
        recyclerView.adapter = feedAdapter

        // Pozorovanie zmeny hodnoty
        viewModel.feed_items.observe(viewLifecycleOwner) { items ->
            // Tu môžete aktualizovať UI podľa hodnoty stringValue
            feedAdapter.updateItems(items)
        }

        viewModel.updateItems(
            listOf(
                MyItem(0, R.drawable.baseline_feed_24, "Prvy"),
                MyItem(1, R.drawable.baseline_map_24, "Druhy"),
                MyItem(2, R.drawable.baseline_account_box_24, "Treti"),
            )
        )

    }
}

V tomto príklade inicializujeme `viewModel` a `adapter` pre RecyclerView. Používame metódu `observe` na `itemList` z ViewModel, aby sme sledovali zmeny v zozname položiek a potom aktualizovali RecyclerView pomocou `adapter.updateItems(items)`.

4. Zdieľanie ViewModel medzi fragmentmi

Jednou z výhod používania `ViewModel` je možnosť zdieľať jeho inštancie medzi viacerými fragmentmi v rámci jednej aktivity. Toto je obzvlášť užitočné, keď potrebujete zdieľať alebo komunikovať dáta medzi fragmentmi.

Postup, ako zdieľať `ViewModel` medzi fragmentmi:

  1. Vytvorte `ViewModel` triedu, ako obvykle.
  2. Pri vytváraní inštancie `ViewModel` v fragmente použite metódu `by activityViewModels()`, ktorá zabezpečí, že fragmenty získajú rovnakú inštanciu `ViewModel`.

Príklad:

class SharedViewModel : ViewModel() {
    val sharedData: MutableLiveData<String> = MutableLiveData()
}

class FirstFragment : Fragment(R.layout.fragment_first) {
    private val viewModel: SharedViewModel by activityViewModels()
    
    // ... zvyšok kódu ...
}

class SecondFragment : Fragment(R.layout.fragment_second) {
    private val viewModel: SharedViewModel by activityViewModels()
    
    // ... zvyšok kódu ...
}
        

Obe fragmenty teraz používajú tú istú inštanciu `SharedViewModel`, čo umožňuje jednoduché zdieľanie dát medzi nimi. Keď jedna z fragmentov zmení dáta v `SharedViewModel`, druhá ju môže okamžite sledovať a reagovať na túto zmenu.

5. Výhody použitia ViewModel s LiveData v porovnaní s ukladaním premenných vo Fragment

Použitie ViewModel spolu s LiveData prináša viacero výhod v porovnaní s tradičným prístupom ukladania dát priamo vo Fragment. Nasledujú niektoré z hlavných výhod:

  • Odolnosť voči zmenám konfigurácie: Keď sa zmení konfigurácia (napr. otočenie obrazovky), Fragment sa zničí a znovu vytvorí. S ViewModel, údaje zostávajú nezmenené a nestratia sa počas týchto zmien.
  • Oddelenie logiky: ViewModel poskytuje čisté oddelenie UI logiky a dát, čo robí kód organizovanejší a ľahšie testovateľný.
  • Automatická aktualizácia UI: LiveData informuje pozorovateľov o zmenách údajov automaticky. Nemusíte manuálne aktualizovať UI v prípade zmien v dátach.
  • Bezpečnosť: ViewModel umožňuje privátne ukladanie MutableLiveData a vystavuje iba nemeniteľné LiveData, čo zabezpečuje, že údaje nemôžu byť zmenené z iných častí aplikácie.
  • Zníženie rizika úniku pamäte: ViewModely sú navrhnuté tak, aby prežili zmeny životného cyklu, čo znižuje riziko úniku pamäte a zrýchľuje prácu s údajmi v rôznych častiach životného cyklu komponenty.

3. Kotlin Coroutines.

1. Základy Kotlin Coroutines

Kotlin Coroutines sú súčasťou knižnice Kotlin a ponúkajú jednoduchý a efektívny spôsob práce s asynchrónnym a neblokujúcim kódom v Kotlin. V praxi nám coroutines umožňujú písať asynchrónny kód, ktorý vyzerá ako synchrónny.

2. Vytvorenie CoroutineScope

Coroutines potrebujú rozsah (scope) na spustenie. `CoroutineScope` definuje životný cyklus coroutine:

val job = Job()
val scope = CoroutineScope(Dispatchers.Main + job)
        

V tomto príklade sme vytvorili nový rozsah so `Dispatchers.Main` a `Job` ako kontextom.

3. Spustenie Coroutine

Po vytvorení rozsahu môžete spustiť coroutine pomocou `launch` alebo `async`:

scope.launch {
    // Toto je blok coroutine
}
        

Coroutine sa vykonáva asynchrónne vo vopred definovanom rozsahu.

4. Práca s Dispatchers

`Dispatchers` určujú, na ktorom vlákne alebo zásobníku sa má coroutine spustiť. Najčastejšie používané sú:

  • `Dispatchers.Main`: hlavné vlákno UI.
  • `Dispatchers.IO`: optimalizované pre I/O operácie (napr. čítanie súborov, databázy).
  • `Dispatchers.Default`: optimalizované pre výpočty.
5. Suspendovanie funkcií

Suspendovanie funkcií sú funkcie, ktoré môžu asynchrónne výkonať dlhotrvajúcu operáciu a suspendovať sa bez blokovania vlákna. Deklarujú sa pomocou kľúčového slova `suspend`:

suspend fun fetchData(): DataType {
    // ... kód ...
}
        

Tieto funkcie je možné volať iba z coroutine alebo z iných suspendovacích funkcií.

6. Zrušenie Coroutine

Ak potrebujete zrušiť spustenú coroutine, môžete použiť `cancel()` na príslušnom `Job` objekte:

job.cancel()
        
7. Coroutines v ViewModel

Základné informácie:

`ViewModel` je ideálny pre prácu s Coroutines v kontexte Android aplikácií. V kombinácii s Coroutines, ViewModel poskytuje silnú základňu pre spracovanie dát v UI vrstve Androidu, najmä pri práci s asynchrónnymi operáciami.

Použitie viewModelScope:

`viewModelScope` je rozšírenie pre `ViewModel`, ktoré poskytuje `CoroutineScope` spojený s životným cyklom `ViewModel`. Keď `ViewModel` je zlikvidovaný (napr. pri zničení aktivity alebo fragmentu), všetky coroutines v tomto rozsahu sú automaticky zrušené.

class SampleViewModel : ViewModel() {
    
    fun fetchData() {
        viewModelScope.launch {
            // Váš asynchrónny kód tu
        }
    }

    override fun onCleared() {
        super.onCleared()
        // Žiadne rušenie potrebné, viewModelScope sa automaticky zruší
    }
}
        

Odporúčania:

Pri práci s Coroutines vo `ViewModel` je odporúčané použiť `viewModelScope`. Týmto predíďte potenciálnym problémom s únikom pamäte a zabezpečíte správne spracovanie životného cyklu asynchrónnych operácií.

8. Príklad: Generovanie náhodných čísel s Coroutines

Suspendovanie funkcie:

suspend fun fetchRandomNumber(): Int {
    delay(5000)
    return (0..10).random()
}
        

ViewModel:

class NumberViewModel : ViewModel() {
    private val _randomNumber = MutableLiveData<Int>()
    val randomNumber: LiveData<Int> get() = _randomNumber

    fun generateRandomNumber() {
        viewModelScope.launch {
            val number = fetchRandomNumber()
            _randomNumber.postValue(number)
        }
    }
}
        

Fragment:

class NumberFragment : Fragment() {
    private lateinit var viewModel: NumberViewModel

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_number, container, false)
        viewModel = ViewModelProvider(this).get(NumberViewModel::class.java)

        viewModel.randomNumber.observe(viewLifecycleOwner, Observer { number ->
            view.findViewById<TextView>(R.id.textViewNumber).text = number.toString()
        })

        view.findViewById<Button>(R.id.buttonGenerate).setOnClickListener {
            viewModel.generateRandomNumber()
        }

        return view
    }
}
        

Layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center">

    <TextView
        android:id="@+id/textViewNumber"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:text="0" />

    <Button
        android:id="@+id/buttonGenerate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Generate Random Number" />

</LinearLayout>
        

4. Komunikácia s REST webservisom pomocou Retrofitu a Gson na parsovanie JSONU.

1. Pridanie potrebných závislostí

Na začiatok pridajte potrebné závislosti pre Retrofit a Gson do vášho súboru build.gradle:

implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.code.gson:gson:2.10")
        

Retrofit je knižnica, ktorá umožňuje ľahko komunikovať s webovými službami v Androide. Gson je moderná knižnica pre spracovanie JSON v Jave a Kotlin, ktorá je efektívna a ľahko sa používa spolu s Retrofit.

2. Definícia API rozhrania

Retrofit pracuje na princípe definovania rozhrania pre váš web servis:

interface ApiService {
    @Headers("x-apikey: ...")
    @POST("user/create.php")
    suspend fun registerUser(@Body userInfo: UserRegistration): Response<RegistrationResponse>
}

data class UserRegistration(val name: String, val email: String, val password: String)
data class RegistrationResponse(val uid: String, val access: String, val refresh: String)
        

V tomto rozhraní sú definované koncové body webovej služby. Anotácie, ako je @POST, označujú akú HTTP metódu má metóda rozhrania reprezentovať. Retrofit automaticky prevedie váš požiadavok a odpoveď na a z JSON pomocou konvertora, v tomto prípade Moshi.

Prehľad annotácií Retrofit

Retrofit poskytuje množstvo annotácií, ktoré vám umožňujú efektívne definovať a prispôsobiť sieťové požiadavky. Tu je zoznam niektorých z nich:

  • @GET: Označuje metódu, ktorá by mala interpretovať HTTP GET požiadavku.
  • @POST: Označuje metódu, ktorá by mala interpretovať HTTP POST požiadavku.
  • @PUT: Pre HTTP PUT požiadavku.
  • @DELETE: Pre HTTP DELETE požiadavku.
  • @PATCH: Pre HTTP PATCH požiadavku.
  • @HEAD: Pre HTTP HEAD požiadavku.
  • @Path: Používa sa na označenie parametrov v URL. Napr. "@GET("users/{id}") fun getUser(@Path("id") userId: Int): Call<User>"
  • @Query: Používa sa na označenie query parametrov v URL. Napr. "@GET("users") fun getUsers(@Query("sort") order: String): Call<List<User>>"
  • @Body: Používa sa na označenie tela požiadavky. Napr. "@POST("users") fun createUser(@Body user: User): Call<User>"
  • @Header: Označuje parameter metódy ako header požiadavky.
  • @Headers: Používa sa na preddefinované headery pre požiadavku.
  • @FormUrlEncoded: Označuje, že požiadavka bude kódovaná v tvare x-www-form-urlencoded.
  • @Field: Používa sa s @FormUrlEncoded na označenie jedného kľúča-páru hodnoty vo formulári.

Keď očakávate, že API nebude vracať telo odpovede, môžete použiť Response<Void> ako typ návratovej hodnoty metódy. Toto vám umožní získať informácie o odpovedi (napr. stavový kód), ale bez tela:

@POST("users/logout")
fun logout(): Call<Response<Void>>
        

V tomto prípade môžete kontrolovať, či bola požiadavka úspešná, avšak telo odpovede bude prázdne.

3. Vytvorenie Retrofit klienta ako singleton

Na komunikáciu s vaším API potrebujete vytvoriť inštanciu Retrofit:

Rozhranie ApiService je časť aplikácie, ktorá sa stará o komunikáciu s webovými službami. Používa knižnicu Retrofit na zjednodušenie HTTP operácií. V tomto kóde máme:

  • Metódu registerUser, ktorá odošle údaje používateľa na server. Používa anotácie Retrofitu na definovanie hlavičiek a typu požiadavky.
  • companion object, ktorý obsahuje metódu create pre vytvorenie inštancie Retrofitu. Táto inštancia je nakonfigurovaná s základným URL a konvertorom Gson pre prácu s JSON dátami.

Keď aplikácia volá registerUser, Retrofit dynamicky vytvorí HTTP požiadavku, odošle ju na server a čaká na odpoveď, všetko zvládne asynchrónne vďaka integrácii s Kotlin korutínami.

interface ApiService {
    @Headers("x-apikey: ...")
    @POST("user/create.php")
    suspend fun registerUser(@Body userInfo: UserRegistration): Response<RegistrationResponse>

    companion object{
        fun create(): ApiService {

            val retrofit = Retrofit.Builder()
                .baseUrl("https://zadanie.mpage.sk/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()

            return retrofit.create(ApiService::class.java)
        }
    }
}
        

Využívaním objektu `ApiService.create()`, môžete získať prístup k `apiService`.

4. Jednotny zdroj dat v aplikácii.

Trieda DataRepository je zodpovedná za správu prístupu k dátam v aplikácii. Táto implementácia používa návrhový vzor Singleton, aby sa zaistilo, že existuje iba jedna inštancia triedy v celej aplikácii. Kľúčové body v kóde zahŕňajú:

  • private constructor: Zabraňuje priamej inštanciácii triedy zvonku.
  • companion object: Obsahuje logiku pre vytvorenie a získanie jedinečnej inštancie triedy.
  • @Volatile a synchronized: Tieto kľúčové slová sú použité na zabezpečenie, že operácia vytvorenia inštancie je bezpečná vo viacvláknovom prostredí.

getInstance(): Táto metóda overí, či už inštancia existuje; ak nie, vytvorí novú inštanciu DataRepository a táto nová inštancia sa uloží do INSTANCE.

Pre jednotny zdroj data v aplikácii budeme pouzivat DataRepository navrhovy vzor. Teda trieda, ktora bude implementovat logiku ci sa udaje ziskavaju z databazy alebo REST webservisu. A tiez bude obsahovat logiku ukladania ziskanich informacii z webservisu do lokalnej databazy.

class DataRepository private constructor(
    private val service: ApiService
) {
    companion object {
        const val TAG = "DataRepository"

        @Volatile
        private var INSTANCE: DataRepository? = null
        private val lock = Any()

        fun getInstance(): DataRepository =
            INSTANCE ?: synchronized(lock) {
                INSTANCE
                    ?: DataRepository(ApiService.create()).also { INSTANCE = it }
            }
    }
    
}
        

Využívaním objektu `DataRepository.getInstance()`, môžete získať prístup k DataRepository.

5. Registracia pouzivatela.

Ak chceme urobit prvu poziadavku na REST API pre registraciu pouzivatela mozeme pridat nasledovny kod do kodu DataRepository:


suspend fun apiRegisterUser(username: String, email: String, password: String) : Pair<String,User?>{
        if (username.isEmpty()){
            return Pair("Username can not be empty", null)
        }
        if (email.isEmpty()){
            return Pair("Email can not be empty", null)
        }
        if (password.isEmpty()){
            return Pair("Password can not be empty", null)
        }
        try {
            val response = service.registerUser(UserRegistration(username, email, password))
            if (response.isSuccessful) {
                response.body()?.let { json_response ->
                    return Pair("", User(username,email,json_response.uid, json_response.access, json_response.refresh))
                }
            }
            return Pair("Failed to create user", null)
        }catch (ex: IOException) {
            ex.printStackTrace()
            return Pair("Check internet connection. Failed to create user.", null)
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
        return Pair("Fatal error. Failed to create user.", null)
}
        

Funkcia apiRegisterUser vykonáva registráciu používateľa prostredníctvom API. Vykonáva kontrolu vstupných údajov a spracováva odpoveď zo servera. Tu je stručný opis toho, ako funguje:

  • Kontroluje, či sú reťazce username, email, a password neprázdne. Vracia chybové správy, ak je niektorý z nich prázdny.
  • Používa metódu registerUser zo služby ApiService na poslanie požiadavky na registráciu používateľa.
  • Spracováva odpoveď zo servera. Ak je odpoveď úspešná a obsahuje telo, vytvorí nového používateľa s údajmi z odpovede a vráti ho.
  • V prípade chyby počas komunikácie s API alebo inej výnimky vráti chybovú správu a null pre používateľa.

Funkcia efektívne rieši rôzne scenáre, ktoré môžu nastať pri komunikácii so serverom, a zabezpečuje, aby boli chyby adekvátne ošetrené a komunikované späť volajúcej strane.

6. ViewModel implementácia s apiService ako argumentom

Použite Retrofit klienta v ViewModeli pre spracovanie požiadaviek:

class AuthViewModel(private val dataRepository: DataRepository) : ViewModel() {
    private val _registrationResult = MutableLiveData<Pair<String,User?>>()
    val registrationResult: LiveData<Pair<String,User?>> get() = _registrationResult

    fun registerUser(username: String, email: String, password: String) {
        viewModelScope.launch {
            _registrationResult.postValue(dataRepository.apiRegisterUser(username, email, password))            
        }
    }
}
        

ViewModel teraz prijíma DataRepository ako argument, čo znamená, že ho môžete poskytnúť pri vytváraní inštancie ViewModelu. To umožňuje väčšiu pružnosť a lepšie testovateľnosť.

5. Observácia LiveData vo Fragmente

RegistrationFragment je súčasťou užívateľského rozhrania, ktorá zodpovedá za registráciu nových používateľov. Tento fragment využíva ViewModel pre správu a uchovanie stavu registrácie. Nasleduje podrobný popis jeho funkcionality:

  • Fragment sa spolieha na RegistrationViewModel pre správu dát a logiky registrácie.
  • V metóde onViewCreated sa inicializuje viewModel pomocou ViewModelProvider, ktorý vytvorí inštanciu AuthViewModel s potrebným DataRepository.
  • Fragment sleduje registrationResult v viewModel pomocou metódy observe. V prípade úspešnej registrácie naviguje na ďalší fragment pomocou findNavController().navigate. Ak registrácia zlyhá, zobrazí chybovú správu pomocou Snackbar.
  • Kliknutím na tlačidlo "Submit" sa spustí metóda registerUser z viewModel, ktorá odošle zadané údaje (meno používateľa, email, heslo) do metódy registerUser v AuthViewModel.

Tento kód efektívne oddeluje logiku užívateľského rozhrania od spracovania dát a biznis logiky, čím zjednodušuje údržbu a testovanie.

class RegistrationFragment : Fragment() {

    private lateinit var viewModel: RegistrationViewModel


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel = ViewModelProvider(requireActivity(), object : ViewModelProvider.Factory {
            override fun <T : ViewModel< create(modelClass: Class<T<): T {
                return AuthViewModel(DataRepository.getInstance()) as T
            }
        })[AuthViewModel::class.java]

        viewModel.registrationResult.observe(viewLifecycleOwner){
            if (it.second != null){
                requireView().findNavController().navigate(R.id.action_signup_feed)
            }else{
                Snackbar.make(
                    view.findViewById(R.id.submit_button),
                    it.first,
                    Snackbar.LENGTH_SHORT
                ).show()
            }
        }

        view.findViewById<TextView<(R.id.submit_button).apply {
            setOnClickListener {
                viewModel.registerUser(
                    view.findViewById<EditText<(R.id.edit_text_username).text.toString(),
                    view.findViewById<EditText<(R.id.edit_text_email).text.toString(),
                    view.findViewById<EditText<(R.id.edit_text_email).text.toString()
                )
            }
        }
    }
}
        
6. Povolenie pre pripojenie sa na internet

Uz pravdepodobne mate pridane povolenie na komunikaciu cez internet, kvoli mape, ktoru ste pridavali.

<manifest ... >		
<uses-permission android:name="android.permission.INTERNET" />
<application
...
</manifest>
        

Treba mat v AndroidManifest.xml.

5. Prihlasenie sa do uctu s REST webservisom pomocou Retrofitu a Gson na parsovanie JSONU.

1. Prihlasenie sa so svojim uctom

Pouzite rovnaky sposob ako pri registracii na prihlasenie sa do uctu cez REST API.


Odporúčané Kurzy, pre cvičenie (optimálne si vyskúšať všetky kroky v kurze)

Najmä tieto Codelaby: