Add Room database, create tables and DAO for schedule

This commit is contained in:
Shcherbatykh Oleg
2026-02-13 12:57:41 +03:00
parent debc838893
commit 7dd3358dac
18 changed files with 306 additions and 31 deletions

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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"
}

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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<LessonCacheEntity>)
@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<LessonCacheEntity>
// 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<LessonCacheEntity>
) {
upsertSchedule(schedule)
deleteLessonsByKey(key)
insertLessons(lessons)
}
/**
* Полная очистка кэша по ключу без прямого DELETE для schedule_cache.
*/
@Transaction
suspend fun clearSchedule(key: String) {
deleteLessonsByKey(key)
val header = getSchedule(key) ?: return
deleteSchedule(header)
}
}

View File

@@ -36,7 +36,7 @@ android {
dependencies {
implementation(libs.core.ktx)
kapt(libs.hiltcompiler)
kapt(libs.hilt.compiler)
implementation(libs.hiltandroid)
api(libs.retrofit)

View File

@@ -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"))
}

View File

@@ -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<ScheduleEntity> =
remote.loadScheduleByGroup(number).map(mapper::invoke)
override suspend fun loadScheduleByTeacher(name: String): NetworkResult<ScheduleEntity> =
remote.loadScheduleByTeacher(name).map(mapper::invoke)
}
override suspend fun loadScheduleByGroup(number: String): NetworkResult<ScheduleEntity> {
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<ScheduleEntity> {
val result = remote.loadScheduleByTeacher(name).map(mapper::invoke)
if (result is NetworkResult.Success) {
local.save(ScheduleCacheKey.teacher(name), result.data)
}
return result
}
}

View File

@@ -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)
}

View File

@@ -1,4 +0,0 @@
package ru.fincode.tsudesk.feature.schedule.data.local;
public class ScheduleDaoFacade {
}

View File

@@ -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)
}
}

View File

@@ -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(

View File

@@ -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<ScheduleCacheEntity, List<LessonCacheEntity>> {
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<LessonCacheEntity>
): 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
)

View File

@@ -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
}

View File

@@ -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" }