From 01e5f14356b4af3ee3c39d0ba8777c0a28f6800b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:55:12 +0000 Subject: [PATCH 1/2] Initial plan From 6d58e46cfdfe0d2dedd669c3d06c83d829209119 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:01:02 +0000 Subject: [PATCH 2/2] feat: Replace in-memory TaskRepositoryImpl with Room database persistence Co-authored-by: jsb-nci <212698004+jsb-nci@users.noreply.github.com> --- app/build.gradle.kts | 4 ++ .../com/example/composebasics/MainActivity.kt | 13 +++++-- .../composebasics/data/local/TaskDao.kt | 26 +++++++++++++ .../composebasics/data/local/TaskDatabase.kt | 28 ++++++++++++++ .../composebasics/data/local/TaskEntity.kt | 11 ++++++ .../data/repository/RoomTaskRepositoryImpl.kt | 38 +++++++++++++++++++ .../data/repository/TaskRepository.kt | 11 +++--- .../data/repository/TaskRepositoryImpl.kt | 10 ++--- .../composebasics/ui/home/HomeScreen.kt | 5 ++- .../composebasics/ui/home/HomeViewModel.kt | 33 ++++++++-------- .../ui/home/HomeViewModelFactory.kt | 15 ++++++++ gradle/libs.versions.toml | 6 +++ 12 files changed, 169 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/example/composebasics/data/local/TaskDao.kt create mode 100644 app/src/main/java/com/example/composebasics/data/local/TaskDatabase.kt create mode 100644 app/src/main/java/com/example/composebasics/data/local/TaskEntity.kt create mode 100644 app/src/main/java/com/example/composebasics/data/repository/RoomTaskRepositoryImpl.kt create mode 100644 app/src/main/java/com/example/composebasics/ui/home/HomeViewModelFactory.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c2f22f6..41a2c27 100644 --- a/app/build.gradle.kts +++ b/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.ksp) } android { @@ -51,6 +52,9 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0") + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/com/example/composebasics/MainActivity.kt b/app/src/main/java/com/example/composebasics/MainActivity.kt index 7d35282..9c7ea4b 100644 --- a/app/src/main/java/com/example/composebasics/MainActivity.kt +++ b/app/src/main/java/com/example/composebasics/MainActivity.kt @@ -6,14 +6,12 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -23,17 +21,24 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.example.composebasics.data.local.TaskDatabase +import com.example.composebasics.data.repository.RoomTaskRepositoryImpl import com.example.composebasics.ui.home.HomeScreen +import com.example.composebasics.ui.home.HomeViewModelFactory import com.example.composebasics.ui.theme.ComposeBasicsTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + + val database = TaskDatabase.getDatabase(this) + val repository = RoomTaskRepositoryImpl(database.taskDao()) + val factory = HomeViewModelFactory(repository) + setContent { ComposeBasicsTheme { - - HomeScreen() + HomeScreen(viewModelFactory = factory) } } } diff --git a/app/src/main/java/com/example/composebasics/data/local/TaskDao.kt b/app/src/main/java/com/example/composebasics/data/local/TaskDao.kt new file mode 100644 index 0000000..5d373d9 --- /dev/null +++ b/app/src/main/java/com/example/composebasics/data/local/TaskDao.kt @@ -0,0 +1,26 @@ +package com.example.composebasics.data.local + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +@Dao +interface TaskDao { + @Query("SELECT * FROM tasks ORDER BY rowid ASC") + fun getAllTasks(): Flow> + + @Query("SELECT * FROM tasks WHERE id = :taskId") + suspend fun getTaskById(taskId: String): TaskEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertTask(task: TaskEntity) + + @Update + suspend fun updateTask(task: TaskEntity) + + @Query("DELETE FROM tasks WHERE id = :taskId") + suspend fun deleteTask(taskId: String) +} diff --git a/app/src/main/java/com/example/composebasics/data/local/TaskDatabase.kt b/app/src/main/java/com/example/composebasics/data/local/TaskDatabase.kt new file mode 100644 index 0000000..d6f322e --- /dev/null +++ b/app/src/main/java/com/example/composebasics/data/local/TaskDatabase.kt @@ -0,0 +1,28 @@ +package com.example.composebasics.data.local + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [TaskEntity::class], version = 1, exportSchema = false) +abstract class TaskDatabase : RoomDatabase() { + abstract fun taskDao(): TaskDao + + companion object { + @Volatile + private var INSTANCE: TaskDatabase? = null + + fun getDatabase(context: Context): TaskDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + TaskDatabase::class.java, + "task_database" + ).build() + INSTANCE = instance + instance + } + } + } +} diff --git a/app/src/main/java/com/example/composebasics/data/local/TaskEntity.kt b/app/src/main/java/com/example/composebasics/data/local/TaskEntity.kt new file mode 100644 index 0000000..0f1f48f --- /dev/null +++ b/app/src/main/java/com/example/composebasics/data/local/TaskEntity.kt @@ -0,0 +1,11 @@ +package com.example.composebasics.data.local + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "tasks") +data class TaskEntity( + @PrimaryKey val id: String, + val title: String, + val isCompleted: Boolean = false +) diff --git a/app/src/main/java/com/example/composebasics/data/repository/RoomTaskRepositoryImpl.kt b/app/src/main/java/com/example/composebasics/data/repository/RoomTaskRepositoryImpl.kt new file mode 100644 index 0000000..d333e91 --- /dev/null +++ b/app/src/main/java/com/example/composebasics/data/repository/RoomTaskRepositoryImpl.kt @@ -0,0 +1,38 @@ +package com.example.composebasics.data.repository + +import com.example.composebasics.data.local.TaskDao +import com.example.composebasics.data.local.TaskEntity +import com.example.composebasics.domain.model.Task +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.util.UUID + +class RoomTaskRepositoryImpl(private val dao: TaskDao) : TaskRepository { + + override fun getTasks(): Flow> { + return dao.getAllTasks().map { entities -> + entities.map { it.toTask() } + } + } + + override suspend fun addTask(title: String) { + val entity = TaskEntity( + id = UUID.randomUUID().toString(), + title = title, + isCompleted = false + ) + dao.insertTask(entity) + } + + override suspend fun toggleTask(taskId: String) { + val entity = dao.getTaskById(taskId) ?: return + dao.updateTask(entity.copy(isCompleted = !entity.isCompleted)) + } + + override suspend fun deleteTask(taskId: String) { + dao.deleteTask(taskId) + } +} + +fun TaskEntity.toTask() = Task(id = id, title = title, isCompleted = isCompleted) +fun Task.toEntity() = TaskEntity(id = id, title = title, isCompleted = isCompleted) diff --git a/app/src/main/java/com/example/composebasics/data/repository/TaskRepository.kt b/app/src/main/java/com/example/composebasics/data/repository/TaskRepository.kt index c88d4e6..2f08b54 100644 --- a/app/src/main/java/com/example/composebasics/data/repository/TaskRepository.kt +++ b/app/src/main/java/com/example/composebasics/data/repository/TaskRepository.kt @@ -1,10 +1,11 @@ package com.example.composebasics.data.repository import com.example.composebasics.domain.model.Task +import kotlinx.coroutines.flow.Flow interface TaskRepository { - fun getTasks(): List - fun addTask(title: String): List - fun toggleTask(taskId: String): List - fun deleteTask(taskId: String): List -} \ No newline at end of file + fun getTasks(): Flow> + suspend fun addTask(title: String) + suspend fun toggleTask(taskId: String) + suspend fun deleteTask(taskId: String) +} diff --git a/app/src/main/java/com/example/composebasics/data/repository/TaskRepositoryImpl.kt b/app/src/main/java/com/example/composebasics/data/repository/TaskRepositoryImpl.kt index 7c7cf73..84788a7 100644 --- a/app/src/main/java/com/example/composebasics/data/repository/TaskRepositoryImpl.kt +++ b/app/src/main/java/com/example/composebasics/data/repository/TaskRepositoryImpl.kt @@ -3,14 +3,14 @@ package com.example.composebasics.data.repository import com.example.composebasics.domain.model.Task import java.util.UUID -class TaskRepositoryImpl: TaskRepository{ +class TaskRepositoryImpl { private val tasks = mutableListOf() - override fun getTasks(): List { + fun getTasks(): List { return tasks.toList() } - override fun addTask(title: String): List { + fun addTask(title: String): List { val newTask = Task( id = UUID.randomUUID().toString(), title = title, @@ -20,7 +20,7 @@ class TaskRepositoryImpl: TaskRepository{ return tasks.toList() } - override fun toggleTask(taskId: String): List { + fun toggleTask(taskId: String): List { val updatedTasks = tasks.map { task -> if (task.id == taskId) { task.copy(isCompleted = !task.isCompleted) @@ -35,7 +35,7 @@ class TaskRepositoryImpl: TaskRepository{ return tasks.toList() } - override fun deleteTask(taskId: String): List { + fun deleteTask(taskId: String): List { tasks.removeAll { it.id == taskId } return tasks.toList() } diff --git a/app/src/main/java/com/example/composebasics/ui/home/HomeScreen.kt b/app/src/main/java/com/example/composebasics/ui/home/HomeScreen.kt index 4e47cab..5e287fc 100644 --- a/app/src/main/java/com/example/composebasics/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/example/composebasics/ui/home/HomeScreen.kt @@ -43,12 +43,13 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel @Composable -@Preview fun HomeScreen( - viewModel: HomeViewModel = viewModel() + viewModelFactory: ViewModelProvider.Factory? = null, + viewModel: HomeViewModel = viewModel(factory = viewModelFactory) ) { val state by viewModel.uiState.collectAsState() var title by remember { mutableStateOf("") } diff --git a/app/src/main/java/com/example/composebasics/ui/home/HomeViewModel.kt b/app/src/main/java/com/example/composebasics/ui/home/HomeViewModel.kt index 76f6907..3c588e8 100644 --- a/app/src/main/java/com/example/composebasics/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/example/composebasics/ui/home/HomeViewModel.kt @@ -1,36 +1,41 @@ package com.example.composebasics.ui.home import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.example.composebasics.data.repository.TaskRepository -import com.example.composebasics.data.repository.TaskRepositoryImpl import com.example.composebasics.domain.model.Task import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import java.util.UUID +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch -class HomeViewModel: ViewModel() { +class HomeViewModel(private val repository: TaskRepository) : ViewModel() { - private val repository: TaskRepository = TaskRepositoryImpl() - private val _uiState = MutableStateFlow(HomeUIState(task = repository.getTasks())) + private val _uiState = MutableStateFlow(HomeUIState()) val uiState: StateFlow = _uiState + init { + viewModelScope.launch { + repository.getTasks().collect { tasks -> + _uiState.update { it.copy(task = tasks) } + } + } + } + fun addTask(title: String) { - val updatedTasks = repository.addTask(title) - _uiState.value = _uiState.value.copy(task = updatedTasks) + viewModelScope.launch { repository.addTask(title) } } fun toggleTask(taskId: String) { - val updatedTasks = repository.toggleTask(taskId) - _uiState.value = _uiState.value.copy(task = updatedTasks) + viewModelScope.launch { repository.toggleTask(taskId) } } fun deleteTask(taskId: String) { - val updatedTasks = repository.deleteTask(taskId) - _uiState.value = _uiState.value.copy(task = updatedTasks) + viewModelScope.launch { repository.deleteTask(taskId) } } fun setFilter(filter: TaskFilter) { - _uiState.value = _uiState.value.copy(filter = filter) + _uiState.update { it.copy(filter = filter) } } fun getVisibleTasks(state: HomeUIState): List { @@ -38,8 +43,6 @@ class HomeViewModel: ViewModel() { TaskFilter.ALL -> state.task TaskFilter.ACTIVE -> state.task.filter { !it.isCompleted } TaskFilter.COMPLETED -> state.task.filter { it.isCompleted } - } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/composebasics/ui/home/HomeViewModelFactory.kt b/app/src/main/java/com/example/composebasics/ui/home/HomeViewModelFactory.kt new file mode 100644 index 0000000..8d36746 --- /dev/null +++ b/app/src/main/java/com/example/composebasics/ui/home/HomeViewModelFactory.kt @@ -0,0 +1,15 @@ +package com.example.composebasics.ui.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.composebasics.data.repository.TaskRepository + +class HomeViewModelFactory(private val repository: TaskRepository) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(HomeViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return HomeViewModel(repository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4623cb6..3649991 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,8 @@ [versions] agp = "8.13.2" kotlin = "2.0.21" +ksp = "2.0.21-1.0.28" +room = "2.7.1" coreKtx = "1.10.1" junit = "4.13.2" junitVersion = "1.1.5" @@ -24,9 +26,13 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } [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" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }