Úlohy pre 7. a 8. cvičenie

  • 1. SharedPreferences
  • 2. OAuth autorizacia REST API poziadaviek s ulozenym tokenom
  • 3. Pokročilá autorizácia s Retrofit a OkHttpClient
  • 4. Pridanie SQLite databazy
  • 5. DataBinding
  • 6. Jednosmerný, Obojsmerný DataBinding
  • 7. Ziskanie povolenia pre zistovanie GPS polohy
  • 8. Zobrazenie GPS polohy na mape a jej odoslanie
  • 9. Spolupráca medzi vytvorenými komponentami

1. 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() alebo getPreferences().
  • 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 instancie SharedPreferences.Editor, potom použite metódy ako putInt(), putString(), atď. a nakoniec metódu apply() alebo commit() 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.

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

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

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

5. 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() alebo findViewById().

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.

6. 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 parametrov android: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()
                }
            }

        }
    }
	
...
	
}

7. Ziskanie povolenia pre zistovanie GPS polohy

Krok 1: Požiadavky na Povolenia

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" />
Krok 2: Integrácia Google Play Služieb

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(libs.play.services.location)
Krok 3: Požiadanie o povolenie polohy

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 možnosť získavania polohy

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.

profile location switch
    <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)
        }
    }
}

8. Zobrazenie GPS polohy na mape

Kontrola Povolení

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
}
Inicializácia Mapy a Povolení

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")
    }
}

Manipulácia s Mapou

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.

Aktualizácia a Sledovanie 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ódu onCameraTrackingDismissed().
  • onMove(detector: MoveGestureDetector): Boolean: Reaguje na udalosti pohybu. V tomto prípade nevykonáva žiadnu akciu a vráti false.
  • 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.

Upratanie listenerov pri ukonceni/minimalizovani aplikacie

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.

Zobrazenie polohy vysledok
location screen
Odoslanie polohy
Keď zachytíte zmenu polohy, tak okrem vykreslenia na mapu ju aj odošlite na server pomocou REST API, pomocou operácie:
POST https://zadanie.mpage.sk/geofence/update.php 
Prípadne ak používateľ v profile vypne zdielanie polohy, tak odstránte jeho poslednú odoslanú polohu pomocou REST API operácie:
DELETE https://zadanie.mpage.sk/geofence/update.php

9. Spolupráca medzi vytvorenými komponentami

Spolupráca

  • Momentálne máte implementovaný skrolovateľný zoznam - RecyclerView.
  • Momentálne máte Repozitár, ktorý načítava z REST API a ukladá do SQLite.
  • Momentálne máte Databinding s využitím LiveData a ViewModel.
  • Momentálne máte mapu, ktorá ukazuje Vašu polohu a odosiela na server.

Dajte všetky tieto časti dokopy aby spolupracovali pri zobrazovaní skrolovateľného zoznamu - RecyclerView.

Využite databinding vo Fragmente kde využívate skrolovateľný zoznam a nastavujte prvky zoznamu z LiveData vo ViewModel, ktorý sleduje zmeny v cez Repozitár. Repozitár, ktorý použijete načítava aktuálne informácie do zoznamu, z databázy. Pri prvom zobrazení skontroluje aj zmeny pomocou REST API. Taktiež pri "pull to refresh", tiež kontroluje zmeny.

Na získanie informácií z REST API použite nasledovnú operáciu, ktorá Vám vráti Vašu polohu, a polohy uložené na serveri. Ak máte vypnutú polohu, vráti sa Vám len prázdne pole bez informácii o Vas.

GET https://zadanie.mpage.sk/geofence/list.php