Cvičenie 6 - REST, Databaza
Ú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.
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)
}
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.
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.
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)`.
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:
- Vytvorte `ViewModel` triedu, ako obvykle.
- 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.
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.
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.
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.
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.
`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.
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í.
Ak potrebujete zrušiť spustenú coroutine, môžete použiť `cancel()` na príslušnom `Job` objekte:
job.cancel()
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í.
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.
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.
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.
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.
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óducreatepre 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`.
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.@Volatileasynchronized: 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.
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, apasswordneprázdne. Vracia chybové správy, ak je niektorý z nich prázdny. - Používa metódu
registerUserzo službyApiServicena 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
nullpre 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.
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ť.
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
RegistrationViewModelpre správu dát a logiky registrácie. - V metóde
onViewCreatedsa inicializujeviewModelpomocouViewModelProvider, ktorý vytvorí inštanciuAuthViewModels potrebnýmDataRepository. - Fragment sleduje
registrationResultvviewModelpomocou metódyobserve. V prípade úspešnej registrácie naviguje na ďalší fragment pomocoufindNavController().navigate. Ak registrácia zlyhá, zobrazí chybovú správu pomocouSnackbar. - Kliknutím na tlačidlo "Submit" sa spustí metóda
registerUserzviewModel, ktorá odošle zadané údaje (meno používateľa, email, heslo) do metódyregisterUservAuthViewModel.
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()
)
}
}
}
}
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
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
@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
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
@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ú
INSTANCEtak, 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
AppRoomDatabasev celej aplikácii (tzv. Singleton vzor). AkINSTANCEnie je nastavená, volá sabuildDatabasea vytvorí sa nová inštancia databázy. - buildDatabase: Táto metóda vytvára novú inštanciu
AppRoomDatabasepoužitímRoom.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
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
LocalCachevrstve bez toho, aby ste menili DAO alebo Repository. - Zlepšená testovateľnosť: S oddelenou vrstvou
LocalCachemôž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
LocalCachevám umožní tieto zmeny uskutočniť bez veľkého vplyvu na zvyšok aplikácie. - Optimalizácia: Vrstva
LocalCachemôž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.
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
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ú
INSTANCEtak, 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
DataRepositoryv 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 )
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)
}
}
}