From 2c5adc70ed1de5b3a17d02d39889eea865dabd26 Mon Sep 17 00:00:00 2001 From: abazov73 <92822431+abazov73@users.noreply.github.com> Date: Wed, 6 Dec 2023 01:39:26 +0400 Subject: [PATCH] Lab 05: add people rest --- .idea/deploymentTargetDropDown.xml | 4 +- app/build.gradle.kts | 8 ++ app/src/main/AndroidManifest.xml | 1 + .../mobile_labs/api/MyServerService.kt | 67 +++++++++++ .../mobile_labs/api/models/EventRemote.kt | 18 +++ .../api/models/PerformanceRemote.kt | 26 +++++ .../mobile_labs/api/models/PersonRemote.kt | 21 ++++ .../api/people/PeopleRemoteMediator.kt | 107 ++++++++++++++++++ .../api/people/RestPersonRepository.kt | 42 +++++++ .../mobile_labs/common/AppContainer.kt | 20 +++- .../common/AppViewModelProvider.kt | 2 +- .../mobile_labs/common/PersonRepository.kt | 4 - .../database/person/dao/PersonDao.kt | 5 +- .../repository/OfflinePersonRepository.kt | 15 ++- build.gradle.kts | 1 + 15 files changed, 327 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/example/mobile_labs/api/MyServerService.kt create mode 100644 app/src/main/java/com/example/mobile_labs/api/models/EventRemote.kt create mode 100644 app/src/main/java/com/example/mobile_labs/api/models/PerformanceRemote.kt create mode 100644 app/src/main/java/com/example/mobile_labs/api/models/PersonRemote.kt create mode 100644 app/src/main/java/com/example/mobile_labs/api/people/PeopleRemoteMediator.kt create mode 100644 app/src/main/java/com/example/mobile_labs/api/people/RestPersonRepository.kt diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index ac823c7..383f5de 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -7,11 +7,11 @@ - + - + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 66803e2..9815ed3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7f574bd..f6fa257 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ android:supportsRtl="true" android:theme="@style/Theme.Mobile_Labs" android:name=".TheatreApplication" + android:usesCleartextTraffic="true" tools:targetApi="31"> + + @GET("performances") + suspend fun getPerformances( + @Query("_page") page: Int, + @Query("_limit") limit: Int, + ): List + + @GET("events") + suspend fun getEvents( + @Query("_page") page: Int, + @Query("_limit") limit: Int, + ): List + + @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 } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mobile_labs/api/models/EventRemote.kt b/app/src/main/java/com/example/mobile_labs/api/models/EventRemote.kt new file mode 100644 index 0000000..3320fac --- /dev/null +++ b/app/src/main/java/com/example/mobile_labs/api/models/EventRemote.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mobile_labs/api/models/PerformanceRemote.kt b/app/src/main/java/com/example/mobile_labs/api/models/PerformanceRemote.kt new file mode 100644 index 0000000..5e1728f --- /dev/null +++ b/app/src/main/java/com/example/mobile_labs/api/models/PerformanceRemote.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mobile_labs/api/models/PersonRemote.kt b/app/src/main/java/com/example/mobile_labs/api/models/PersonRemote.kt new file mode 100644 index 0000000..fa4da4b --- /dev/null +++ b/app/src/main/java/com/example/mobile_labs/api/models/PersonRemote.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mobile_labs/api/people/PeopleRemoteMediator.kt b/app/src/main/java/com/example/mobile_labs/api/people/PeopleRemoteMediator.kt new file mode 100644 index 0000000..115a67c --- /dev/null +++ b/app/src/main/java/com/example/mobile_labs/api/people/PeopleRemoteMediator.kt @@ -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() { + + override suspend fun initialize(): InitializeAction { + return InitializeAction.LAUNCH_INITIAL_REFRESH + } + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): 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): RemoteKeys? { + return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull() + ?.let { person -> + dbRemoteKeyRepository.getAllRemoteKeys(person.uid !!, RemoteKeyType.PERSON) + } + } + + private suspend fun getRemoteKeyForFirstItem(state: PagingState): RemoteKeys? { + return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull() + ?.let { person -> + dbRemoteKeyRepository.getAllRemoteKeys(person.uid !!, RemoteKeyType.PERSON) + } + } + + private suspend fun getRemoteKeyClosestToCurrentPosition( + state: PagingState + ): RemoteKeys? { + return state.anchorPosition?.let { position -> + state.closestItemToPosition(position)?.uid?.let { personUid -> + dbRemoteKeyRepository.getAllRemoteKeys(personUid, RemoteKeyType.PERSON) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mobile_labs/api/people/RestPersonRepository.kt b/app/src/main/java/com/example/mobile_labs/api/people/RestPersonRepository.kt new file mode 100644 index 0000000..e7a3729 --- /dev/null +++ b/app/src/main/java/com/example/mobile_labs/api/people/RestPersonRepository.kt @@ -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> { + + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mobile_labs/common/AppContainer.kt b/app/src/main/java/com/example/mobile_labs/common/AppContainer.kt index 872b32c..ddf580e 100644 --- a/app/src/main/java/com/example/mobile_labs/common/AppContainer.kt +++ b/app/src/main/java/com/example/mobile_labs/common/AppContainer.kt @@ -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 diff --git a/app/src/main/java/com/example/mobile_labs/common/AppViewModelProvider.kt b/app/src/main/java/com/example/mobile_labs/common/AppViewModelProvider.kt index 6ddee02..a66a63a 100644 --- a/app/src/main/java/com/example/mobile_labs/common/AppViewModelProvider.kt +++ b/app/src/main/java/com/example/mobile_labs/common/AppViewModelProvider.kt @@ -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) diff --git a/app/src/main/java/com/example/mobile_labs/common/PersonRepository.kt b/app/src/main/java/com/example/mobile_labs/common/PersonRepository.kt index 8a6f5b1..764fa8f 100644 --- a/app/src/main/java/com/example/mobile_labs/common/PersonRepository.kt +++ b/app/src/main/java/com/example/mobile_labs/common/PersonRepository.kt @@ -6,8 +6,4 @@ import kotlinx.coroutines.flow.Flow interface PersonRepository { fun getAllPeople(): Flow> - fun getPerson(uid: Int): Flow - suspend fun insertPerson(person: Person) - suspend fun updatePerson(person: Person) - suspend fun deletePerson(person: Person) } \ No newline at end of file diff --git a/app/src/main/java/com/example/mobile_labs/database/person/dao/PersonDao.kt b/app/src/main/java/com/example/mobile_labs/database/person/dao/PersonDao.kt index 462f81f..feb4a40 100644 --- a/app/src/main/java/com/example/mobile_labs/database/person/dao/PersonDao.kt +++ b/app/src/main/java/com/example/mobile_labs/database/person/dao/PersonDao.kt @@ -18,11 +18,14 @@ interface PersonDao { fun getByUid(uid: Int): Flow @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() } \ No newline at end of file diff --git a/app/src/main/java/com/example/mobile_labs/database/person/repository/OfflinePersonRepository.kt b/app/src/main/java/com/example/mobile_labs/database/person/repository/OfflinePersonRepository.kt index 2f45a3d..4fc1d8b 100644 --- a/app/src/main/java/com/example/mobile_labs/database/person/repository/OfflinePersonRepository.kt +++ b/app/src/main/java/com/example/mobile_labs/database/person/repository/OfflinePersonRepository.kt @@ -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 = personDao.getByUid(uid); + fun getPerson(uid: Int): Flow = 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) = + personDao.insert(*people.toTypedArray()) + + fun getAllPeoplePagingSource(): PagingSource = personDao.getAll() } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index bd1eb8a..04a426d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 } \ No newline at end of file