Lab 05: add people rest

This commit is contained in:
abazov73 2023-12-06 01:39:26 +04:00
parent 213b67fdfa
commit 2c5adc70ed
15 changed files with 327 additions and 14 deletions

View File

@ -7,11 +7,11 @@
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\Андрей\.android\avd\Pixel_7_Pro_API_30.avd" />
<value value="C:\Users\Андрей\.android\avd\Pixel_3_API_30.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-12-05T19:20:06.168865100Z" />
<timeTargetWasSelectedWithDropDown value="2023-12-05T19:38:57.752433Z" />
</component>
</project>

View File

@ -2,6 +2,7 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.plugin.serialization")
}
android {
@ -78,6 +79,13 @@ dependencies {
implementation("androidx.paging:paging-compose:3.2.1")
// retrofit
val retrofitVersion = "2.9.0"
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
//Tests
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")

View File

@ -15,6 +15,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.Mobile_Labs"
android:name=".TheatreApplication"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"

View File

@ -0,0 +1,67 @@
package com.example.mobile_labs.api
import com.example.mobile_labs.api.models.EventRemote
import com.example.mobile_labs.api.models.PerformanceRemote
import com.example.mobile_labs.api.models.PersonRemote
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
interface MyServerService {
@GET("persons")
suspend fun getPeople(
@Query("_page") page: Int,
@Query("_limit") limit: Int,
): List<PersonRemote>
@GET("performances")
suspend fun getPerformances(
@Query("_page") page: Int,
@Query("_limit") limit: Int,
): List<PersonRemote>
@GET("events")
suspend fun getEvents(
@Query("_page") page: Int,
@Query("_limit") limit: Int,
): List<EventRemote>
@GET("performances/{id}")
suspend fun getPerformance(
@Path("id") id: Int,
): PerformanceRemote
companion object {
private const val BASE_URL = "http://10.0.2.2:26000/"
@Volatile
private var INSTANCE: MyServerService? = null
fun getInstance(): MyServerService {
return INSTANCE ?: synchronized(this) {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BASIC
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.build()
.create(MyServerService::class.java)
.also { INSTANCE = it }
}
}
}
}

View File

@ -0,0 +1,18 @@
package com.example.mobile_labs.api.models
import com.example.mobile_labs.database.event.model.Event
import kotlinx.serialization.Serializable
import java.time.LocalDate
@Serializable
data class EventRemote(
val id: Int = 0,
val date: String = "",
val performanceId: Int = 0,
)
fun EventRemote.toEvent(): Event = Event(
id,
LocalDate.parse(date),
performanceId
)

View File

@ -0,0 +1,26 @@
package com.example.mobile_labs.api.models
import com.example.mobile_labs.database.performance.model.Performance
import com.example.mobile_labs.database.person.model.Person
import kotlinx.serialization.Serializable
@Serializable
data class PerformanceRemote(
val id: Int = 0,
val title: String = "",
val description: String = "",
val authorId: Int = 0,
val directorId: Int = 0,
val imageURL: String = "",
val previewImageURL: String = ""
)
fun PerformanceRemote.toPerformance(): Performance = Performance(
id,
title,
description,
authorId,
directorId,
imageURL,
previewImageURL
)

View File

@ -0,0 +1,21 @@
package com.example.mobile_labs.api.models
import com.example.mobile_labs.database.event.model.Event
import com.example.mobile_labs.database.person.model.Person
import kotlinx.serialization.Serializable
import java.time.LocalDate
@Serializable
data class PersonRemote(
val id: Int = 0,
val last_name: String = "",
val first_name: String = "",
val imageURL: String = ""
)
fun PersonRemote.toPerson(): Person = Person(
id,
last_name,
first_name,
imageURL
)

View File

@ -0,0 +1,107 @@
package com.example.mobile_labs.api.people
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.example.mobile_labs.api.MyServerService
import com.example.mobile_labs.api.models.toPerson
import com.example.mobile_labs.database.AppDatabase
import com.example.mobile_labs.database.person.model.Person
import com.example.mobile_labs.database.person.repository.OfflinePersonRepository
import com.example.mobile_labs.database.remotekeys.model.RemoteKeyType
import com.example.mobile_labs.database.remotekeys.model.RemoteKeys
import com.example.mobile_labs.database.remotekeys.repository.OfflineRemoteKeyRepository
import retrofit2.HttpException
import java.io.IOException
@OptIn(ExperimentalPagingApi::class)
class PeopleRemoteMediator(
private val service: MyServerService,
private val dbPersonRepository: OfflinePersonRepository,
private val dbRemoteKeyRepository: OfflineRemoteKeyRepository,
private val database: AppDatabase
) : RemoteMediator<Int, Person>() {
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Person>
): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: 1
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
remoteKeys?.prevKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
remoteKeys?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
}
try {
val people = service.getPeople(page, state.config.pageSize).map { it.toPerson() }
val endOfPaginationReached = people.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.PERSON)
dbPersonRepository.clearPeople()
}
val prevKey = if (page == 1) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = people.map {
RemoteKeys(
entityId = it.uid !!,
type = RemoteKeyType.PERSON,
prevKey = prevKey,
nextKey = nextKey
)
}
dbRemoteKeyRepository.createRemoteKeys(keys)
dbPersonRepository.insertPeople(people)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Person>): RemoteKeys? {
return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { person ->
dbRemoteKeyRepository.getAllRemoteKeys(person.uid !!, RemoteKeyType.PERSON)
}
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Person>): RemoteKeys? {
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { person ->
dbRemoteKeyRepository.getAllRemoteKeys(person.uid !!, RemoteKeyType.PERSON)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, Person>
): RemoteKeys? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.uid?.let { personUid ->
dbRemoteKeyRepository.getAllRemoteKeys(personUid, RemoteKeyType.PERSON)
}
}
}
}

View File

@ -0,0 +1,42 @@
package com.example.mobile_labs.api.people
import android.util.Log
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.mobile_labs.api.MyServerService
import com.example.mobile_labs.common.AppDataContainer
import com.example.mobile_labs.common.PersonRepository
import com.example.mobile_labs.database.AppDatabase
import com.example.mobile_labs.database.person.model.Person
import com.example.mobile_labs.database.person.repository.OfflinePersonRepository
import com.example.mobile_labs.database.remotekeys.repository.OfflineRemoteKeyRepository
import kotlinx.coroutines.flow.Flow
class RestPersonRepository(
private val service: MyServerService,
private val dbPersonRepository: OfflinePersonRepository,
private val dbRemoteKeyRepository: OfflineRemoteKeyRepository,
private val database: AppDatabase
) : PersonRepository {
override fun getAllPeople(): Flow<PagingData<Person>> {
val pagingSourceFactory = { dbPersonRepository.getAllPeoplePagingSource() }
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(
pageSize = AppDataContainer.LIMIT,
enablePlaceholders = false
),
remoteMediator = PeopleRemoteMediator(
service,
dbPersonRepository,
dbRemoteKeyRepository,
database,
),
pagingSourceFactory = pagingSourceFactory
).flow
}
}

View File

@ -1,14 +1,18 @@
package com.example.mobile_labs.common
import android.content.Context
import com.example.mobile_labs.api.MyServerService
import com.example.mobile_labs.api.people.RestPersonRepository
import com.example.mobile_labs.database.event.repository.EventRepository
import com.example.mobile_labs.database.event.repository.OfflineEventRepository
import com.example.mobile_labs.database.performance.repository.OfflinePerformanceRepository
import com.example.mobile_labs.database.person.repository.OfflinePersonRepository
import com.example.mobile_labs.database.AppDatabase
import com.example.mobile_labs.database.remotekeys.repository.OfflineRemoteKeyRepository
import com.example.mobile_labs.database.remotekeys.repository.RemoteKeyRepository
interface AppContainer {
val personRepository: PersonRepository
val personRestRepository: RestPersonRepository
val eventRepository: EventRepository
val performanceRepository: PerformanceRepository
}
@ -22,9 +26,21 @@ class AppDataContainer(private val context: Context) : AppContainer {
OfflinePerformanceRepository(AppDatabase.getInstance(context).performanceDao())
}
override val personRepository: PersonRepository by lazy {
val personRepository: OfflinePersonRepository by lazy {
OfflinePersonRepository(AppDatabase.getInstance(context).personDao())
}
val remoteKeyRepository: OfflineRemoteKeyRepository by lazy {
OfflineRemoteKeyRepository(AppDatabase.getInstance(context).remoteKeysDao())
}
override val personRestRepository: RestPersonRepository by lazy {
RestPersonRepository(
MyServerService.getInstance(),
personRepository,
remoteKeyRepository,
AppDatabase.getInstance(context)
)
}
companion object {
const val TIMEOUT = 5000L

View File

@ -14,7 +14,7 @@ import com.example.mobile_labs.ui.person.list.PeopleListViewModel
object AppViewModelProvider {
val Factory = viewModelFactory {
initializer {
PeopleListViewModel(theatreApplication().container.personRepository)
PeopleListViewModel(theatreApplication().container.personRestRepository)
}
initializer {
EventListViewModel(theatreApplication().container.eventRepository)

View File

@ -6,8 +6,4 @@ import kotlinx.coroutines.flow.Flow
interface PersonRepository {
fun getAllPeople(): Flow<PagingData<Person>>
fun getPerson(uid: Int): Flow<Person?>
suspend fun insertPerson(person: Person)
suspend fun updatePerson(person: Person)
suspend fun deletePerson(person: Person)
}

View File

@ -18,11 +18,14 @@ interface PersonDao {
fun getByUid(uid: Int): Flow<Person>
@Insert
suspend fun insert(person: Person)
suspend fun insert(vararg person: Person)
@Update
suspend fun update(person: Person)
@Delete
suspend fun delete(person: Person)
@Query("DELETE FROM people")
suspend fun deleteAll()
}

View File

@ -3,6 +3,7 @@ package com.example.mobile_labs.database.person.repository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import com.example.mobile_labs.common.PersonRepository
import com.example.mobile_labs.common.AppDataContainer
import com.example.mobile_labs.database.person.dao.PersonDao
@ -17,12 +18,18 @@ class OfflinePersonRepository(private val personDao: PersonDao) : PersonReposito
),
pagingSourceFactory = personDao::getAll
).flow;
override fun getPerson(uid: Int): Flow<Person?> = personDao.getByUid(uid);
fun getPerson(uid: Int): Flow<Person?> = personDao.getByUid(uid);
override suspend fun insertPerson(person: Person) = personDao.insert(person);
suspend fun insertPerson(person: Person) = personDao.insert(person);
override suspend fun updatePerson(person: Person) = personDao.update(person);
suspend fun updatePerson(person: Person) = personDao.update(person);
override suspend fun deletePerson(person: Person) = personDao.delete(person);
suspend fun deletePerson(person: Person) = personDao.delete(person);
suspend fun clearPeople() = personDao.deleteAll()
suspend fun insertPeople(people: List<Person>) =
personDao.insert(*people.toTypedArray())
fun getAllPeoplePagingSource(): PagingSource<Int, Person> = personDao.getAll()
}

View File

@ -3,4 +3,5 @@ plugins {
id("com.android.application") version "8.1.1" apply false
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
id("com.google.devtools.ksp") version "1.8.20-1.0.11" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.20" apply false
}