Úlohy pre 6. cvičenie

  • 1. Použitie ViewModelu a LiveData pre asynchrónne úlohy a uchovávanie údajov.
  • 2. Kotlin Coroutines.
  • 3. Komunikácia s REST webservisom pomocou Retrofitu a Gson na parsovanie JSONU.
  • 4. Pridanie SQLite databazy

Ukazkovy kod mozete najst na https://github.com/marosc/mobv24


Hodnotenie

Prve hodnotenie aplikacii bude na cviceni 6.11.2025, kde treba mat hotove vsetky ulohy pre moznost ziskania max. poctu bodov.


1. 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(libs.androidx.lifecycle.viewmodel.ktx)
    implementation(libs.androidx.lifecycle.livedata.ktx)

}
        
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.

2. 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

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

dependencies {
    implementation(libs.kotlinx.coroutines.android)

}
        

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>
        

3. 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(libs.retrofit)
    implementation(libs.converter.gson)
    implementation(libs.gson)
        

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.


4. Pridanie SQLite databazy

1. Pridanie závislostí do projektu

Room je časť Android Architecture Components, ktoré poskytujú robustnú databázovú vrstvu pre vaše aplikácie. Kotlin Coroutines ponúka elegantný spôsob práce s asynchrónnymi operáciami, čo je ideálne pre databázové operácie, ktoré môžu trvať dlhšie a mali by sa vykonávať mimo hlavného vlákna.

Pridajte nasledujúce závislosti do vášho `build.gradle` (Module: app):

val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
kapt("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
Na začiatok súboru pridajte ešte aj :
plugins {
	alias(libs.plugins.android.application)
	alias(libs.plugins.kotlin.android)
	id("kotlin-kapt")      < ---- tento riadok 
}
Po pridaní závislostí kliknite na 'Sync Now' v Android Studio, aby sa závislosti stiahli a integrovali do vášho projektu. Keď je táto závislosť pridaná, môžete v DAO definovať metódy, ktoré vrátia `suspend` funkcie. Použitím `suspend` funkcií s Room a coroutines môžete jednoducho a bezpečne pristupovať k databáze mimo hlavného vlákna.

2. Vytvorenie entít

Entita je základný stavebný kameň Room databázy, ktorý reprezentuje tabuľku. Každá entita sa mapuje na tabuľku v databáze.
@Entity(tableName = "users")
class UserEntity(
    @PrimaryKey val uid: String,
    val name: String,
    val updated: String,
    val lat: Double,
    val lon: Double,
    val radius: Double,
    val photo: String = ""
)
        
V tomto príklade je trieda `UserEntity` mapovaná na tabuľku s názvom "users". Atribút `@PrimaryKey` definuje, ktorý stĺpec je primárnym kľúčom v tejto tabuľke.

3. Vytvorenie DAO (Data Access Object)

DAO je rozhranie, ktoré definuje všetky potrebné metódy pre interakciu s databázou.

@Dao
interface DbDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUserItems(items: List<UserEntity>)

    @Query("select * from users where uid=:uid")
    fun getUserItem(uid: String): LiveData<UserEntity?>

    @Query("select * from users")
    fun getUsers(): LiveData<List<UserEntity?>>

    @Query("delete from users")
    suspend fun deleteUserItems()

}

Tu `@Query` definuje SQL dotaz, `@Insert` je pre vkladanie nových záznamov. Potom existuje aj `@Update` pre aktualizáciu existujúcich záznamov a `@Delete` pre vymazanie záznamov.

4. Vytvorenie databázy

Na centralizáciu databázovej logiky vytvorte abstraktnú triedu, ktorá rozširuje `RoomDatabase`.

@Database(
    entities = [
        UserEntity::class
    ],
    version = 1,
    exportSchema = false
)
abstract class AppRoomDatabase : RoomDatabase() {

    abstract fun appDao(): DbDao

    companion object {
        @Volatile
        private var INSTANCE: AppRoomDatabase? = null

        fun getInstance(context: Context): AppRoomDatabase =
            INSTANCE ?: synchronized(this) {
                INSTANCE
                    ?: buildDatabase(context).also { INSTANCE = it }
            }

        private fun buildDatabase(context: Context) =
            Room.databaseBuilder(
                context.applicationContext,
                AppRoomDatabase::class.java, "treeCamDB"
            ).fallbackToDestructiveMigration()
                .build()
    }
}

@Database je anotácia, ktorá označuje triedu ako Room databázu. Má nasledujúce atribúty:
  • entities: Zoznam všetkých entít, ktoré budú v databáze reprezentované ako tabuľky. V tomto prípade obsahuje iba UserEntity::class.
  • version: Verzia databázy. Keď urobíte zmeny v štruktúre databázy, mali by ste zvýšiť túto hodnotu.
  • exportSchema: Určuje, či má byť schéma databázy exportovaná do súboru pre kontrolu. V tomto prípade je nastavené na false.
abstract class AppRoomDatabase : RoomDatabase() - Táto trieda je abstraktná definícia databázy, ktorá rozširuje RoomDatabase.

abstract fun appDao(): DbDao - Táto metóda deklaruje, že naša databáza bude obsahovať DAO s názvom DbDao. Týmto spôsobom môžeme získať prístup k metódam DAO, keď máme inštanciu databázy.

companion object je špeciálny objekt v Kotlin, ktorý obsahuje metódy a premenné, ktoré môžu byť volané na úrovni triedy.

  • @Volatile: Označuje premennú INSTANCE tak, aby zmeny v tejto premennej boli okamžite viditeľné všetkým ostatným vláknam.
  • getInstance: Táto metóda zabezpečuje, že máme iba jednu inštanciu AppRoomDatabase v celej aplikácii (tzv. Singleton vzor). Ak INSTANCE nie je nastavená, volá sa buildDatabase a vytvorí sa nová inštancia databázy.
  • buildDatabase: Táto metóda vytvára novú inštanciu AppRoomDatabase použitím Room.databaseBuilder(). fallbackToDestructiveMigration() zabezpečuje, že ak dojde k nezlučiteľným zmenám v databázovej štruktúre medzi migráciami, stávajúca databáza sa zmaže a vytvorí sa znova namiesto toho, aby aplikácia spadla.

5. Vrstva LocalCache

V architektúre aplikácie je často užitočné mať medzivrstvu medzi DAO (Data Access Object) a Repository vrstvou. Táto vrstva sa môže volať LocalCache. Hlavným účelom tejto vrstvy je poskytnúť flexibilitu pri práci s databázou a umožniť zmeny v logike práce s databázou bez toho, aby to ovplyvnilo Repository alebo zvyšok aplikácie.
  • Oddelenie logiky: Keď chcete zmeniť spôsob, akým sa údaje ukladajú alebo získavajú z databázy, môžete to jednoducho urobiť v LocalCache vrstve bez toho, aby ste menili DAO alebo Repository.
  • Zlepšená testovateľnosť: S oddelenou vrstvou LocalCache môžete jednoducho napísať jednotkové testy pre túto vrstvu, čím zabezpečíte, že logika ukladania a načítavania údajov funguje správne.
  • Flexibilita: Ak sa rozhodnete zmeniť databázový nástroj alebo migráciu na iný druh úložiska, vrstva LocalCache vám umožní tieto zmeny uskutočniť bez veľkého vplyvu na zvyšok aplikácie.
  • Optimalizácia: Vrstva LocalCache môže tiež zahrňovať optimalizácie, ako sú dávkové operácie alebo medzipamäťovanie, aby sa zvýšila rýchlosť a výkon práce s databázou.
V závere, pridanie vrstvy LocalCache medzi DAO a Repository vám poskytne väčšiu kontrolu a flexibilitu nad tým, ako vaša aplikácia interaguje s databázou, a zároveň zachová čistotu a organizáciu vášho kódu.

class LocalCache(private val dao: DbDao) {

    suspend fun logoutUser() {
        deleteUserItems()
    }

    suspend fun insertUserItems(items: List<UserEntity>) {
        dao.insertUserItems(items)
    }

    fun getUserItem(uid: String): LiveData<UserEntity?> {
        return dao.getUserItem(uid)
    }

    fun getUsers(): LiveData<List<UserEntity?>> = dao.getUsers()

    suspend fun deleteUserItems() {
        dao.deleteUserItems()
    }

}

6. Komunikacia s databazou cez DataRepository

Trieda DataRepository predstavuje kľúčový komponent v architektúre vašej aplikácie. Slúži ako stredná vrstva medzi zdrojmi údajov (v tomto prípade API a lokálnou databázou) a zvyškom vašej aplikácie.

Hlavným účelom tejto triedy je poskytnúť ústredné miesto pre získavanie údajov, či už ide o načítavanie údajov z API alebo ich získavanie z lokálnej databázy. Týmto spôsobom, ak by ste potrebovali zmeniť logiku získavania údajov, môžete to urobiť na jednom mieste, bez toho, aby ste museli meniť mnoho častí aplikácie.

Vďaka Singleton dizajnovému vzoru máte zabezpečené, že v rámci celej aplikácie existuje iba jedna inštancia DataRepository. To zabezpečuje konzistentnosť údajov a minimalizuje riziko chýb spojených s viacnásobným prístupom k zdrojom údajov.

Metóda apiListGeofence() je zodpovedná za komunikáciu s API a zabezpečuje, že údaje sú správne načítané a následne uložené do lokálnej databázy. Na druhej strane metóda getUsers() poskytuje rýchly prístup k údajom uloženým v databáze.




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

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

        fun getInstance(context: Context): DataRepository =
            INSTANCE ?: synchronized(lock) {
                INSTANCE
                    ?: DataRepository(
                        ApiService.create(context),
                        LocalCache(AppRoomDatabase.getInstance(context).appDao())
                    ).also { INSTANCE = it }
            }
    }  
    suspend fun apiListGeofence(): String {
        try {
            val response = service.listGeofence()

            if (response.isSuccessful) {
                response.body()?.let {
                    val users = it.map {
                        UserEntity(
                            it.uid, it.name, it.updated,
                            it.lat, it.lon, it.radius, it.photo
                        )
                    }
                    cache.insertUserItems(users)
                    return ""
                }
            }

            return "Failed to load users"
        } catch (ex: IOException) {
            ex.printStackTrace()
            return "Check internet connection. Failed to load users."
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
        return "Fatal error. Failed to load users."
    }

    fun getUsers() = cache.getUsers()
}

Trieda DataRepository je dizajnovaná ako Singleton, čo znamená, že v rámci aplikácie bude existovať iba jedna inštancia tejto triedy. Je zodpovedná za získavanie údajov z API a ukladanie ich do lokálnej databázy pomocou LocalCache.
  • service: Inštancia API služby.
  • cache: Lokálna medzipamäť (LocalCache) pre prácu s databázou.
companion object obsahuje metódy a premenné, ktoré môžu byť volané na úrovni triedy.
  • @Volatile: Označuje premennú INSTANCE tak, aby zmeny v tejto premennej boli okamžite viditeľné všetkým ostatným vláknam.
  • getInstance: Táto metóda zabezpečuje, že máme iba jednu inštanciu DataRepository v celej aplikácii.
suspend fun apiListGeofence() funkcia je určená na získavanie údajov z API služby. Po úspešnom získaní údajov sa tieto údaje prevedú na entity a uložia sa do databázy pomocou LocalCache.
  • try-catch: Ošetruje možné chyby počas komunikácie s API alebo pri iných chybách.
  • response.isSuccessful: Kontroluje, či bola odpoveď od API úspešná.
  • cache.insertUserItems(users): Ukladá získané údaje do databázy.
getUsers() metóda vráti zoznam používateľov uložených v lokálnej databáze. Je to priamy prístup k LocalCache pre získanie uložených údajov.

7. Nacitavanie a zobrazovanie data pouzivatelovi ( z Databazy/API )

Trieda FeedViewModel predstavuje jednu z kľúčových súčastí architektúry MVVM (Model-View-ViewModel) v Android aplikáciách. Jej úloha spočíva v spracovaní a poskytovaní údajov pre užívateľské rozhranie a správe užívateľských interakcií s týmito údajmi.

Prístup k dátam prostredníctvom repozitára: Hlavnou výhodou použitia repozitára v kombinácii s ViewModelom je oddeľovanie logiky získavania dát od logiky zobrazenia. V triede FeedViewModel je repozitár využívaný ako most medzi databázou a API službou. Keď si užívateľ vyžiada aktualizáciu údajov, ViewModel prostredníctvom repozitára okamžite získava dáta z lokálnej databázy a súčasne inicializuje požiadavku na získanie aktualizovaných údajov z API.

Princíp práce s dátami: Základným princípom je poskytnúť užívateľovi okamžitý prístup k dátam z lokálnej databázy, čím sa minimalizuje čakanie. Po načítaní dát z API sú tieto údaje synchronizované a aktualizované v lokálnej databáze, čo následne ovplyvní zobrazenie v užívateľskom rozhraní. Týmto spôsobom sa dosahuje kombinácia rýchlosti (vďaka lokálnym údajom) a aktuálnosti (vďaka online údajom z API).

Indikácia načítavania a správa o chybách: Aby užívateľ bol v obraze o tom, čo sa deje v aplikácii, ViewModel obsahuje logiku na zobrazovanie stavu načítavania a prípadných chybových hlásení. Premenná loading sleduje, či práve prebieha načítavanie údajov. Funkcia updateItems() sa postará o celý proces aktualizácie údajov – od zahájenia načítavania, cez spracovanie chýb až po ukončenie načítavania.

Výhody lokálnej databázy: Lokálna databáza je kľúčovým prvkom pre zabezpečenie plynulosti aplikácie v rôznych sieťových podmienkach. Keď je internetové pripojenie slabé alebo dokonca nedostupné, užívateľ stále môže pracovať s údajmi, ktoré boli naposledy synchronizované. Toto riešenie zabezpečuje, že aplikácia je oveľa viac odolná voči nestabilitám siete a poskytuje užívateľovi konzistentné skúsenosti nezávisle od kvality internetového pripojenia.

Na záver, trieda FeedViewModel demonštruje silnú kombináciu MVVM architektúry, repozitára a lokálnej databázy, vďaka čomu je možné efektívne a efektívne pracovať s údajmi v modernej Android aplikácii.


class FeedViewModel(private val repository: DataRepository) : ViewModel() {

    val feed_items: LiveData<List<UserEntity?>> =
        liveData {
            loading.postValue(true)
            repository.apiListGeofence()
            loading.postValue(false)
            emitSource(repository.getUsers())
        }

    val loading = MutableLiveData(false)

    private val _message = MutableLiveData<Evento<String>>()
    val message: LiveData<Evento<String>>
        get() = _message

    fun updateItems() {
        viewModelScope.launch {
            loading.postValue(true)
            _message.postValue(Evento(repository.apiListGeofence()))
            loading.postValue(false)
        }
    }
}