ValiZhang 11 месяцев назад
Родитель
Сommit
f40eba5ec5

+ 57 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,57 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+  </profile>
+</component>

+ 6 - 0
app/build.gradle.kts

@@ -2,6 +2,7 @@ plugins {
     alias(libs.plugins.android.application)
     alias(libs.plugins.kotlin.android)
     alias(libs.plugins.kotlin.compose)
+    alias(libs.plugins.kotlin.ksp)
 }
 
 android {
@@ -51,6 +52,11 @@ dependencies {
     implementation(libs.androidx.material3)
     implementation(libs.androidx.navigation.compose.android)
     implementation(libs.androidx.material.icons.extended)
+    ksp(libs.androidx.room.compiler)
+    implementation(libs.androidx.room.runtime)
+    implementation(libs.androidx.room.ktx)
+    implementation(libs.androidx.room.testing)
+    implementation(libs.androidx.room.paging)
     testImplementation(libs.junit)
     androidTestImplementation(libs.androidx.junit)
     androidTestImplementation(libs.androidx.espresso.core)

+ 1 - 0
app/src/main/AndroidManifest.xml

@@ -11,6 +11,7 @@
         android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
         android:theme="@style/Theme.JAPP"
+        android:name=".MyApplication"
         tools:targetApi="31">
         <activity
             android:name=".MainActivity"

+ 17 - 0
app/src/main/java/com/lostc/japp/MyApplication.kt

@@ -0,0 +1,17 @@
+package com.lostc.japp
+
+
+import android.app.Application
+import com.lostc.japp.data.local.AppDatabase
+import com.lostc.japp.data.mapper.ChallengeMapper
+import com.lostc.japp.data.repository.ChallengeRepositoryImpl
+
+class MyApplication : Application() {
+    // 使用lazy确保数据库和仓库只在第一次访问时初始化
+    val database by lazy { AppDatabase.getDatabase(this)}
+    val repository by lazy { ChallengeRepositoryImpl(
+        database.challengeDao(),
+        ChallengeMapper
+    ) }
+
+}

+ 30 - 0
app/src/main/java/com/lostc/japp/data/local/AppDatabase.kt

@@ -0,0 +1,30 @@
+package com.lostc.japp.data.local
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import com.lostc.japp.data.local.dao.ChallengeDao
+import com.lostc.japp.data.local.entity.ChallengeEntity
+
+@Database(entities = [ChallengeEntity::class], version = 1)
+abstract class AppDatabase : RoomDatabase() {
+    abstract fun challengeDao(): ChallengeDao
+
+    companion object {
+        @Volatile
+        private var INSTANCE: AppDatabase? = null
+
+        fun getDatabase(context: Context): AppDatabase {
+            return INSTANCE ?: synchronized(this) {
+                val instance = Room.databaseBuilder(
+                    context.applicationContext,
+                    AppDatabase::class.java,
+                    "jay_world_database"
+                ).build()
+                INSTANCE = instance
+                instance
+            }
+        }
+    }
+}

+ 26 - 0
app/src/main/java/com/lostc/japp/data/local/dao/ChallengeDao.kt

@@ -0,0 +1,26 @@
+package com.lostc.japp.data.local.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import com.lostc.japp.data.local.entity.ChallengeEntity
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface ChallengeDao {
+    @Query("SELECT * FROM challengeentity")
+    fun getAll(): Flow<List<ChallengeEntity>>
+
+    @Query("SELECT * FROM challengeentity WHERE bid IN (:challengeIds)")
+    fun loadAllByIds(challengeIds: IntArray): Flow<List<ChallengeEntity>>
+
+    @Query("SELECT * FROM challengeentity WHERE title LIKE :keywords LIMIT 1")
+    fun findByName(keywords: String): ChallengeEntity
+
+    @Insert
+    fun insertAll(vararg users: ChallengeEntity)
+
+    @Delete
+    fun delete(user: ChallengeEntity)
+}

+ 29 - 0
app/src/main/java/com/lostc/japp/data/local/entity/ChallengeEntity.kt

@@ -0,0 +1,29 @@
+package com.lostc.japp.data.local.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class ChallengeEntity(
+    @PrimaryKey(autoGenerate = true) val bid: Int,
+    @ColumnInfo(name = "title") val title: String,
+    @ColumnInfo(name = "description") val description: String,
+    @ColumnInfo(name = "category") val category: Int,
+    @ColumnInfo(name = "difficulty") val difficulty: Int, // 1-简单 2-普通 3-困难 4-地狱
+    @ColumnInfo(name = "start_time") val startTime: String,
+    @ColumnInfo(name = "end_time") val endTime: String,
+    @ColumnInfo(name = "action_days") val actionDays: Int?, // 目标打卡次数,可选
+    @ColumnInfo(name = "status") val status: Int?, // 1-未开始 2-进行中 3-挑战成功 4-挑战失败
+    @ColumnInfo(name = "give_up_reason") val giveUpReason: String?, // 失败原因
+    @ColumnInfo(name = "finish_time") val finishTime: String?,
+)
+
+enum class ChallengeDifficulty(val value: Int) {
+    EASY(1), MEDIUM(2), HARD(3), EXPERT(4);
+}
+
+enum class ChallengeStatus(val value: Int) {
+    INIT(1), PROCESSING(2), SUCCESS(3), FAILED(4);
+}
+

+ 43 - 0
app/src/main/java/com/lostc/japp/data/mapper/ChallengeMapper.kt

@@ -0,0 +1,43 @@
+package com.lostc.japp.data.mapper
+
+import com.lostc.japp.data.local.entity.ChallengeEntity
+import com.lostc.japp.data.model.Challenge
+import com.lostc.japp.data.model.Difficulty
+
+
+object ChallengeMapper {
+    // 将数据库实体转化为业务模型
+    fun fromEntity(entity: ChallengeEntity): Challenge = Challenge(
+        bid = entity.bid,
+        category = entity.category,
+        title = entity.title,
+        description = entity.description,
+        difficulty = when (entity.difficulty) {
+            0 -> Difficulty.EASY
+            1 -> Difficulty.MEDIUM
+            2 -> Difficulty.HARD
+            else -> Difficulty.MEDIUM // 默认值
+        },
+        actionDays = entity.actionDays,
+        status = entity.status,
+        startTime = entity.startTime,
+        endTime = entity.endTime,
+        finishTime = entity.finishTime,
+        giveUpReason = entity.giveUpReason,
+    )
+
+    // 业务模型转数据库实体(用于缓存)
+    fun toEntity(cha: Challenge): ChallengeEntity = ChallengeEntity(
+        bid = cha.bid,
+        title = cha.title,
+        description = cha.description,
+        category = cha.category,
+        difficulty = 1,
+        startTime = "",
+        endTime = "",
+        actionDays = cha.actionDays,
+        status = cha.status,
+        giveUpReason = cha.giveUpReason,
+        finishTime = ""
+    )
+}

+ 19 - 0
app/src/main/java/com/lostc/japp/data/model/challenge.kt

@@ -0,0 +1,19 @@
+package com.lostc.japp.data.model
+
+data class Challenge(
+    val bid: Int,
+    val category: Int,
+    val title: String,
+    val description: String,
+    val difficulty: Difficulty,
+    val actionDays: Int?,
+    val status: Int?,
+    val giveUpReason: String?,
+    val startTime: String?,
+    val endTime: String?,
+    val finishTime: String?
+)
+
+enum class Difficulty {
+    EASY, MEDIUM, HARD, HELL
+}

+ 27 - 0
app/src/main/java/com/lostc/japp/data/repository/ChallengeRepository.kt

@@ -0,0 +1,27 @@
+package com.lostc.japp.data.repository
+
+import com.lostc.japp.data.local.dao.ChallengeDao
+import com.lostc.japp.data.mapper.ChallengeMapper
+import com.lostc.japp.data.model.Challenge
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+
+interface ChallengeRepository {
+    fun allChallenges(): Flow<List<Challenge>>
+}
+
+
+class ChallengeRepositoryImpl(
+    private val challengeDao: ChallengeDao,
+    private val challengeMapper: ChallengeMapper
+) : ChallengeRepository {
+
+    override fun allChallenges(): Flow<List<Challenge>> {
+        return challengeDao.getAll().map { list ->
+            list.map { entity ->
+                challengeMapper.fromEntity(entity) // 实体转业务模型
+            }
+        }
+    }
+}

+ 231 - 4
app/src/main/java/com/lostc/japp/ui/features/challenge/ChallengeScreen.kt

@@ -1,18 +1,245 @@
 package com.lostc.japp.ui.features.challenge
 
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.List
+import androidx.compose.material.icons.filled.AccessTime
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.lostc.japp.MyApplication
+import com.lostc.japp.data.model.Challenge
+import com.lostc.japp.data.model.Difficulty
+
+//@Composable
+//fun ChallengeListScreen(modifier: Modifier = Modifier) {
+//    Box(
+//        modifier = Modifier.fillMaxSize(),
+//        contentAlignment = Alignment.Center
+//    ) {
+//        Text("Challenge Screen")
+//    }
+//}
+
+// ChallengeListScreen.kt
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ChallengeListScreen(
+    viewModel: ChallengeViewModel = viewModel(
+        factory = ChallengeViewModelFactory((LocalContext.current.applicationContext as MyApplication).repository)
+    )
+) {
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+    Column(modifier = Modifier.fillMaxSize()) {
+        TopAppBar(
+            title = { Text("挑战列表") },
+            actions = {
+                IconButton(onClick = { viewModel.refreshChallenges() }) {
+                    Icon(Icons.Filled.Refresh, contentDescription = "刷新")
+                }
+            }
+        )
+
+        when {
+            uiState.isLoading -> {
+                Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+                    CircularProgressIndicator()
+                }
+            }
+            uiState.error != null -> {
+                ErrorView(message = uiState.error!!) {
+                    viewModel.refreshChallenges()
+                }
+            }
+            uiState.challenges.isEmpty() -> {
+                EmptyView()
+            }
+            else -> {
+                ChallengeList(challenges = uiState.challenges)
+            }
+        }
+    }
+}
+
+@Composable
+fun ChallengeList(challenges: List<Challenge>) {
+    LazyColumn(
+        modifier = Modifier.fillMaxSize(),
+        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
+    ) {
+        items(challenges) { challenge ->
+            ChallengeCard(challenge = challenge)
+        }
+    }
+}
+
+@Composable
+fun ChallengeCard(challenge: Challenge) {
+    Card(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(vertical = 8.dp),
+        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
+    ) {
+        Column(
+            modifier = Modifier.padding(16.dp)
+        ) {
+            Row(
+                modifier = Modifier.fillMaxWidth(),
+                horizontalArrangement = Arrangement.SpaceBetween
+            ) {
+                Text(
+                    text = challenge.title,
+                    style = MaterialTheme.typography.titleMedium,
+                    fontWeight = FontWeight.Bold
+                )
+
+                // 难度标签
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .padding(start = 8.dp)
+                        .background(
+                            color = when (challenge.difficulty) {
+                                Difficulty.EASY -> Color(0xFFA5D6A7)
+                                Difficulty.MEDIUM -> Color(0xFFFFECB3)
+                                Difficulty.HARD -> Color(0xFFF5DEB3)
+                                Difficulty.HELL -> Color(0xFFF5DEB3)
+                            },
+                            shape = RoundedCornerShape(12.dp)
+                        )
+                        .padding(horizontal = 8.dp, vertical = 4.dp)
+                ) {
+                    Text(
+                        text = challenge.difficulty.name,
+                        style = MaterialTheme.typography.labelSmall,
+                        color = when (challenge.difficulty) {
+                            Difficulty.EASY -> Color(0xFF2E7D32)
+                            Difficulty.MEDIUM -> Color(0xFFFF8F00)
+                            Difficulty.HARD -> Color(0xFFBF360C)
+                            Difficulty.HELL->Color(0xFFBF360C)
+                        }
+                    )
+                }
+            }
+
+            Spacer(modifier = Modifier.height(8.dp))
+
+            Text(
+                text = challenge.description,
+                style = MaterialTheme.typography.bodyMedium,
+                color = MaterialTheme.colorScheme.onSurfaceVariant,
+                maxLines = 2,
+                overflow = TextOverflow.Ellipsis
+            )
+
+            Spacer(modifier = Modifier.height(12.dp))
+
+            Row(
+                modifier = Modifier.fillMaxWidth(),
+                horizontalArrangement = Arrangement.SpaceBetween
+            ) {
+                Row(verticalAlignment = Alignment.CenterVertically) {
+                    Icon(
+                        imageVector = Icons.Filled.AccessTime,
+                        contentDescription = "时长",
+                        modifier = Modifier.size(16.dp)
+                    )
+                    Text(
+                        text = "${challenge.actionDays} 天",
+                        style = MaterialTheme.typography.labelSmall,
+                        modifier = Modifier.padding(start = 4.dp)
+                    )
+                }
+
+                // 进度指示器或其他操作按钮
+                Button(
+                    onClick = { /* 处理挑战项点击 */ },
+                    modifier = Modifier.size(40.dp, 40.dp),
+                    shape = CircleShape
+                ) {
+                    Icon(Icons.Filled.PlayArrow, contentDescription = "开始挑战")
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun ErrorView(message: String, onRetry: () -> Unit) {
+    Column(
+        modifier = Modifier.fillMaxSize(),
+        verticalArrangement = Arrangement.Center,
+        horizontalAlignment = Alignment.CenterHorizontally
+    ) {
+        Text(
+            text = "出错了",
+            style = MaterialTheme.typography.headlineMedium,
+            color = MaterialTheme.colorScheme.error
+        )
+        Spacer(modifier = Modifier.height(16.dp))
+        Text(text = message)
+        Spacer(modifier = Modifier.height(24.dp))
+        Button(onClick = onRetry) {
+            Text("重试")
+        }
+    }
+}
 
 @Composable
-fun ChallengeListScreen(modifier: Modifier = Modifier) {
-    Box(
+fun EmptyView() {
+    Column(
         modifier = Modifier.fillMaxSize(),
-        contentAlignment = Alignment.Center
+        verticalArrangement = Arrangement.Center,
+        horizontalAlignment = Alignment.CenterHorizontally
     ) {
-        Text("Challenge Screen")
+        Icon(
+            imageVector = Icons.AutoMirrored.Filled.List,
+            contentDescription = "空列表",
+            modifier = Modifier.size(64.dp),
+            tint = MaterialTheme.colorScheme.outline
+        )
+        Spacer(modifier = Modifier.height(16.dp))
+        Text(
+            text = "暂无挑战",
+            style = MaterialTheme.typography.headlineMedium,
+            color = MaterialTheme.colorScheme.outline
+        )
     }
 }

+ 63 - 0
app/src/main/java/com/lostc/japp/ui/features/challenge/ChallengeViewModel.kt

@@ -0,0 +1,63 @@
+package com.lostc.japp.ui.features.challenge
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.lostc.japp.data.model.Challenge
+import com.lostc.japp.data.repository.ChallengeRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+// ChallengeViewModel.kt
+class ChallengeViewModel(private val repository: ChallengeRepository) : ViewModel() {
+
+    // UI状态
+    data class UiState(
+        val challenges: List<Challenge> = emptyList(),
+        val isLoading: Boolean = false,
+        val error: String? = null
+    )
+
+    private val _uiState = MutableStateFlow(UiState())
+    val uiState = _uiState.asStateFlow()
+
+    init {
+        loadChallenges()
+    }
+
+    private fun loadChallenges() {
+        viewModelScope.launch {
+            _uiState.update { it.copy(isLoading = true) }
+
+            try {
+                repository.allChallenges().collect { challenges ->
+                    _uiState.update { it.copy(challenges = challenges, isLoading = false) }
+                }
+            } catch (e: Exception) {
+                _uiState.update {
+                    it.copy(
+                        isLoading = false,
+                        error = e.message ?: "加载挑战失败"
+                    )
+                }
+            }
+        }
+    }
+
+    fun refreshChallenges() {
+        loadChallenges()
+    }
+}
+
+// ViewModel工厂
+class ChallengeViewModelFactory(private val repository: ChallengeRepository) : ViewModelProvider.Factory {
+    override fun <T : ViewModel> create(modelClass: Class<T>): T {
+        if (modelClass.isAssignableFrom(ChallengeViewModel::class.java)) {
+            @Suppress("UNCHECKED_CAST")
+            return ChallengeViewModel(repository) as T
+        }
+        throw IllegalArgumentException("Unknown ViewModel class")
+    }
+}

+ 1 - 0
build.gradle.kts

@@ -3,4 +3,5 @@ plugins {
     alias(libs.plugins.android.application) apply false
     alias(libs.plugins.kotlin.android) apply false
     alias(libs.plugins.kotlin.compose) apply false
+    alias(libs.plugins.kotlin.ksp) apply false
 }

+ 9 - 1
gradle/libs.versions.toml

@@ -10,6 +10,8 @@ activityCompose = "1.8.0"
 composeBom = "2024.04.01"
 navigationComposeJvmstubs = "2.9.0"
 navigationComposeAndroid = "2.9.0"
+room_version = "2.7.2"
+ksp_version = "2.0.21-1.0.27"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -28,10 +30,16 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
 androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
 androidx-navigation-compose-jvmstubs = { group = "androidx.navigation", name = "navigation-compose-jvmstubs", version.ref = "navigationComposeJvmstubs" }
 androidx-navigation-compose-android = { group = "androidx.navigation", name = "navigation-compose-android", version.ref = "navigationComposeAndroid" }
-androidx-material-icons-extended = {  group = "androidx.compose.material", name = "material-icons-extended"}
+androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
+androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room_version" }
+androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room_version" }
+androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room_version" }
+androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room_version" }
+androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "room_version" }
 
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }
 kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
 kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp_version" }