Úlohy pre 6. cvičenie
- 1. Pridanie SQLite databazy
- 2. Ziskanie polohy a vytvorenie Geofence okolo nej
Hodnotenie
Za splnenie prvej úlohy celkovo 2 body.
Za splnenie druhej úlohy celkovo 1 bod.
1. Pridanie SQLite databazy
1. Pridanie závislostí do projektu
Pridajte nasledujúce závislosti do vášho `build.gradle` (Module: app):
val room_version = "2.6.0" 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")Po pridaní závislostí kliknite na 'Sync Now' v Android Studio, aby sa závislosti stiahli a integrovali do vášho projektu. Keď je táto závislosť pridaná, môžete v DAO definovať metódy, ktoré vrátia `suspend` funkcie. Použitím `suspend` funkcií s Room a coroutines môžete jednoducho a bezpečne pristupovať k databáze mimo hlavného vlákna.
2. Vytvorenie entít
@Entity(tableName = "users") class UserEntity( @PrimaryKey val uid: String, val name: String, val updated: String, val lat: Double, val lon: Double, val radius: Double, val photo: String = "" )V tomto príklade je trieda `UserEntity` mapovaná na tabuľku s názvom "users". Atribút `@PrimaryKey` definuje, ktorý stĺpec je primárnym kľúčom v tejto tabuľke.
3. Vytvorenie DAO (Data Access Object)
@Dao
interface DbDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUserItems(items: List<UserEntity>)
@Query("select * from users where uid=:uid")
fun getUserItem(uid: String): LiveData<UserEntity?>
@Query("select * from users")
fun getUsers(): LiveData<List<UserEntity?>>
@Query("delete from users")
suspend fun deleteUserItems()
}
Tu `@Query` definuje SQL dotaz, `@Insert` je pre vkladanie nových záznamov. Potom existuje aj `@Update` pre aktualizáciu existujúcich záznamov a `@Delete` pre vymazanie záznamov.
4. Vytvorenie databázy
@Database(
entities = [
UserEntity::class
],
version = 1,
exportSchema = false
)
abstract class AppRoomDatabase : RoomDatabase() {
abstract fun appDao(): DbDao
companion object {
@Volatile
private var INSTANCE: AppRoomDatabase? = null
fun getInstance(context: Context): AppRoomDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE
?: buildDatabase(context).also { INSTANCE = it }
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
AppRoomDatabase::class.java, "treeCamDB"
).fallbackToDestructiveMigration()
.build()
}
}
@Database
je anotácia, ktorá označuje triedu ako Room databázu. Má nasledujúce atribúty:
- entities: Zoznam všetkých entít, ktoré budú v databáze reprezentované ako tabuľky. V tomto prípade obsahuje iba
UserEntity::class
. - version: Verzia databázy. Keď urobíte zmeny v štruktúre databázy, mali by ste zvýšiť túto hodnotu.
- exportSchema: Určuje, či má byť schéma databázy exportovaná do súboru pre kontrolu. V tomto prípade je nastavené na
false
.
abstract class AppRoomDatabase : RoomDatabase()
- Táto trieda je abstraktná definícia databázy, ktorá rozširuje RoomDatabase.
abstract fun appDao(): DbDao
- Táto metóda deklaruje, že naša databáza bude obsahovať DAO s názvom DbDao
. Týmto spôsobom môžeme získať prístup k metódam DAO, keď máme inštanciu databázy.
companion object
je špeciálny objekt v Kotlin, ktorý obsahuje metódy a premenné, ktoré môžu byť volané na úrovni triedy.
- @Volatile: Označuje premennú
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). AkINSTANCE
nie je nastavená, volá sabuildDatabase
a vytvorí sa nová inštancia databázy. - buildDatabase: Táto metóda vytvára novú inštanciu
AppRoomDatabase
použitímRoom.databaseBuilder()
.fallbackToDestructiveMigration()
zabezpečuje, že ak dojde k nezlučiteľným zmenám v databázovej štruktúre medzi migráciami, stávajúca databáza sa zmaže a vytvorí sa znova namiesto toho, aby aplikácia spadla.
5. Vrstva LocalCache
LocalCache
. Hlavným účelom tejto vrstvy je poskytnúť flexibilitu pri práci s databázou a umožniť zmeny v logike práce s databázou bez toho, aby to ovplyvnilo Repository alebo zvyšok aplikácie.
- Oddelenie logiky: Keď chcete zmeniť spôsob, akým sa údaje ukladajú alebo získavajú z databázy, môžete to jednoducho urobiť v
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.
LocalCache
medzi DAO a Repository vám poskytne väčšiu kontrolu a flexibilitu nad tým, ako vaša aplikácia interaguje s databázou, a zároveň zachová čistotu a organizáciu vášho kódu.
class LocalCache(private val dao: DbDao) {
suspend fun logoutUser() {
deleteUserItems()
}
suspend fun insertUserItems(items: List<UserEntity>) {
dao.insertUserItems(items)
}
fun getUserItem(uid: String): LiveData<UserEntity?> {
return dao.getUserItem(uid)
}
fun getUsers(): LiveData<List<UserEntity?>> = dao.getUsers()
suspend fun deleteUserItems() {
dao.deleteUserItems()
}
}
6. Komunikacia s databazou cez DataRepository
DataRepository
predstavuje kľúčový komponent v architektúre vašej aplikácie. Slúži ako stredná vrstva medzi zdrojmi údajov (v tomto prípade API a lokálnou databázou) a zvyškom vašej aplikácie.
Hlavným účelom tejto triedy je poskytnúť ústredné miesto pre získavanie údajov, či už ide o načítavanie údajov z API alebo ich získavanie z lokálnej databázy. Týmto spôsobom, ak by ste potrebovali zmeniť logiku získavania údajov, môžete to urobiť na jednom mieste, bez toho, aby ste museli meniť mnoho častí aplikácie.
Vďaka Singleton dizajnovému vzoru máte zabezpečené, že v rámci celej aplikácie existuje iba jedna inštancia DataRepository
. To zabezpečuje konzistentnosť údajov a minimalizuje riziko chýb spojených s viacnásobným prístupom k zdrojom údajov.
Metóda apiListGeofence()
je zodpovedná za komunikáciu s API a zabezpečuje, že údaje sú správne načítané a následne uložené do lokálnej databázy. Na druhej strane metóda getUsers()
poskytuje rýchly prístup k údajom uloženým v databáze.
class DataRepository private constructor(
private val service: ApiService,
private val cache: LocalCache
) {
companion object {
const val TAG = "DataRepository"
@Volatile
private var INSTANCE: DataRepository? = null
private val lock = Any()
fun getInstance(context: Context): DataRepository =
INSTANCE ?: synchronized(lock) {
INSTANCE
?: DataRepository(
ApiService.create(context),
LocalCache(AppRoomDatabase.getInstance(context).appDao())
).also { INSTANCE = it }
}
}
suspend fun apiListGeofence(): String {
try {
val response = service.listGeofence()
if (response.isSuccessful) {
response.body()?.let {
val users = it.map {
UserEntity(
it.uid, it.name, it.updated,
it.lat, it.lon, it.radius, it.photo
)
}
cache.insertUserItems(users)
return ""
}
}
return "Failed to load users"
} catch (ex: IOException) {
ex.printStackTrace()
return "Check internet connection. Failed to load users."
} catch (ex: Exception) {
ex.printStackTrace()
}
return "Fatal error. Failed to load users."
}
fun getUsers() = cache.getUsers()
}
Trieda DataRepository
je dizajnovaná ako Singleton, čo znamená, že v rámci aplikácie bude existovať iba jedna inštancia tejto triedy. Je zodpovedná za získavanie údajov z API a ukladanie ich do lokálnej databázy pomocou LocalCache
.
- service: Inštancia API služby.
- cache: Lokálna medzipamäť (LocalCache) pre prácu s databázou.
companion object
obsahuje metódy a premenné, ktoré môžu byť volané na úrovni triedy.
- @Volatile: Označuje premennú
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 )
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)
}
}
}
2. Ziskanie polohy a vytvorenie Geofence okolo nej
Povolenie prístupu k lokalizácii na pozadí
Povolenie <uses-permission-sdk-23 android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
v súbore AndroidManifest.xml je nevyhnutné pre aplikácie, ktoré potrebujú získavať prístup k lokalizácii zariadenia, keď aplikácia beží na pozadí alebo nie je aktívna na obrazovke. Toto povolenie je špecifikované pre aplikácie cieľujúce na Android SDK verziu 23 a vyššiu. Od tejto verzie Androidu sú pravidlá pre prístup k lokalizácii na pozadí prísnejšie kvôli ochrane súkromia užívateľa. Aplikácie, ktoré chcú využívať túto schopnosť, by mali byť transparentné voči užívateľovi a jasne komunikovať, prečo potrebujú takýto prístup, aby mohli získať potrebné povolenie od užívateľa.
Nižšie uvádzam ukážkový kód ako je možné overiť či použivateľ udelil dostatočné povolenia, a tiež ako ich vyžiadať ak ich ešte nepovolil.
private val PERMISSIONS_REQUIRED = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
}
else -> {
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
}
}
val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) {
}
fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
Pred vytvorením geofence potrebujete získať aktuálnu polohu zariadenia. To môžete urobiť použitím FusedLocationProviderClient. Nezabudnite skontrolovať, či máte povolenie na prístup k polohe pred tým, ako získate polohu.
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireActivity())
fusedLocationClient.lastLocation.addOnSuccessListener(requireActivity()) {
// Logika pre prácu s poslednou polohou
Log.d("ProfileFragment", "poloha posledna $it")
setupGeofence(it)
}
Po získaní aktuálnej polohy môžete nastaviť geofence. Tu je príklad, ako vytvoriť geofence s určitým polomerom okolo aktuálnej polohy:
val geofencingClient = LocationServices.getGeofencingClient(requireActivity())
val geofence = Geofence.Builder()
.setRequestId("my-geofence")
.setCircularRegion(location.latitude, location.longitude, 100f) // 100m polomer
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_EXIT)
.build()
Ďalej potrebujete vytvoriť požiadavku na geofencing a pridať geofence. Geofencing je technológia, ktorá umožňuje aplikácii reagovať na vstup alebo výstup z definovanej geografickej oblasti, čo sa často nazýva "virtuálny plot".
Vytvorenie objektu GeofencingRequest: Prostredníctvom konštruktora Builder()
sa vytvára nový objekt GeofencingRequest
. Táto konštrukcia umožňuje nastaviť rôzne parametre a vlastnosti pre požiadavku na geofencing.
Nastavenie počiatočneho stavu: Metóda setInitialTrigger()
definuje, aká akcia spustí upozornenie geofencingu. V tomto prípade je nastavená na INITIAL_TRIGGER_ENTER
, čo znamená, že upozornenie bude spustené hneď ako užívateľ vstúpi do definovanej oblasti.
Pridanie geofence: Metóda addGeofence()
pridáva konkrétny geofence (virtuálny plot) do požiadavky. V tomto prípade je geofence
predom definovaný objekt, ktorý určuje geografické hranice a pravidlá pre geofencing.
Nakoniec metóda build()
vytvára a vráti objekt GeofencingRequest
s nastavenými vlastnosťami.
Výsledná požiadavka na geofencing môže byť následne použitá s rôznymi službami, ktoré monitorujú polohu zariadenia a vyvolávajú upozornenia alebo akcie, keď sú splnené podmienky geofencingu.
val geofencingRequest = GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofence(geofence)
.build()
Na zachytenie udalostí geofence potrebujete PendingIntent, ktorý spustí službu alebo vysielanie, keď sa vykoná udalosť geofence:
val intent = Intent(requireActivity(), GeofenceBroadcastReceiver::class.java)
val geofencePendingIntent =
PendingIntent.getBroadcast(
requireActivity(),
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
V danom kóde sa vytvára Intent
, ktorý definuje, že chceme spustiť službu GeofenceBroadcastReceiver
. Tento Intent je potom zabalený do PendingIntent
, čo umožňuje externým aplikáciám alebo službám spustiť tento Intent v našom mene. Použitím PendingIntent.FLAG_UPDATE_CURRENT
zabezpečujeme, že ak už existuje takýto PendingIntent, bude aktualizovaný novými dátami z aktuálneho Intentu.
Upozornenie: GeofenceBroadcastReceiver
nebude Android Studio poznať, keďže ste ho ešte neimplementovali, jeho implementácia je nižšie.
Nakoniec, pridajte váš geofence pomocou LocationServices.getGeofencingClient:
geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent).run {
addOnSuccessListener {
// Geofences boli úspešne pridané
Log.d("ProfileFragment", "geofence vytvoreny")
}
addOnFailureListener {
// Chyba pri pridaní geofences
it.printStackTrace()
binding.locationSwitch.isChecked = false
PreferenceData.getInstance().putSharing(requireContext(), false)
}
}
Ak chcete reagovať na prechody geofence, vytvorte BroadcastReceiver
, ktorý zachytí udalosti geofence. Počas udalosti EXIT môžete získať novú polohu na pozadí:
class GeofenceBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent == null) { // no geofence exit Log.e("GeofenceBroadcastReceiver", "error 1") return } val geofencingEvent = GeofencingEvent.fromIntent(intent) if (geofencingEvent == null) { // error Log.e("GeofenceBroadcastReceiver", "error 2") return } if (geofencingEvent.hasError()) { val errorMessage = GeofenceStatusCodes .getStatusCodeString(geofencingEvent.errorCode) //send error message to user using notification Log.e("GeofenceBroadcastReceiver", "error 3") return } // Get the transition type. val geofenceTransition = geofencingEvent.geofenceTransition if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { val triggeringLocation = geofencingEvent.triggeringLocation if (context == null || triggeringLocation == null) { // error Log.e("GeofenceBroadcastReceiver", "error 4") return } setupGeofence(triggeringLocation, context) } } private fun setupGeofence(location: Location, context: Context) { val geofencingClient = LocationServices.getGeofencingClient(context.applicationContext) val geofence = Geofence.Builder() .setRequestId("my-geofence") .setCircularRegion(location.latitude, location.longitude, 100f) // 100m polomer .setExpirationDuration(Geofence.NEVER_EXPIRE) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_EXIT) .build() val geofencingRequest = GeofencingRequest.Builder() .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER) .addGeofence(geofence) .build() val intent = Intent(context, GeofenceBroadcastReceiver::class.java) val geofencePendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) if (ActivityCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) != PackageManager.PERMISSION_GRANTED ) { // permission issue, geofence not created Log.e("GeofenceBroadcastReceiver", "error 5") return } geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent).run { addOnSuccessListener { // Geofences boli úspešne pridané Log.d("GeofenceBroadcastReceiver", "novy geofence vytvoreny") } addOnFailureListener { // Chyba pri pridaní geofences Log.e("GeofenceBroadcastReceiver", "error 6") } } } }
Preštudujte a odskúšajte si kódy hlavne z týchto stránok:
- Získanie povolení od používateľa
- Získanie poslednej známej/ aktuálnej polohy.
- Nastavenia získavania polohy.
- Vytvorenie Geofencingu.