From 9ca225db942dacdf0284ebbb5c60a6a28469cd57 Mon Sep 17 00:00:00 2001 From: Shcherbatykh Oleg Date: Mon, 16 Feb 2026 11:58:24 +0300 Subject: [PATCH] Magrate schedule domain to flow --- .../java/ru/fincode/tsudesk/MainActivity.kt | 26 +++++-- .../fincode/tsudesk/di/NetworkDebugModule.kt | 2 +- .../core/common/{config => app}/AppConfig.kt | 4 +- .../tsudesk/core/common/model/DataResult.kt | 16 +++++ .../core/database/schedule/ScheduleDao.kt | 40 ++++++----- .../core/network/OkHttpClientFactory.kt | 2 +- .../tsudesk/core/network/di/RetrofitModule.kt | 2 +- feature/schedule/build.gradle.kts | 1 + .../schedule/data/ScheduleRepositoryImpl.kt | 68 +++++++++++++++---- .../datasource/ScheduleLocalDataSource.kt | 2 + .../data/local/ScheduleLocalDataSourceImpl.kt | 9 +++ .../domain/repository/ScheduleRepository.kt | 7 +- .../domain/usecase/GetScheduleUseCase.kt | 9 ++- gradle/libs.versions.toml | 8 +-- 14 files changed, 139 insertions(+), 57 deletions(-) rename core/common/src/main/java/ru/fincode/tsudesk/core/common/{config => app}/AppConfig.kt (68%) create mode 100644 core/common/src/main/java/ru/fincode/tsudesk/core/common/model/DataResult.kt diff --git a/app/src/main/java/ru/fincode/tsudesk/MainActivity.kt b/app/src/main/java/ru/fincode/tsudesk/MainActivity.kt index 9b37427..3e4f419 100644 --- a/app/src/main/java/ru/fincode/tsudesk/MainActivity.kt +++ b/app/src/main/java/ru/fincode/tsudesk/MainActivity.kt @@ -6,10 +6,13 @@ import androidx.activity.ComponentActivity import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import ru.fincode.tsudesk.core.common.model.DataResult import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType import ru.fincode.tsudesk.feature.schedule.domain.usecase.GetScheduleUseCase import javax.inject.Inject +private const val LOG_TAG = "NETWORK_DEBUG" + @AndroidEntryPoint class MainActivity : ComponentActivity() { @Inject @@ -19,13 +22,22 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) lifecycleScope.launch { - try { - val result = getScheduleUseCase( - ScheduleType.Group("220631") - ) - Log.d("TSUDesk", result.toString()) - } catch (e: Exception) { - Log.e("TSUDesk", "Error loading schedule", e) + getScheduleUseCase( + ScheduleType.Group("220631") // пример группы + ).collect { result -> + when (result) { + is DataResult.Cache -> { + Log.d(LOG_TAG, "CACHE: ${result.data}") + } + + is DataResult.Network -> { + Log.d(LOG_TAG, "NETWORK: ${result.data}") + } + + is DataResult.Error -> { + Log.e(LOG_TAG, "ERROR: ${result.throwable}") + } + } } } } diff --git a/app/src/main/java/ru/fincode/tsudesk/di/NetworkDebugModule.kt b/app/src/main/java/ru/fincode/tsudesk/di/NetworkDebugModule.kt index e21ae77..cbed2b5 100644 --- a/app/src/main/java/ru/fincode/tsudesk/di/NetworkDebugModule.kt +++ b/app/src/main/java/ru/fincode/tsudesk/di/NetworkDebugModule.kt @@ -5,7 +5,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import ru.fincode.tsudesk.BuildConfig -import ru.fincode.tsudesk.core.common.config.AppConfig +import ru.fincode.tsudesk.core.common.app.AppConfig import javax.inject.Singleton @Module diff --git a/core/common/src/main/java/ru/fincode/tsudesk/core/common/config/AppConfig.kt b/core/common/src/main/java/ru/fincode/tsudesk/core/common/app/AppConfig.kt similarity index 68% rename from core/common/src/main/java/ru/fincode/tsudesk/core/common/config/AppConfig.kt rename to core/common/src/main/java/ru/fincode/tsudesk/core/common/app/AppConfig.kt index 33d19b3..9fe3e6e 100644 --- a/core/common/src/main/java/ru/fincode/tsudesk/core/common/config/AppConfig.kt +++ b/core/common/src/main/java/ru/fincode/tsudesk/core/common/app/AppConfig.kt @@ -1,7 +1,7 @@ -package ru.fincode.tsudesk.core.common.config +package ru.fincode.tsudesk.core.common.app data class AppConfig( val isDebug: Boolean, val baseUrl: String, val networkTimeoutSec: Long -) +) \ No newline at end of file diff --git a/core/common/src/main/java/ru/fincode/tsudesk/core/common/model/DataResult.kt b/core/common/src/main/java/ru/fincode/tsudesk/core/common/model/DataResult.kt new file mode 100644 index 0000000..3bd51b7 --- /dev/null +++ b/core/common/src/main/java/ru/fincode/tsudesk/core/common/model/DataResult.kt @@ -0,0 +1,16 @@ +package ru.fincode.tsudesk.core.common.model + +sealed interface DataResult { + + data class Cache( + val data: T + ) : DataResult + + data class Network( + val data: T + ) : DataResult + + data class Error( + val throwable: Throwable + ) : DataResult +} diff --git a/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/ScheduleDao.kt b/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/ScheduleDao.kt index 895b341..ddf475c 100644 --- a/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/ScheduleDao.kt +++ b/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/ScheduleDao.kt @@ -1,49 +1,55 @@ package ru.fincode.tsudesk.core.database.schedule import androidx.room.Dao -import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction -import ru.fincode.tsudesk.core.database.schedule.Query.DELETE_LESSON_BY_GROUP_KEY_QUERY +import kotlinx.coroutines.flow.Flow import ru.fincode.tsudesk.core.database.schedule.Query.SELECT_LESSON_BY_KEY_QUERY import ru.fincode.tsudesk.core.database.schedule.Query.SELECT_SCHEDULE_BY_KEY_QUERY @Dao interface ScheduleDao { - @Insert(onConflict = OnConflictStrategy.Companion.REPLACE) - suspend fun upsertSchedule(schedule: ScheduleCacheEntity) - - @Insert(onConflict = OnConflictStrategy.Companion.REPLACE) - suspend fun insertLessons(lessons: List) - @Query(SELECT_SCHEDULE_BY_KEY_QUERY) - suspend fun getSchedule(key: String): ScheduleCacheEntity? + fun observeSchedule(key: String): Flow @Query(SELECT_LESSON_BY_KEY_QUERY) + fun observeLessons(key: String): Flow> + + @Query("SELECT * FROM schedule_cache WHERE `key` = :key LIMIT 1") + suspend fun getSchedule(key: String): ScheduleCacheEntity? + + @Query("SELECT * FROM lesson_cache WHERE scheduleKey = :key") suspend fun getLessons(key: String): List - @Query(DELETE_LESSON_BY_GROUP_KEY_QUERY) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertSchedule(schedule: ScheduleCacheEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertLessons(lessons: List) + + @Query("DELETE FROM lesson_cache WHERE scheduleKey = :key") suspend fun deleteLessonsByKey(key: String) - @Delete - suspend fun deleteSchedule(entity: ScheduleCacheEntity) + @Query("DELETE FROM schedule_cache WHERE `key` = :key") + suspend fun deleteScheduleByKey(key: String) @Transaction suspend fun replaceSchedule( - key: String, schedule: ScheduleCacheEntity, lessons: List + key: String, + schedule: ScheduleCacheEntity, + lessons: List ) { - upsertSchedule(schedule) deleteLessonsByKey(key) + upsertSchedule(schedule) insertLessons(lessons) } @Transaction suspend fun clearSchedule(key: String) { deleteLessonsByKey(key) - val header = getSchedule(key) ?: return - deleteSchedule(header) + deleteScheduleByKey(key) } -} \ No newline at end of file +} diff --git a/core/network/src/main/java/ru/fincode/tsudesk/core/network/OkHttpClientFactory.kt b/core/network/src/main/java/ru/fincode/tsudesk/core/network/OkHttpClientFactory.kt index 2c5a5d8..b59ffe4 100644 --- a/core/network/src/main/java/ru/fincode/tsudesk/core/network/OkHttpClientFactory.kt +++ b/core/network/src/main/java/ru/fincode/tsudesk/core/network/OkHttpClientFactory.kt @@ -1,7 +1,7 @@ package ru.fincode.tsudesk.core.network import okhttp3.OkHttpClient -import ru.fincode.tsudesk.core.common.config.AppConfig +import ru.fincode.tsudesk.core.common.app.AppConfig import ru.fincode.tsudesk.core.network.interceptor.DebugInterceptor import java.util.concurrent.TimeUnit import javax.inject.Inject diff --git a/core/network/src/main/java/ru/fincode/tsudesk/core/network/di/RetrofitModule.kt b/core/network/src/main/java/ru/fincode/tsudesk/core/network/di/RetrofitModule.kt index 331d4e1..e861a8d 100644 --- a/core/network/src/main/java/ru/fincode/tsudesk/core/network/di/RetrofitModule.kt +++ b/core/network/src/main/java/ru/fincode/tsudesk/core/network/di/RetrofitModule.kt @@ -6,7 +6,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import retrofit2.Retrofit -import ru.fincode.tsudesk.core.common.config.AppConfig +import ru.fincode.tsudesk.core.common.app.AppConfig import ru.fincode.tsudesk.core.network.RetrofitFactory import javax.inject.Singleton diff --git a/feature/schedule/build.gradle.kts b/feature/schedule/build.gradle.kts index 31ca44f..693694c 100644 --- a/feature/schedule/build.gradle.kts +++ b/feature/schedule/build.gradle.kts @@ -43,4 +43,5 @@ dependencies { implementation(project(":core:network")) implementation(project(":core:database")) + implementation(project(":core:common")) } \ No newline at end of file diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/ScheduleRepositoryImpl.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/ScheduleRepositoryImpl.kt index 5de7a4a..d77b1ac 100644 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/ScheduleRepositoryImpl.kt +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/ScheduleRepositoryImpl.kt @@ -1,5 +1,13 @@ package ru.fincode.tsudesk.feature.schedule.data +import android.os.Build +import androidx.annotation.RequiresApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch +import ru.fincode.tsudesk.core.common.model.DataResult +import ru.fincode.tsudesk.core.common.model.DataResult.Error import ru.fincode.tsudesk.core.network.model.NetworkResult import ru.fincode.tsudesk.core.network.model.map import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleLocalDataSource @@ -7,6 +15,7 @@ import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleRemoteDataSou import ru.fincode.tsudesk.feature.schedule.data.mapper.ScheduleCacheKey import ru.fincode.tsudesk.feature.schedule.data.mapper.ScheduleNetworkMapper import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity +import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType import ru.fincode.tsudesk.feature.schedule.domain.repository.ScheduleRepository import javax.inject.Inject @@ -16,19 +25,50 @@ class ScheduleRepositoryImpl @Inject constructor( private val mapper: ScheduleNetworkMapper ) : ScheduleRepository { - override suspend fun loadScheduleByGroup(number: String): NetworkResult { - val result = remote.loadScheduleByGroup(number).map(mapper::invoke) - if (result is NetworkResult.Success) { - local.saveSchedule(ScheduleCacheKey.group(number), result.data) - } - return result - } + @RequiresApi(Build.VERSION_CODES.O) + override fun observeSchedule(type: ScheduleType): Flow> = + channelFlow { + val key = when (type) { + is ScheduleType.Group -> ScheduleCacheKey.group(type.number) + is ScheduleType.Teacher -> ScheduleCacheKey.teacher(type.name) + } - override suspend fun loadScheduleByTeacher(name: String): NetworkResult { - val result = remote.loadScheduleByTeacher(name).map(mapper::invoke) - if (result is NetworkResult.Success) { - local.saveSchedule(ScheduleCacheKey.teacher(name), result.data) + // кэш -> UI + val cacheJob = launch { + local.observeSchedule(key).collect { cached -> + if (cached != null) send(DataResult.Cache(cached)) + } + } + + val networkResult: NetworkResult = when (type) { + is ScheduleType.Group -> + remote.loadScheduleByGroup(type.number).map(mapper::invoke) + + is ScheduleType.Teacher -> + remote.loadScheduleByTeacher(type.name).map(mapper::invoke) + } + + when (networkResult) { + is NetworkResult.Success -> { + // (опционально) сразу отдать сетевой результат + send(DataResult.Network(networkResult.data)) + + // single source of truth -> сохраняем в БД + local.saveSchedule(key, networkResult.data) + + // дать Room время эмитнуть обновление + kotlinx.coroutines.yield() + + // закрываем поток по твоему требованию + close() + } + + is NetworkResult.Error -> { + send(Error(Throwable(networkResult.error.toString()))) + close() + } + } + + awaitClose { cacheJob.cancel() } } - return result - } -} \ No newline at end of file +} diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleLocalDataSource.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleLocalDataSource.kt index 1c475b1..94eba47 100644 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleLocalDataSource.kt +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/datasource/ScheduleLocalDataSource.kt @@ -1,9 +1,11 @@ package ru.fincode.tsudesk.feature.schedule.data.datasource +import kotlinx.coroutines.flow.Flow import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity interface ScheduleLocalDataSource { suspend fun saveSchedule(key: String, schedule: ScheduleEntity) suspend fun loadSchedule(key: String): ScheduleEntity? + fun observeSchedule(key: String): Flow suspend fun removeSchedule(key: String) } diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/local/ScheduleLocalDataSourceImpl.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/local/ScheduleLocalDataSourceImpl.kt index 3786d09..56da269 100644 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/local/ScheduleLocalDataSourceImpl.kt +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/local/ScheduleLocalDataSourceImpl.kt @@ -1,5 +1,7 @@ package ru.fincode.tsudesk.feature.schedule.data.local +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import ru.fincode.tsudesk.core.database.schedule.ScheduleDao import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleLocalDataSource import ru.fincode.tsudesk.feature.schedule.data.mapper.toCache @@ -22,6 +24,13 @@ class ScheduleLocalDataSourceImpl @Inject constructor( return schedule.toDomain(lessons) } + override fun observeSchedule(key: String): Flow { + return dao.observeSchedule(key) + .combine(dao.observeLessons(key)) { schedule, lessons -> + schedule?.toDomain(lessons) + } + } + override suspend fun removeSchedule(key: String) { dao.deleteLessonsByKey(key) } diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/repository/ScheduleRepository.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/repository/ScheduleRepository.kt index a85bf5a..fc16acb 100644 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/repository/ScheduleRepository.kt +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/repository/ScheduleRepository.kt @@ -1,9 +1,10 @@ package ru.fincode.tsudesk.feature.schedule.domain.repository -import ru.fincode.tsudesk.core.network.model.NetworkResult +import kotlinx.coroutines.flow.Flow +import ru.fincode.tsudesk.core.common.model.DataResult import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity +import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType interface ScheduleRepository { - suspend fun loadScheduleByGroup(number: String): NetworkResult - suspend fun loadScheduleByTeacher(name: String): NetworkResult + fun observeSchedule(type: ScheduleType): Flow> } diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/usecase/GetScheduleUseCase.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/usecase/GetScheduleUseCase.kt index 9863d5e..0960a89 100644 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/usecase/GetScheduleUseCase.kt +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/domain/usecase/GetScheduleUseCase.kt @@ -1,6 +1,7 @@ package ru.fincode.tsudesk.feature.schedule.domain.usecase -import ru.fincode.tsudesk.core.network.model.NetworkResult +import kotlinx.coroutines.flow.Flow +import ru.fincode.tsudesk.core.common.model.DataResult import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType import ru.fincode.tsudesk.feature.schedule.domain.repository.ScheduleRepository @@ -9,8 +10,6 @@ import javax.inject.Inject class GetScheduleUseCase @Inject constructor( private val repository: ScheduleRepository ) { - suspend operator fun invoke(type: ScheduleType): NetworkResult = when (type) { - is ScheduleType.Group -> repository.loadScheduleByGroup(type.number) - is ScheduleType.Teacher -> repository.loadScheduleByTeacher(type.name) - } + operator fun invoke(type: ScheduleType): Flow> = + repository.observeSchedule(type) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09e86d1..ce166c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,17 +24,13 @@ lifecycle = "2.7.0" coroutines = "1.8.1" room = "2.6.1" -junit = "4.13.2" -junitVersion = "1.1.5" -espressoCore = "3.5.1" - [libraries] -#Android +# Android androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } -#UI: AndroidX, Jetpack Compose androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +# UI: AndroidX, Jetpack Compose material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }