diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a0601c1..b4cbcf9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -43,7 +43,9 @@ android { jvmTarget = jvm.toString() } } - +kapt { + correctErrorTypes = true +} dependencies { implementation(libs.core.ktx) implementation(libs.androidx.appcompat) @@ -51,7 +53,7 @@ dependencies { implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) - kapt(libs.hiltcompiler) + kapt(libs.hilt.compiler) implementation(libs.hiltandroid) implementation(libs.okhttp) diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index a667871..cbbda30 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.kapt) } android { @@ -16,8 +17,7 @@ android { release { isMinifyEnabled = false proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } @@ -30,7 +30,14 @@ android { jvmTarget = jvm.toString() } } - +kapt { + correctErrorTypes = true +} dependencies { implementation(libs.core.ktx) + implementation(libs.room.runtime) + implementation(libs.room.ktx) + kapt(libs.room.compiler) + implementation(libs.hiltandroid) + kapt(libs.hilt.compiler) } \ No newline at end of file diff --git a/core/database/src/main/java/ru/fincode/tsudesk/core/database/AppDatabase.kt b/core/database/src/main/java/ru/fincode/tsudesk/core/database/AppDatabase.kt new file mode 100644 index 0000000..cd6336f --- /dev/null +++ b/core/database/src/main/java/ru/fincode/tsudesk/core/database/AppDatabase.kt @@ -0,0 +1,24 @@ +package ru.fincode.tsudesk.core.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import ru.fincode.tsudesk.core.database.schedule.ScheduleDao +import ru.fincode.tsudesk.core.database.schedule.LessonCacheEntity +import ru.fincode.tsudesk.core.database.schedule.ScheduleCacheEntity + +@Database( + entities = [ + // schedule feature + ScheduleCacheEntity::class, + LessonCacheEntity::class, + + // future: + // NewsEntity::class, + // GradeEntity::class, + ], + version = 1, + exportSchema = false +) +abstract class AppDatabase : RoomDatabase() { + abstract fun scheduleCacheDao(): ScheduleDao +} diff --git a/core/database/src/main/java/ru/fincode/tsudesk/core/database/di/DatabaseModule.kt b/core/database/src/main/java/ru/fincode/tsudesk/core/database/di/DatabaseModule.kt new file mode 100644 index 0000000..73455fe --- /dev/null +++ b/core/database/src/main/java/ru/fincode/tsudesk/core/database/di/DatabaseModule.kt @@ -0,0 +1,30 @@ +package ru.fincode.tsudesk.core.database.di + +import android.content.Context +import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import ru.fincode.tsudesk.core.database.AppDatabase +import ru.fincode.tsudesk.core.database.schedule.ScheduleDao +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + private const val DB_NAME = "tsudesk.db" + + @Provides + @Singleton + fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase = + Room.databaseBuilder(context, AppDatabase::class.java, DB_NAME) + .fallbackToDestructiveMigration() + .build() + + @Provides + fun provideScheduleCacheDao(db: AppDatabase): ScheduleDao = + db.scheduleCacheDao() +} diff --git a/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/DBConstant.kt b/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/DBConstant.kt new file mode 100644 index 0000000..6c018d4 --- /dev/null +++ b/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/DBConstant.kt @@ -0,0 +1,9 @@ +package ru.fincode.tsudesk.core.database.schedule + +object ScheduleDbConstants { + const val SCHEDULE_TABLE = "schedule_cache" + const val LESSON_TABLE = "lesson_cache" + + const val COL_SCHEDULE_KEY = "scheduleKey" + const val COL_KEY = "key" +} diff --git a/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/LessonCacheEntity.kt b/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/LessonCacheEntity.kt new file mode 100644 index 0000000..47dc271 --- /dev/null +++ b/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/LessonCacheEntity.kt @@ -0,0 +1,24 @@ +package ru.fincode.tsudesk.core.database.schedule + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import ru.fincode.tsudesk.core.database.schedule.ScheduleDbConstants.COL_SCHEDULE_KEY +import ru.fincode.tsudesk.core.database.schedule.ScheduleDbConstants.LESSON_TABLE + +@Entity( + tableName = LESSON_TABLE, indices = [Index(value = [COL_SCHEDULE_KEY])] +) +data class LessonCacheEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + + val scheduleKey: String, + val date: String, + val time: String, + val name: String, + val typeName: String, + val room: String, + val teacher: String, + val groupId: String, + val type: String +) diff --git a/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/ScheduleCacheEntity.kt b/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/ScheduleCacheEntity.kt new file mode 100644 index 0000000..7482f81 --- /dev/null +++ b/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/ScheduleCacheEntity.kt @@ -0,0 +1,12 @@ +package ru.fincode.tsudesk.core.database.schedule + +import androidx.room.Entity +import androidx.room.PrimaryKey +import ru.fincode.tsudesk.core.database.schedule.ScheduleDbConstants.SCHEDULE_TABLE + +@Entity(tableName = SCHEDULE_TABLE) +data class ScheduleCacheEntity( + @PrimaryKey + val key: String, // "group:220631" | "teacher:ФИО" + val timestamp: Long +) 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 new file mode 100644 index 0000000..b5c3e2e --- /dev/null +++ b/core/database/src/main/java/ru/fincode/tsudesk/core/database/schedule/ScheduleDao.kt @@ -0,0 +1,57 @@ +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 + +@Dao +interface ScheduleDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertSchedule(schedule: ScheduleCacheEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertLessons(lessons: List) + + + @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 + + // Bulk delete уроков по ключу — самый простой/эффективный путь в Room это @Query + @Query("DELETE FROM lesson_cache WHERE scheduleKey = :key") + suspend fun deleteLessonsByKey(key: String) + + // Удаление schedule_cache без SQL: типобезопасно + @Delete + suspend fun deleteSchedule(entity: ScheduleCacheEntity) + + /** + * Обновить кэш расписания (header + lessons) атомарно. + */ + @Transaction + suspend fun replaceSchedule( + key: String, + schedule: ScheduleCacheEntity, + lessons: List + ) { + upsertSchedule(schedule) + deleteLessonsByKey(key) + insertLessons(lessons) + } + + /** + * Полная очистка кэша по ключу без прямого DELETE для schedule_cache. + */ + @Transaction + suspend fun clearSchedule(key: String) { + deleteLessonsByKey(key) + val header = getSchedule(key) ?: return + deleteSchedule(header) + } +} diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 7fc2d8a..c0974b9 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -36,7 +36,7 @@ android { dependencies { implementation(libs.core.ktx) - kapt(libs.hiltcompiler) + kapt(libs.hilt.compiler) implementation(libs.hiltandroid) api(libs.retrofit) diff --git a/feature/schedule/build.gradle.kts b/feature/schedule/build.gradle.kts index af17808..19f2498 100644 --- a/feature/schedule/build.gradle.kts +++ b/feature/schedule/build.gradle.kts @@ -19,8 +19,7 @@ android { release { isMinifyEnabled = false proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } @@ -40,8 +39,9 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.material) - kapt(libs.hiltcompiler) + kapt(libs.hilt.compiler) implementation(libs.hiltandroid) implementation(project(":core:network")) + implementation(project(":core:database")) } \ 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 8fcf4ec..5fbf610 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 @@ -2,22 +2,33 @@ package ru.fincode.tsudesk.feature.schedule.data 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 import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleRemoteDataSource -import ru.fincode.tsudesk.feature.schedule.data.mapper.ScheduleDtoToDomainMapper +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.repository.ScheduleRepository import javax.inject.Inject class ScheduleRepositoryImpl @Inject constructor( private val remote: ScheduleRemoteDataSource, - private val mapper: ScheduleDtoToDomainMapper + private val local: ScheduleLocalDataSource, + private val mapper: ScheduleNetworkMapper ) : ScheduleRepository { - override suspend fun loadScheduleByGroup(number: String): NetworkResult = - remote.loadScheduleByGroup(number).map(mapper::invoke) - - override suspend fun loadScheduleByTeacher(name: String): NetworkResult = - remote.loadScheduleByTeacher(name).map(mapper::invoke) - -} + override suspend fun loadScheduleByGroup(number: String): NetworkResult { + val result = remote.loadScheduleByGroup(number).map(mapper::invoke) + if (result is NetworkResult.Success) { + local.save(ScheduleCacheKey.group(number), result.data) + } + return result + } + override suspend fun loadScheduleByTeacher(name: String): NetworkResult { + val result = remote.loadScheduleByTeacher(name).map(mapper::invoke) + if (result is NetworkResult.Success) { + local.save(ScheduleCacheKey.teacher(name), result.data) + } + 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 b6d7ab4..4c5b601 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 @@ -2,9 +2,8 @@ package ru.fincode.tsudesk.feature.schedule.data.datasource import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity - interface ScheduleLocalDataSource { - suspend fun getScheduleByGroup(groupNumber: String): ScheduleEntity? - suspend fun getScheduleByTeacher(teacherName: String): ScheduleEntity? - suspend fun saveSchedule(entity: ScheduleEntity) + suspend fun save(key: String, schedule: ScheduleEntity) + suspend fun load(key: String): ScheduleEntity? + suspend fun clear(key: String) } diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/local/ScheduleDaoFacade.java b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/local/ScheduleDaoFacade.java deleted file mode 100644 index f185536..0000000 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/local/ScheduleDaoFacade.java +++ /dev/null @@ -1,4 +0,0 @@ -package ru.fincode.tsudesk.feature.schedule.data.local; - -public class ScheduleDaoFacade { -} 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 new file mode 100644 index 0000000..b9d9aea --- /dev/null +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/local/ScheduleLocalDataSourceImpl.kt @@ -0,0 +1,28 @@ +package ru.fincode.tsudesk.feature.schedule.data.local + +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 +import ru.fincode.tsudesk.feature.schedule.data.mapper.toDomain +import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity +import javax.inject.Inject + +class ScheduleLocalDataSourceImpl @Inject constructor( + private val dao: ScheduleDao +) : ScheduleLocalDataSource { + + override suspend fun save(key: String, schedule: ScheduleEntity) { + val (scheduleCache, lessonsCache) = schedule.toCache(key) + dao.replaceSchedule(key, scheduleCache, lessonsCache) + } + + override suspend fun load(key: String): ScheduleEntity? { + val schedule = dao.getSchedule(key) ?: return null + val lessons = dao.getLessons(key) + return schedule.toDomain(lessons) + } + + override suspend fun clear(key: String) { + dao.deleteLessonsByKey(key) + } +} diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleDtoToDomainMapper.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleNetworkMapper.kt similarity index 95% rename from feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleDtoToDomainMapper.kt rename to feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleNetworkMapper.kt index ea18a30..a5662a0 100644 --- a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleDtoToDomainMapper.kt +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleNetworkMapper.kt @@ -7,7 +7,7 @@ import ru.fincode.tsudesk.feature.schedule.domain.model.LessonEntity import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity import javax.inject.Inject -class ScheduleDtoToDomainMapper @Inject constructor() { +class ScheduleNetworkMapper @Inject constructor() { operator fun invoke(dto: ScheduleDto, meta: NetworkMeta): ScheduleEntity = ScheduleEntity( diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleStorageMapper.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleStorageMapper.kt new file mode 100644 index 0000000..c26160f --- /dev/null +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/data/mapper/ScheduleStorageMapper.kt @@ -0,0 +1,59 @@ +package ru.fincode.tsudesk.feature.schedule.data.mapper + +import ru.fincode.tsudesk.core.database.schedule.LessonCacheEntity +import ru.fincode.tsudesk.core.database.schedule.ScheduleCacheEntity +import ru.fincode.tsudesk.feature.schedule.domain.model.LessonEntity +import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity + + +object ScheduleCacheKey { + fun group(number: String) = "group:$number" + fun teacher(name: String) = "teacher:$name" +} + + +fun ScheduleEntity.toCache( + key: String +): Pair> { + + val scheduleCache = ScheduleCacheEntity( + key = key, + timestamp = timestamp + ) + + val lessonsCache = lessons.map { lesson -> + LessonCacheEntity( + scheduleKey = key, + date = lesson.date, + time = lesson.time, + name = lesson.name, + typeName = lesson.typeName, + room = lesson.room, + teacher = lesson.teacher, + groupId = lesson.groupId, + type = lesson.type + ) + } + + return scheduleCache to lessonsCache +} + +fun ScheduleCacheEntity.toDomain( + lessons: List +): ScheduleEntity = + ScheduleEntity( + lessons = lessons.map { it.toDomain() }, + timestamp = timestamp + ) + +private fun LessonCacheEntity.toDomain(): LessonEntity = + LessonEntity( + date = date, + time = time, + name = name, + typeName = typeName, + room = room, + teacher = teacher, + groupId = groupId, + type = type + ) diff --git a/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/di/ScheduleLocalModule.kt b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/di/ScheduleLocalModule.kt new file mode 100644 index 0000000..ae5828f --- /dev/null +++ b/feature/schedule/src/main/java/ru/fincode/tsudesk/feature/schedule/di/ScheduleLocalModule.kt @@ -0,0 +1,20 @@ +package ru.fincode.tsudesk.feature.schedule.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleLocalDataSource +import ru.fincode.tsudesk.feature.schedule.data.local.ScheduleLocalDataSourceImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class ScheduleLocalModule { + + @Binds + @Singleton + abstract fun bindScheduleLocalDataSource( + impl: ScheduleLocalDataSourceImpl + ): ScheduleLocalDataSource +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96c089b..eb6c075 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,15 +49,12 @@ retrofitMoshi = { group = "com.squareup.retrofit2", name = "converter-moshi", ve converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } # DI: Hilt hiltandroid = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } -hiltcompiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } +hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } # DB: Room room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } [plugins] hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }