Cvičenie 5,6 - LiveData, REST
Úlohy pre 5. 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.
Ukazkovy kod mozete najst na https://github.com/marosc/mobv24
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óducreate
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`.
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
asynchronized
: 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
, apassword
neprázdne. Vracia chybové správy, ak je niektorý z nich prázdny. - Používa metódu
registerUser
zo službyApiService
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.
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
RegistrationViewModel
pre správu dát a logiky registrácie. - V metóde
onViewCreated
sa inicializujeviewModel
pomocouViewModelProvider
, ktorý vytvorí inštanciuAuthViewModel
s potrebnýmDataRepository
. - Fragment sleduje
registrationResult
vviewModel
pomocou 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
registerUser
zviewModel
, ktorá odošle zadané údaje (meno používateľa, email, heslo) do metódyregisterUser
vAuthViewModel
.
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.
5. Prihlasenie sa do uctu s REST webservisom pomocou Retrofitu a Gson na parsovanie JSONU.
Pouzite rovnaky sposob ako pri registracii na prihlasenie sa do uctu cez REST API.