Úlohy pre 5. cvičenie
- 1. DataBinding
- 2. Jednosmerný, Obojsmerný DataBinding
- 3. SharedPreferences
- 4. OAuth autorizacia REST API poziadaviek s ulozenym tokenom
- 5. Pokročilá autorizácia s Retrofit a OkHttpClient
- 6. Ziskanie povolenia pre zistovanie GPS polohy
- 7. Zobrazenie GPS polohy na mape
Hodnotenie
Za splnenie prvých 2 úloh celkovo 1 bod.
Za splnenie úlohy 3. celkovo 1 bod.
Za splnenie úloh 4 a 5 celkovo 1 bod.
Za splnenie úloh 6 a 7 celkovo 1 bod.
1. DataBinding
5.1.1 DataBinding v Android Aplikáciách
DataBinding je technika, ktorá umožňuje väzbu medzi vašimi UI komponentami v layout súborech a dátovými zdrojmi vo vašej aplikácii, použitím deklaratívneho formátu namiesto programovania. Táto knižnica ponúka spôsoby, ako aplikácii priradiť dáta tak, že UI komponenty nie sú naplnené dátami ručne v kóde.
- Automatizácia aktualizácie UI: S DataBinding, aktualizácie vašich UI komponentov sú automatizované. Akonáhle sú dáta zmenené, zobrazenie sa aktualizuje automaticky, čo znižuje potrebu volania "setText()" alebo "findViewById()".
- Zníženie množstva kódu: Pomáha redukovať množstvo kódu v aplikácii, čo vedie k zvýšeniu čitateľnosti a zníženiu pravdepodobnosti chýb.
- Previazanie dát: Umožňuje modelovať dáta priamo s UI komponentmi v XML layout súbore, čo zjednodušuje prácu s dátami v UI.
- Expression Language: Knižnica poskytuje vlastný jazyk pre výrazy (Expressions), ktorý umožňuje manipuláciu s dátami priamo v XML súbore.
- Typová kontrola v čase kompilácie: Keďže väzby sú vyhodnotené v čase kompilácie, chyby sú ľahšie nájdené a opravené, pretože IDE ich môže identifikovať ešte pred spustením aplikácie.
Pre použitie DataBinding v projekte je potrebné pridať dataBinding
do vášho build.gradle súboru. Potom môžete začať používať <layout>
tag vo vašich XML layout súboroch a vytvoriť váš DataBinding objekt v kóde vašich aktivít alebo fragmentov.
build.gradle.kts android { namespace = "eu.mcomputing.mobv.mobvzadanie" compileSdk = 33 ... buildFeatures { dataBinding = true } ...
5.1.2 Použitie DataBinding v XML Layoutoch
DataBinding v Android aplikáciách vyžaduje špeciálnu štruktúru v XML layout súboroch. Táto štruktúra zahŕňa použitie <layout>
tagu a vnoreného <data>
tagu, ktorý umožňuje prepojenie dátových modelov s UI elementmi.
- <layout> tag: Všetky XML layout súbory, ktoré chcú využívať DataBinding, musia byť obalené v
<layout>
tagu. Tento tag signalizuje, že layout bude používať databinding a umožňuje Android Studio generovať väzobné triedy. - <data> tag: Vnorený v
<layout>
tagu,<data>
tag definuje dáta, ktoré budú použité v layoute. Môžete tu špecifikovať premenné, ktoré budú reprezentovať modely alebo logiku, ktorú chcete používať v UI. - Použitie premenných: Premenné definované v
<data>
tagu sa môžu používať priamo v layoute na nastavenie hodnôt UI komponentov alebo na manipuláciu s UI elementmi. - Automatická aktualizácia UI: Keď sú dáta v modeloch aktualizované, zmeny sa automaticky prejavia v príslušných UI komponentoch, bez nutnosti manuálneho zasahovania alebo volania metód ako
setText()
alebofindViewById()
.
Príklad použitia v XML layoute:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.example.MyViewModel" />
</data>
<-- Váš UI layout... -->
</layout>
Tento prístup umožňuje efektívne a prehľadne oddeliť logiku aplikácie od jej UI, čo zjednodušuje údržbu kódu a podporuje lepšiu organizáciu projektu.
5.1.3 Použitie DataBinding v Fragmentoch spolu s ViewModelom
DataBinding je mocný nástroj, ktorý poskytuje väzbu medzi UI komponentami v layoutoch a dátovými modelmi, logikou alebo ViewModelmi vo vašej aplikácii. V kombinácii s ViewModelom, DataBinding prispieva k udržateľnosti, testovateľnosti a zníženiu množstva boilerplate kódu vo vašich fragmentoch.
- Inicializácia DataBinding: V rámci metódy
onCreateView
vo vašom fragmente sa inicializuje DataBinding. Tento proces zahŕňa inflatovanie layoutu fragmentu a následne pripojenie k ViewModelu. - Pripojenie k ViewModelu: Vytvoríte inštanciu ViewModelu pomocou
ViewModelProvider
. Potom nastavíte ViewModel pre váš DataBinding. Týmto sa ViewModel stane dostupným vo vašom XML layoute. - Aktualizácia dát: Keď sa dáta vo ViewModeli zmenia, UI sa automaticky aktualizuje vďaka observables alebo LiveData objektom, bez nutnosti manuálneho zasahovania.
- Bezpečnostný aspekt: Použitie DataBindingu znižuje pravdepodobnosť vzniku chýb v runtime, ako sú NullPointerExceptions pri práci s UI komponentami, pretože väčšina týchto interakcií sa deje v compile-time.
Príklad použitia v kóde fragmentu:
class FeedFragment : Fragment(R.layout.fragment_intro) { private var binding: FragmentFeedBinding? = null private lateinit var viewModel: FeedViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(requireActivity())[FeedViewModel::class.java] } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentFeedBinding.bind(view).apply { lifecycleOwner = viewLifecycleOwner model = viewModel }.also { bnd-> bnd.btnGenerate.setOnClickListener { viewModel.updateItems() } } } override fun onDestroyView() { binding = null super.onDestroyView() } }
Tento prístup zjednodušuje kód vo fragmentoch, umožňuje lepšiu organizáciu a znižuje riziko chýb spojených s prácou s UI elementmi.
2. Jednosmerný, Obojsmerný DataBinding
5.2 Jednosmerný, Obojsmerný DataBinding a udalosti na kliknutia v XML Layoute
DataBinding v Android aplikáciách umožňuje priamu väzbu UI komponentov v XML layoutoch s dátovými zdrojmi v kóde aplikácie. Poskytuje možnosti pre jednosmerný (one-way) a obojsmerný (two-way) databinding, ako aj pridávanie udalosti na kliknutia priamo do XML.
Jednosmerný DataBinding
- Definícia: Jednosmerný databinding aktualizuje UI komponenty keď sa zmenia dáta. Zmeny v UI neovplyvňujú dátové zdroje.
- Použitie: Používa sa najmä pre zobrazenie informácií, kde interakcia používateľa nie je potrebná alebo nevedie k zmene dát.
- Príklad: Môžete zobraziť meno používateľa zo ViewModelu v TextView pomocou
@{viewModel.userName}
.
Obojsmerný DataBinding
- Definícia: Obojsmerný databinding umožňuje komunikáciu medzi UI a dátovým zdrojom v oboch smeroch. Ak sa zmení UI, zmení sa aj dátový zdroj a naopak.
- Použitie: Ideálne pre formuláre alebo interaktívne rozhrania, kde zmeny vykonané používateľom musia byť odrážané v dátovom modeli.
- Príklad: V prípade, že chcete, aby zmeny v EditText boli automaticky odrážané v vašom ViewModeli, použijete
@={viewModel.userInput}
.
Pridanie udalosti na kliknutia v XML
- Definícia: DataBinding umožňuje definovať udalosti na kliknutia, priamo v XML layoute.
- Použitie: Umožňuje vyvolanie metód z ViewModelu alebo akéhokoľvek pridruženého dátového zdroja priamo z UI elementu, bez potreby definovania listenera v Java alebo Kotlin kóde.
- Príklad: Môžete pridať udalosti na kliknutia k tlačidlu takto:
android:onClick="@{() -> viewModel.onButtonClick()}"
alebo pre prenos parametrovandroid:onClick="@{(view) -> viewModel.onItemClick(item)}"
.
V oboch prípadoch databindingu a pri definovaní udalosti na kliknutia musíte vytvoriť vhodnú premennú alebo metódu v triede ViewModel alebo inom dátovom zdroji, ktorá sa použije pre databinding vo vašom XML. Taktiež musíte deklarovať databinding vo vašom XML layoute pomocou <layout>
tagu a definovať premenné v jeho <data>
sekcii.
Použitie jednosmerného a obojsmerného databindingu, spolu s definovaním udalosti na kliknutia priamo v XML, efektívne znižuje množstvo boilerplate kódu, zvyšuje čitateľnosť a udržateľnosť kódu a zjednodušuje prácu s UI aktualizáciami a interakciami používateľa.
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="model" type="eu.mcomputing.mobv.mobvzadanie.viewmodels.AuthViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="20dp"> <EditText android:id="@+id/edit_text_username" android:layout_width="0dp" android:layout_height="wrap_content" android:hint="@string/zadajte_username" android:text="@={model.username}" app:layout_constraintTop_toBottomOf="@+id/label_username" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> <Button android:id="@+id/submit_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/prihlasit_sa" android:onClick="@{()->model.loginUser()}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
class AuthViewModel(private val dataRepository: DataRepository) : ViewModel() { ... val username = MutableLiveData<String>() val email = MutableLiveData<String>() val password = MutableLiveData<String>() val repeat_password = MutableLiveData<String>() ... fun loginUser() { viewModelScope.launch { val result = dataRepository.apiLoginUser(username.value?:"", password.value?:"") _loginResult.postValue(result.first ?: "") _userResult.postValue(result.second) } } }
class LoginFragment : Fragment(R.layout.fragment_login) { ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentLoginBinding.bind(view).apply { lifecycleOwner = viewLifecycleOwner model = viewModel }.also { bnd -> viewModel.loginResult.observe(viewLifecycleOwner) { if (it.isEmpty()) { requireView().findNavController().navigate(R.id.action_login_feed) } else { Snackbar.make( bnd.submitButton, it, Snackbar.LENGTH_SHORT ).show() } } } } ... }
3. SharedPreferences
Android SharedPreferences
SharedPreferences je mechanizmus v systéme Android na ukladanie jednoduchých párov kľúč-hodnota a je bežne používaný na ukladanie primitívnych typov dát. Táto možnosť ukladania je vhodná na ukladanie malých množstiev dát, ako sú nastavenia používateľa, konfigurácie aplikácií alebo používateľskej relácie.
1. Inicializácia:
- Prístup: K SharedPreferences môžete pristupovať prostredníctvom ľubovoľného kontextu v systéme Android (napríklad Activity, Service alebo BroadcastReceiver). Najčastejšie sa však používa metóda
getSharedPreferences()
alebogetPreferences()
. - Súbory: SharedPreferences ukladá dáta do súborov XML v priečinku vašej aplikácie.
class PreferenceData private constructor() { private fun getSharedPreferences(context: Context?): SharedPreferences? { return context?.getSharedPreferences( shpKey, Context.MODE_PRIVATE ) } companion object { @Volatile private var INSTANCE: PreferenceData? = null private val lock = Any() fun getInstance(): PreferenceData = INSTANCE ?: synchronized(lock) { INSTANCE ?: PreferenceData().also { INSTANCE = it } } private const val shpKey = "eu.mcomputing.mobv.zadanie" private const val userKey = "userKey" } }
2. Použitie:
- Ukladanie dát: Na ukladanie dát používajte metódu
edit()
na získanie instancieSharedPreferences.Editor
, potom použite metódy akoputInt()
,putString()
, atď. a nakoniec metóduapply()
alebocommit()
na uloženie zmien. - Načítanie dát: Na načítanie dát používajte metódy ako
getInt()
,getString()
, atď., poskytujte kľúč a predvolenú hodnotu, ktorá sa použije, ak kľúč neexistuje.
class PreferenceData private constructor() { ... fun clearData(context: Context?) { val sharedPref = getSharedPreferences(context) ?: return val editor = sharedPref.edit() editor.clear() editor.apply() } fun putUser(context: Context?, user: User?) { val sharedPref = getSharedPreferences(context) ?: return val editor = sharedPref.edit() user?.toJson()?.let { editor.putString(userKey, it) } ?: editor.remove(userKey) editor.apply() } fun getUser(context: Context?): User? { val sharedPref = getSharedPreferences(context) ?: return null val json = sharedPref.getString(userKey, null) ?: return null return User.fromJson(json) } ... }
data class User( val username: String, val email: String, val id: String, val access: String, val refresh: String ) ) { fun toJson(): String? { return try { Gson().toJson(this) } catch (ex: IOException) { ex.printStackTrace() null } } companion object { fun fromJson(string: String): User? { return try { Gson().fromJson(string, User::class.java) } catch (ex: IOException) { ex.printStackTrace() null } } } }
3. Výhody a nevýhody:
- Výhody: Jednoduché ukladanie malých dát, synchronizácia uložených dát medzi rôznymi časťami aplikácie.
- Nevýhody: Nie je vhodné na ukladanie veľkých objemov dát alebo citlivých informácií bez šifrovania. Čítanie a písanie prebieha synchronne, čo môže viesť k zablokovaniu hlavného vlákna aplikácie.
4. OAuth autorizacia REST API poziadaviek s ulozenym tokenom
OAuth 2.0 Autorizácia
OAuth 2.0 je široko uznávaný štandard používaný v celom priemysle pre bezpečnú autorizáciu. Umožňuje aplikáciám tretích strán získať obmedzený prístup k HTTP službe buď v mene vlastníka zdroja alebo tým, že aplikácii tretích strán umožní získať prístup na vlastnú päsť.
Ak používateľ autorizuje požiadavku ( prihlasenim sa ), klient dostane autorizačný grant, ktorý je poverením predstavujúcim súhlas používateľa, aby klient mal prístup k jeho zdrojom. Typ poverenia závisí od požadovaného typu grantu (napr. autorizačný kód, implicitný, poverenia hesla vlastníka zdroja alebo poverenia klienta).
Klient požiada o prístupový token z autorizačného servera (koncového bodu) prezentovaním autentifikácie svojej vlastnej identity a autorizačného grantu.
Ak je požiadavka klienta platná, autorizačný server vydá prístupový token klientovi. Klient môže tento token použiť na prístup k zdrojom vlastníka zdroja na zdrojovom serveri.
Prístupový token sa posiela na server zdrojov ako súčasť hlavičky požiadavky. Obvykle sa posiela ako "Bearer" token v hlavičke "Authorization". Server zdrojov potom overí prístupový token a ak je overenie úspešné, poskytne klientovi prístup k požadovaným zdrojom.
Nacitanie profilu
Táto funkcia je asynchrónna úloha, ktorá sa pokúša získať údaje o používateľovi z webovej služby a spracovať rôzne stavy odpovede vrátane obnovenia tokenu.
Funkcia najprv vykonáva požiadavku na získanie údajov o používateľovi. Používa prístupový token a poskytuje ho ako časť "Authorization" hlavičky s predponou "Bearer".
Ak je požiadavka úspešná, funkcia vráti údaje o používateľovi. V opačnom prípade kontroluje, či kód odpovede je 401 (Neautorizovaný), čo znamená, že prístupový token už možno nie je platný.
Pri chybe 401 funkcia pokračuje v pokuse o obnovenie prístupového tokenu. Ak je požiadavka na obnovenie úspešná, funkcia opäť pokúša získať údaje o používateľovi, ale už s novým prístupovým tokenom.
Ak sa vyskytnú chyby počas vykonávania požiadaviek (napr. sieťové chyby, nečakané chyby), funkcia zachytí tieto výnimky, zaznamená ich a vráti chybové správy.
interface ApiService { .... @GET("user/get.php") suspend fun getUser( @HeaderMap header: Map<String, String>, @Query("id") id: String ): Response<UserResponse> @POST("user/refresh.php") suspend fun refreshToken( @HeaderMap header: Map<String, String>, @Body refreshInfo: RefreshTokenRequest ): Response<RefreshTokenResponse> }
suspend fun apiGetUser( uid: String, my_uid: String, accessToken: String, refreshToken: String ): Pair<String, User?> { try { val response = service.getUser( mapOf( "x-apikey" to AppConfig.API_KEY, "Authorization" to "Bearer $accessToken" ), uid ) if (response.isSuccessful) { response.body()?.let { return Pair( "", User( it.name, "", it.id, accessToken, refreshToken, it.photo ) ) } } if (response.code() == 401) { val refreshResponse = service.refreshToken( mapOf( "x-apikey" to AppConfig.API_KEY, "x-user" to my_uid ), RefreshTokenRequest(refreshToken) ) if (refreshResponse.isSuccessful) { refreshResponse.body()?.let { newtoken -> val response2 = service.getUser( mapOf( "x-apikey" to AppConfig.API_KEY, "Authorization" to "Bearer ${newtoken.access}" ), uid ) if (response2.isSuccessful) { response2.body()?.let { return Pair( "", User( it.name, "", it.id, newtoken.access, newtoken.refresh, it.photo ) ) } } } } } return Pair("Failed to load user", null) } catch (ex: IOException) { ex.printStackTrace() return Pair("Check internet connection. Failed to load user.", null) } catch (ex: Exception) { ex.printStackTrace() } return Pair("Fatal error. Failed to load user.", null) }
5. Pokročilá autorizácia s Retrofit a OkHttpClient
Integrácia AuthInterceptor a TokenAuthenticator s Retrofit
AuthInterceptor: Interceptor, ktorý sa používa na pridanie autentizačných hlavičiek k požiadavkám pred ich odoslaním. Vytvoríte triedu, ktorá implementuje Interceptor a predefinuje metódu 'intercept'. V tejto metóde pridáte hlavičku 'Authorization' s hodnotou 'Bearer ' + token.
class AuthInterceptor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
// Získajte token
val token = ...
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(request)
}
}
TokenAuthenticator: Trieda, ktorá automaticky rieši obnovenie prístupových tokenov. Ak požiadavka vráti HTTP 401, TokenAuthenticator získa nový prístupový token a opätovne vykoná neúspešnú požiadavku.
class TokenAuthenticator() : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
// Získajte nový token
val newToken = ...
// Vytvorte novú požiadavku s novým tokenom
return response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
}
}
Integrácia s OkHttpClient a Retrofit: Pri vytváraní inštancie Retrofit vytvoríte OkHttpClient, ktorý zahrnie vytvorený AuthInterceptor a TokenAuthenticator. Týmto spôsobom sa zabezpečí, že každá požiadavka bude obsahovať potrebné autentizačné údaje a v prípade vypršania platnosti tokenov budú automaticky obnovené.
val client = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.authenticator(TokenAuthenticator())
.build()
val retrofit = Retrofit.Builder()
.client(client)
.baseUrl("https://your.api/")
.addConverterFactory(GsonConverterFactory.create())
.build()
Dôsledky: Používanie AuthInterceptor a TokenAuthenticator spolu automatizuje proces autentizácie a obnovy tokenov, čo znižuje riziko neúspešných požiadaviek kvôli problémom s autentizáciou. Avšak, ak sa token neobnoví úspešne, môže to viesť k trvalému zlyhaniu požiadaviek, kým sa používateľ nevráti do aplikácie a neprihlási sa znova.
Integrácia v zadani
Trieda AuthInterceptor
je typom interceptora, ktorý pridáva hlavičky k HTTP požiadavkám predtým, ako sú odoslané na server. Tieto hlavičky sú nevyhnutné pre správne fungovanie komunikácie s API, keďže obsahujú informácie o autentizácii a iné dôležité údaje.
Krok 1: Vytvorenie Requestu
Najprv sa vytvorí nový builder požiadavky na základe pôvodnej požiadavky a pridajú sa hlavičky "Accept" a "Content-Type", ktoré definujú, že klient akceptuje JSON formát a že obsah požiadavky je v JSON formáte.
Krok 2: Získanie Tokenu
Token sa získava z preferencií aplikácie pomocou metódy getUser()
, ktorá vracia objekt používateľa, z ktorého sa následne získa prístupový token.
Krok 3: Pridanie Hlavičky Autorizácie
Po získaní tokenu sa vytvorí hlavička "Authorization" s hodnotou "Bearer " nasledovanou samotným tokenom. Toto je štandardný spôsob, ako poskytnúť prístupový token v HTTP požiadavkách.
Krok 4: Pridanie API Kľúča
Každá požiadavka potrebuje tiež API kľúč, ktorý je špecifický pre dané API. Tento kľúč sa pridáva do hlavičky s názvom "x-apikey".
Krok 5: Vykonanie Požiadavky
Po pridaniu všetkých hlavičiek sa vykoná požiadavka s novými hlavičkami. Ak server vyžaduje tieto hlavičky pre autentizáciu alebo na spracovanie požiadavky, bez nich by požiadavka neuspela.
class AuthInterceptor(private val context: Context) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
.newBuilder()
.addHeader("Accept", "application/json")
.addHeader("Content-Type", "application/json")
val token = PreferenceData.getInstance().getUser(context)?.access
request.header("Authorization","Bearer $token")
// add api key to each request
request.addHeader("x-apikey", AppConfig.API_KEY)
return chain.proceed(request.build())
}
}
TokenAuthenticator
je trieda v Kotlin, ktorá implementuje Authenticator
interface z knižnice OkHttp. Jej úlohou je automatické obnovovanie prístupových tokenov, keď server vráti chybu 401 (neautorizovaný prístup).
Krok 1: Detekcia Chyby 401
Keď metóda authenticate()
zistí, že odpoveď serveru má stavový kód 401, znamená to, že aktuálny token je neplatný alebo vypršal.
Krok 2: Získanie Aktuálneho Používateľa a Tokenu
Metóda získa informácie o aktuálnom používateľovi a jeho refresh tokene zo shared preferences aplikácie prostredníctvom triedy PreferenceData
.
Krok 3: Obnovenie Tokenu
Pokiaľ je token neplatný, vykoná sa synchronná požiadavka na obnovenie tokenu prostredníctvom metódy refreshTokenBlocking()
. Ak je požiadavka úspešná, vytvorí sa nový objekt používateľa s novými tokenmi a uloží sa do shared preferences.
Krok 4: Vytvorenie Novej Požiadavky
Následne sa vytvorí nová požiadavka s novým prístupovým tokenom v hlavičke "Authorization".
Krok 5: Čistenie Dát pri Neúspechu
Ak obnovenie tokenu zlyhá, metóda vyčistí všetky uložené dáta a informácie o používateľovi, aby sa zabránilo neautorizovanému prístupu a ďalším chybám.
Krok 6: Vrátenie Hodnoty
Metóda vráti novú požiadavku s obnoveným tokenom alebo null
, ak obnovenie nebolo možné.
class TokenAuthenticator(val context: Context) : Authenticator { override fun authenticate(route: Route?, response: okhttp3.Response): Request? { if (response.code == 401) { val userItem = PreferenceData.getInstance().getUser(context) userItem?.let { user -> val tokenResponse = ApiService.create(context).refreshTokenBlocking( RefreshTokenRequest(user.refresh) ).execute() if (tokenResponse.isSuccessful) { tokenResponse.body()?.let { val new_user = User( user.username, user.email, user.id, it.access, it.refresh, user.photo ) PreferenceData.getInstance().putUser(context, new_user) return response.request.newBuilder() .header("Authorization", "Bearer ${new_user.access}") .build() } } } //if there was no success of refresh token we logout user and clean any data PreferenceData.getInstance().clearData(context) return null } } return null } }
interface ApiService { .... @GET("user/get.php") suspend fun getUser( @Query("id") id: String ): Response<UserResponse> @POST("user/refresh.php") suspend fun refreshToken( @Body refreshInfo: RefreshTokenRequest ): Response<RefreshTokenResponse> companion object { fun create(context: Context): ApiService { val client = OkHttpClient.Builder() .addInterceptor(AuthInterceptor(context)) .authenticator(TokenAuthenticator(context)) .build() val retrofit = Retrofit.Builder() .baseUrl("https://zadanie.mpage.sk/") .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() return retrofit.create(ApiService::class.java) } } }
suspend fun apiGetUser( uid: String ): Pair<String, User?> { try { val response = service.getUser(uid) if (response.isSuccessful) { response.body()?.let { return Pair("", User(it.name, "", it.id, "", "", it.photo)) } } return Pair("Failed to load user", null) } catch (ex: IOException) { ex.printStackTrace() return Pair("Check internet connection. Failed to load user.", null) } catch (ex: Exception) { ex.printStackTrace() } return Pair("Fatal error. Failed to load user.", null) }
6. Ziskanie povolenia pre zistovanie GPS polohy
Prvým krokom je požiadanie o povolenia potrebné pre získanie prístupu k presnej polohe zariadenia. V súbore AndroidManifest.xml
pridajte nasledujúce riadky:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Aby ste mohli používať geofencing, je potrebné integrovať Google Play služby do vášho projektu pridaním nasledujúcej závislosti do vášho build.gradle (Module):
implementation("com.google.android.gms:play-services-location:18.0.0")
Zabezpečte, aby vaša aplikácia požiadala o povolenia v čase behu, ak je to potrebné (Android 6.0 a vyššie).
Pre získanie presnej polohy potrebujeme povolenie ACCESS_FINE_LOCATION. Tu je príklad, ako požiadať o toto povolenie v čase behu:
class ProfileFragment : Fragment() { private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> if (isGranted){ // run logic if user accepted usage of gps location }else{ // run logic if user not accepted usage of gps location } } // returns if Permissions are accepted fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } .... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { .... if (!hasPermissions(requireContext())) { requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) } .... } }
Pridajte do profilu alebo nastavení svojej aplikácie, možnosť kde si môže používateľ zapnúť zdieľanie svojej polohy ostatným používateľov v okolí.
Pri zapnutí nezabudnite najprv skontrolovať či používateľ povolil prístup k polohe zariadenia, podľa predchádzajúceho kroku.
Následne keď používateľ zmení stav získavania, uložte tento stav do SharedPreferences
, aby ste tento stav nezabudli pri odchode z obrazovky.
<androidx.appcompat.widget.SwitchCompat android:layout_margin="16dp" android:checked="@={model.sharingLocation}" android:id="@+id/location_switch" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/name" app:layout_constraintEnd_toEndOf="parent" />
class ProfileViewModel(private val dataRepository: DataRepository) : ViewModel() { .... val sharingLocation = MutableLiveData<Boolean?>(null) .... }
... vo Fragmente ...
viewModel.sharingLocation.postValue(PreferenceData.getInstance().getSharing(requireContext()))
viewModel.sharingLocation.observe(viewLifecycleOwner){
it?.let {
if (it){
if (!hasPermissions(requireContext())) {
viewModel.sharingLocation.postValue(false)
requestPermissionLauncher.launch(
Manifest.permission.ACCESS_FINE_LOCATION
)
}else{
PreferenceData.getInstance().putSharing(requireContext(),true)
}
}else{
PreferenceData.getInstance().putSharing(requireContext(),false)
}
}
}
7. Zobrazenie GPS polohy na mape
Kontrola povolení je kľúčová pre zabezpečenie, že aplikácia má správne oprávnenia na prístup k funkciam zariadenia, ako je získavanie aktuálnej polohy. Kód definuje požadované povolenia a metódy na kontrolu týchto povolení.
PERMISSIONS_REQUIRED
je pole, ktoré obsahuje všetky povolenia potrebné pre fragment, v tomto prípade ACCESS_FINE_LOCATION
.
Metóda hasPermissions
prechádza všetky povolenia definované v PERMISSIONS_REQUIRED
a kontroluje, či boli udelené. Používa sa funkcia checkSelfPermission
pre každé povolenie.
val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
initLocationComponent()
addLocationListeners()
}
}
fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
V metóde onViewCreated
fragment inicializuje mapu a zároveň kontroluje, či má potrebné povolenia. Ak povolenia nie sú udelené, vyžiada si ich od používateľa pomocou requestPermissionLauncher
. Ak sú povolenia udelené, pokračuje v inicializácii komponentov pre prácu s polohou a nastaví poslucháčov pre aktualizácie polohy.
requestPermissionLauncher
je inštancia, ktorá sa stará o výsledok požiadavky na povolenie. V prípade, že je povolenie udelené, volajú sa metódy initLocationComponent
a addLocationListeners
.
annotationManager = bnd.mapView.annotations.createCircleAnnotationManager()
val hasPermission = hasPermissions(requireContext())
onMapReady(hasPermission)
bnd.myLocation.setOnClickListener {
if (!hasPermissions(requireContext())) {
requestPermissionLauncher.launch(
Manifest.permission.ACCESS_FINE_LOCATION
)
} else {
lastLocation?.let { refreshLocation(it) }
addLocationListeners()
Log.d("MapFragment","location click")
}
}
Metóda onMapReady
sa zavolá, keď je mapa pripravená. Nastaví kameru mapy, načíta štýl a pridá poslucháčov pre kliknutie na mapu a zmeny polohy. Ak je povolenie udelené, inicializuje sa komponent polohy:
private fun onMapReady(enabled: Boolean) {
binding.mapView.getMapboxMap().setCamera(
CameraOptions.Builder()
.center(Point.fromLngLat(14.3539484, 49.8001304))
.zoom(2.0)
.build()
)
binding.mapView.getMapboxMap().loadStyleUri(
Style.MAPBOX_STREETS
) {
if (enabled) {
initLocationComponent()
addLocationListeners()
}
}
binding.mapView.getMapboxMap().addOnMapClickListener {
if (hasPermissions(requireContext())) {
onCameraTrackingDismissed()
}
true
}
}
}
Metóda addMarker
pridáva značku na mapu na základe poskytnutej polohy.
Metódy initLocationComponent
a addLocationListeners
sa starajú o inicializáciu komponentov polohy a pridanie poslucháčov pre zmeny polohy.
initLocationComponent
je metóda, ktorá inicializuje komponent pre získavanie aktuálnej polohy používateľa. Nastavenia komponentu umožňujú jeho aktiváciu a zapnutie vizuálneho efektu pulzovania pre aktuálnu polohu.
addLocationListeners
pridáva poslucháčov, ktoré sledujú zmeny polohy a gestá na mape. Tieto poslucháče zahŕňajú OnIndicatorPositionChangedListener
pre zmenu polohy a OnMoveListener
pre sledovanie gest pohybu.
Pri zmene polohy, metóda refreshLocation
aktualizuje kameru mapy, aby zobrazovala novú polohu a pridá marker na túto polohu.
private fun initLocationComponent() {
Log.d("MapFragment","initLocationComponent")
val locationComponentPlugin = binding.mapView.location
locationComponentPlugin.updateSettings {
this.enabled = true
this.pulsingEnabled = true
}
}
private fun addLocationListeners() {
Log.d("MapFragment","addLocationListeners")
binding.mapView.location.addOnIndicatorPositionChangedListener(
onIndicatorPositionChangedListener
)
binding.mapView.gestures.addOnMoveListener(onMoveListener)
}
onIndicatorPositionChangedListener
sa aktivuje, keď sa zmení poloha indikátora polohy (zvyčajne poloha používateľa). Pri zmene polohy zaznamenáva novú polohu a volá funkciu refreshLocation()
s novou polohou.
refreshLocation(it)
aktualizuje užívateľské rozhranie mapy tak, aby odrážalo novú polohu.
refreshLocation(point: Point)
funkcia sa volá na aktualizáciu užívateľského rozhrania mapy na novú polohu používateľa. Vykonáva niekoľko akcií:
- Centruje kameru mapy na novú polohu:
binding.mapView.getMapboxMap().setCamera(CameraOptions.Builder().center(point).zoom(14.0).build())
- Nastavuje stredový bod mapy na novú polohu pre interakcie gestami:
binding.mapView.gestures.focalPoint = binding.mapView.getMapboxMap().pixelForCoordinate(point)
- Aktualizuje
lastLocation
na aktuálnu polohu. - Pridáva značku na aktuálnu polohu s funkciou
addMarker(point)
.
onMoveListener
reaguje na pohybové udalosti na mape. Má tri metódy:
onMoveBegin(detector: MoveGestureDetector)
: Volá sa na začiatku pohybu. Aktivuje metóduonCameraTrackingDismissed()
.onMove(detector: MoveGestureDetector): Boolean
: Reaguje na udalosti pohybu. V tomto prípade nevykonáva žiadnu akciu a vrátifalse
.onMoveEnd(detector: MoveGestureDetector)
: Volá sa na konci pohybu. V tomto prípade nevykonáva žiadnu akciu.
onCameraTrackingDismissed()
sa volá, keď je potrebné zrušiť sledovanie polohy kamery vzhľadom na polohu používateľa. Odstraňuje onIndicatorPositionChangedListener
a onMoveListener
z príslušných komponentov.
private val onIndicatorPositionChangedListener = OnIndicatorPositionChangedListener { Log.d("MapFragment","poloha je $it") refreshLocation(it) } private fun refreshLocation(point: Point) { binding.mapView.getMapboxMap().setCamera(CameraOptions.Builder().center(point).zoom(14.0).build()) binding.mapView.gestures.focalPoint = binding.mapView.getMapboxMap().pixelForCoordinate(point) lastLocation = point addMarker(point) } private val onMoveListener = object : OnMoveListener { override fun onMoveBegin(detector: MoveGestureDetector) { onCameraTrackingDismissed() } override fun onMove(detector: MoveGestureDetector): Boolean { return false } override fun onMoveEnd(detector: MoveGestureDetector) {} } private fun onCameraTrackingDismissed() { binding.mapView.apply { location.removeOnIndicatorPositionChangedListener(onIndicatorPositionChangedListener) gestures.removeOnMoveListener(onMoveListener) } }
Tieto metódy zabezpečujú, že ak používateľ posunie mapu alebo sa jeho poloha zmení, mapa sa aktualizuje a zobrazí správne informácie.
Metóda onDestroyView()
je súčasťou životného cyklu fragmentu v Android aplikáciách. Táto metóda sa automaticky volá, keď je pohľad fragmentu odstraňovaný a je na ceste k zničeniu. Je to miesto, kde môžete uvoľniť všetky zdroje, ktoré nie sú viac potrebné a ktoré ste pravdepodobne inicializovali v metóde onCreateView()
alebo onViewCreated()
.
location.removeOnIndicatorPositionChangedListener(onIndicatorPositionChangedListener)
: Odstraňuje počúvač, ktorý bol zaregistrovaný na sledovanie zmien polohy indikátora.gestures.removeOnMoveListener(onMoveListener)
: Odstraňuje počúvač, ktorý bol zaregistrovaný na sledovanie pohybových udalostí na mape.
Týmto spôsobom metóda onDestroyView()
pomáha predchádzať úniku pamäte odstraňovaním počúvačov, keď už nie sú potrebné, čo je dôležitou súčasťou správy zdrojov v Android aplikáciách.
Preštudujte a odskúšajte si kódy hlavne z týchto stránok: