跳到正文 Lab Week 7 Summary · Jimmy Huang's Blog
← Week 7 Android Jetpack: Room 学习 › C6001 › 2026 S1 › FIT5046 Mobile and distributed computing systems › Week 7 Android Jetpack: Room Lab Week 7 Summary Lab Week 7 Summary 本页目录· 40 条 Lab做完总结 ›
Overview 这周的主题是把 Android 本地数据库 Room 接到 Jetpack Compose 上,做一个可以新增、修改、删除 subject 的小型 CRUD app。重点不只是把资料存进去,而是把整条数据链串起来: Entity → DAO → Database → Repository → ViewModel → Compose UI。
另外,这周还要理解为什么 Room 的数据库操作不能直接跑在主线程、为什么 Room 常常和 Flow / suspend function 一起用,以及为什么 Compose 里的 UI 会随着数据库变化自动刷新。
Goal 会在 Android Studio 的 Compose 项目中接入 Room 与 KSP。
会自己写出 Entity、 DAO、 Database、 Repository、 ViewModel。
会把 Room 的 Flow 数据接到 Compose 的 LazyColumn。
会完成 subject 的新增、修改、删除,并验证数据确实写进本地数据库。
会理解 suspend、 Flow、 AndroidViewModel、 copy() 在这个实验里分别解决什么问题。
Task 0: 创建项目并完成 Room / KSP 环境配置 本页目录 AndroidRoom
。
配好 Room 运行库、Room KTX、Room Compiler,以及 KSP 插件。
这一步做完后,后面的 @Entity、 @Dao、 @Database 注解才能正常工作。
如果你项目模板自带的版本和下面不完全一样,优先保证 Kotlin 版本与 KSP 版本匹配 。
Step 0.1:创建 Android Studio 项目 建议用 Android Studio 新建一个 Empty Activity (Jetpack Compose 模板)项目。
先拿到一个最基础的 Compose 工程,后面直接在这个工程里加 Room。
项目可以正常打开,Gradle 初次同步完成,没有红色报错。
Step 0.2:修改 gradle/libs.versions.toml在原有内容基础上补充下面这些条目。下面是 建议写法 ;如果你用 Android Studio 的黄灯 quick-fix 自动生成 catalog 名称,也可以,只要后面的 build.gradle.kts 和这里保持一致即可。
room-runtime:Room 的核心运行库
room-ktx:让 Room 更容易和协程一起用
room-compiler:根据注解生成 Room 需要的代码
ksp:负责处理 Room 注解
compose-icons-extended:Part 3 用 Edit / Delete 图标时会用到
Room 不是只加一个依赖就够了。它既要运行时库,也要编译期生成代码。没有 KSP 和 compiler, @Entity、 @Dao 这些注解不会真正生成可用代码。
如果你的模板已经有 roomRuntime 或 ksp 相关条目,避免重复定义。
如果你的 Kotlin 版本不是 2.0.21,要改成与当前 Kotlin 版本匹配的 KSP 版本。
Step 0.3:修改项目级 build.gradle.kts打开项目级 build.gradle.kts,在 plugins {} 里补上 KSP 插件声明:
项目级先声明 KSP 插件,模块级才能真正启用它。
Step 0.4:修改模块级 app/build.gradle.kts下面只展示 需要新增或确认 的部分,模板原有内容保留,不要把整个文件全删掉重写。
implementation(libs.room.runtime):把 Room 本体加进来
implementation(libs.room.ktx):配合 Flow / suspend functions 使用
ksp(libs.room.compiler):告诉 KSP 在编译时为 Room 生成代码
implementation(libs.compose.icons.extended):让 Icons.Default.Edit / Delete 可用
Room 的很多东西不是你手写完就能跑,编译器会基于注解生成一部分底层代码,所以这里的 ksp(...) 不能漏。
Step 0.5:同步 Gradle 并检查依赖是否正常 完成上面修改后,点击 Android Studio 顶部的 Sync Now 。
Unresolved reference: room先看 libs.versions.toml 里的别名和 build.gradle.kts 调用是否一致。
Plugin [id: 'com.google.devtools.ksp'] was not found通常是 libs.versions.toml 的 [plugins] 没写对,或者项目级 build.gradle.kts 没加 alias(libs.plugins.ksp) apply false。
KSP 版本不匹配 检查 Kotlin 版本和 KSP 版本是否对应;这类问题最常见。
Task 1: 创建 Room 数据层
Step 1.1:确定文件结构 为了跟 Lab 一样简单直接,这周可以先把文件都放在同一个 package 下。后面如果你想拆成 data/、 ui/ 再整理也可以。
Step 1.2:创建 Entity —— Subject.kt这就是 Room 的实体类 Entity。它会被映射成数据库里的一张表,这里表名是 Subject。
新增 subject 时,你只需要传 name,不需要手动管理 ID。这样 UI 逻辑更简单。
Step 1.3:创建 DAO —— SubjectDAO.ktDAO(Data Access Object)就是数据库操作入口。你对 Subject 表做的 CRUD,都是从这里定义的。
Flow 适合“数据后续还会变化,UI 要持续观察”。
Step 1.4:创建 Database —— SubjectDatabase.kt声明这个数据库里有哪些 Entity
暴露 DAO
以 singleton 方式创建数据库实例
@Database(...):告诉 Room 这是数据库类
entities = [Subject::class]:这个数据库目前只有一张 Subject 表
version = 1:当前数据库版本
abstract fun subjectDAO():暴露 DAO
INSTANCE:保证整个 app 只创建一个数据库对象
context.applicationContext:避免 Activity context 泄漏
fallbackToDestructiveMigration(true):如果 schema 版本变化但你没写 migration,Room 会清掉旧表重建
数据库对象很重,不应该每用一次就 new 一次。用 singleton 是标准写法。
fallbackToDestructiveMigration(...) 很适合实验阶段,但它的含义是: 结构变了就可能清空旧数据 。
如果你后面改了 Entity 字段又没做 migration,看到数据消失是正常的。
文件不报错, Subject::class 和 SubjectDAO 都能识别。
Step 1.5:创建 Repository —— SubjectRepository.ktRepository 是数据库和 UI 之间的一层包装。它本质上没有发明新功能,而是把 DAO 包起来,让架构更清晰。
以后如果数据来源不只 Room,例如还要混合网络数据、缓存、或多个 DAO,Repository 会非常有用。
这周的 Lab 也明确要你知道它是“额外的 abstraction layer”。
allSubjects 能从 DAO 正常取得 Flow<List<Subject>>。
Step 1.6:创建 ViewModel —— SubjectViewModel.ktViewModel 负责把 Room 数据暴露给 UI,并且把数据库操作放进协程里执行。
为什么这里用 AndroidViewModel 而不是普通 ViewModel
因为这个 Repository 的构造函数需要 Application。
普通 ViewModel 默认不带 Application,而 AndroidViewModel 会带。
数据库读写属于 I/O 操作,不应该堵在主线程上。
这也是本周 Lab 特别强调的点: 不要 block UI thread 。
viewModelScope、 Dispatchers.IO、 Flow<List<Subject>> 都不报错。
Task 2: 实现基础版 Compose UI,并完成 CRUD 验证
Step 2.1:把 MainActivity.kt 改成基础版 Room UI TextField:输入 subject name
Add:新增记录
Update:更新当前选中的记录
Delete:删除当前选中的记录
LazyColumn:把 Room 里的所有 subject 显示出来
点击某一行:把它设为当前选中项,并把名字回填到输入框
collectAsState(initial = emptyList()):把 Room 的 Flow<List<Subject>> 转成 Compose 可观察状态
remember { mutableStateOf(...) }:保存当前输入框内容、当前选中项
copy(name = name):创建一个新的 Subject 对象用于更新
selectedSubject?.uid == subject.uid:高亮当前选中行
如果你只是偷偷改对象里的某个字段,UI 不一定会重组。
所以更新时用 copy() 生成一个新对象,是这周必须理解的点。
Step 2.2:运行 app 并测试 Add / Update / Delete 在输入框输入一个 subject,例如 FIT9131
点击 Add
再输入第二个 subject,例如 FIT9136
点击列表中任意一项,确认该行高亮,且名字回填到输入框
修改输入框内容后点击 Update
再选中某一项点击 Delete
新增后,列表立即多一条记录
更新后,列表对应项名称变化
删除后,对应项从列表消失
整个过程不需要手动刷新页面
Step 2.3:用 Database Inspector 验证数据确实写进 Room(推荐) 如果老师更在意“你真的写进数据库了吗”,这一步很值得做。
先让 app 保持运行
在 Android Studio 打开 App Inspection
进入 Database Inspector
选择当前运行中的 app process
找到 subject_database
展开 Subject 表并查看数据
不是只看 UI,而是直接看 Room 底层 SQLite 数据表。
Database Inspector 能更直接证明: 记录真的存在本地数据库里 。
你能在 Subject 表中看到新增后的记录,字段至少包含:
Database Inspector 通常需要 app 处于运行状态才能看到数据库内容。
Task 3: 完成 Part 3 的进阶版 UI(AlertDialog + 列表图标操作) 这部分对应原始 Lab 的 group problem-solving。
核心不是重写数据库层,而是在 同样的 Room / ViewModel 数据层 上换一种 UI 交互。
你要做到: 用 LazyColumn 显示 subject 列表
用 AlertDialog 输入 Add / Edit 的 subject name
用按钮触发 Add
用 Edit / Delete 图标触发修改和删除
UI 不需要和老师截图 100% 一样,但交互要求要对应上。
Step 3.1:新建 DialogSubjectScreen.kt新建 DialogSubjectScreen.kt:
这是 Lab Part 3 最关键的点: 同样的 Room 功能,可以换不同的 Compose UI 写法实现 。
Step 3.2:把 MainActivity.kt 切换到 Dialog 版本 如果你想运行 Part 3,就把 setContent 里的 SubjectScreen(...) 改成 SubjectDialogScreen(...)。
让 app 启动后显示的是 Part 3 版本,而不是基础版。
如果你想同时保留两个版本,最简单的做法就是先把另一个版本注释掉,按需要切换。
Step 3.3:运行并验证 Dialog 版本 点击 Add Subject
在弹出的 AlertDialog 中输入 subject name
点击 Save
在列表中点击某条记录右侧的 Edit 图标
修改名称并保存
点击 Delete 图标删除记录
Add 时会弹出对话框
Edit 时会弹出带初始值的对话框
Save 后列表立即刷新
Delete 后对应项立即消失
如果你没改数据层代码,但 Part 3 也能正常增删改,说明你的 Room + ViewModel 结构是对的,UI 只是换了一层壳。
Summary 这周真正学到的,不只是“把数据存到本地数据库”这么简单,而是学会了 Android 里一条很常见的本地数据链路:
Entity → DAO → Database → Repository → ViewModel → Compose UI
Room 负责把 Kotlin 对象映射成 SQLite 表
DAO 负责定义数据库操作
Repository 负责多加一层抽象,避免 UI 直接碰数据库
ViewModel 负责把数据暴露给 UI,并用协程发起数据库操作
Flow 负责把数据库变化持续推给 UI
Compose 负责把这些状态显示出来,并在数据变化时自动重组
基础版让你理解最直接的 CRUD;进阶版则说明: 同一套数据层可以支撑不同 UI 设计 。这正是这周 Lab 想让你真正掌握的主线。
本周必须理解的知识点
1. Entity / DAO / Database / Repository / ViewModel 各自负责什么 Entity:表结构,决定数据库里存什么字段
DAO:数据库操作入口,写 SQL 和增删改查方法
Database:Room 数据库配置与 DAO 提供者
Repository:额外抽象层,统一组织数据来源
ViewModel:把数据交给 UI,并处理协程与生命周期
2. 为什么数据库操作不能直接跑在主线程 如果你把 insert / update / delete 直接放在主线程跑,界面可能卡顿,甚至直接抛异常。
所以这里用 viewModelScope.launch(Dispatchers.IO) 来把数据库操作丢到背景线程。
3. suspend function 和 Flow 的区别
4. 为什么更新时要用 copy()selectedSubject!!.copy(name = newName) 会创建一个新对象,让 Compose 和 Room 都明确知道“这是一次真正的更新”。
关键概念对比
suspend function vs Flowsuspend function 是“一次调用,一次结果”;
在本实验里,写操作适合 suspend,读整个列表适合 Flow。
ViewModel vs AndroidViewModel普通 ViewModel 不自带 Application;
AndroidViewModel 会把 Application 带进来。
因为这里的 Repository 需要 Application 去拿数据库实例,所以 Lab 选择 AndroidViewModel。
DAO vs RepositoryRepository 是对 DAO 再包一层,方便以后扩展和维护。
小项目里你可能感觉“多此一举”,但这是 Android 常见架构习惯。
老师可能会问什么
为什么 Room 里读全部数据要用 Flow? 因为数据库内容可能随 insert / update / delete 一直变化。
用 Flow 后,UI 可以持续接收最新数据,不用手动刷新。
为什么这里还要加 Repository? 因为它让数据访问结构更清晰,也方便以后扩展成多数据来源。
为什么新增时不用传 uid? 因为 uid 被声明成 @PrimaryKey(autoGenerate = true),Room 会自动生成。
为什么 Update 要先选中某一条记录? 选中记录后,程序拿到的是带 uid 的完整对象,Room 才能更新正确的那一行。
为什么 Part 3 能换 UI 但数据库层不用改? 因为数据层已经通过 Repository + ViewModel 封装好了。
只要 UI 仍然调用同样的 ViewModel 方法,底层 Room 逻辑就可以保持不变。
提交前自查 Room 依赖和 KSP 已配置完成,项目能正常 Sync
Subject.kt、 SubjectDAO.kt、 SubjectDatabase.kt、 SubjectRepository.kt、 SubjectViewModel.kt 已创建
基础版 SubjectScreen 能完成 Add / Update / Delete
LazyColumn 能正确显示数据库中的所有 records
至少测试过一次更新与删除,不只是新增
最好补一张 Database Inspector 截图,证明数据确实写进 Room
如果老师要求做 Part 3,确认 AlertDialog 版本也能正常运行
gradle/libs.versions.toml[versions]
roomRuntime = "2.7.2"
ksp = "2.0.21-1.0.26"
[libraries]
room-runtime = {group = "androidx.room",name = "room-runtime",version.ref = "roomRuntime" }
room-ktx = {group = "androidx.room",name = "room-ktx",version.ref = "roomRuntime" }
room-compiler = {group = "androidx.room",name = "room-compiler",version.ref = "roomRuntime" }
compose-icons-extended = {group = "androidx.compose.material",name = "material-icons-extended" }
[plugins]
ksp = {id = "com.google.devtools.ksp",version.ref = "ksp" }com.example.androidroom
├─ Subject.kt
├─ SubjectDAO.kt
├─ SubjectDatabase.kt
├─ SubjectRepository.kt
├─ SubjectViewModel.kt
├─ MainActivity.kt
└─ DialogSubjectScreen.kt // Task 3 用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.ksp) apply false } plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.ksp) } dependencies { // 保留模板原有依赖,不要删 implementation(libs.room.runtime) implementation(libs.room.ktx) ksp(libs.room.compiler) // Part 3 的 Edit / Delete 图标会用到 implementation(libs.compose.icons.extended) } package com.example.androidroom package com.example.androidroom import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "Subject" ) data class Subject( @PrimaryKey(autoGenerate = true) val uid: Int = 0, val name: String ) package com.example.androidroom import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import androidx.room.Update import kotlinx.coroutines.flow.Flow @Dao interface SubjectDAO { @Query( "SELECT * FROM Subject" ) fun getAllSubjects(): Flow<List<Subject>> @Insert suspend fun insertSubject(subject: Subject) @Update suspend fun updateSubject(subject: Subject) @Delete suspend fun deleteSubject(subject: Subject) } package com.example.androidroom import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @Database( entities = [Subject::class], version = 1, exportSchema = false ) abstract class SubjectDatabase : RoomDatabase() { abstract fun subjectDAO(): SubjectDAO companion object { @Volatile private var INSTANCE: SubjectDatabase? = null fun getDatabase(context: Context): SubjectDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, SubjectDatabase::class.java, "subject_database" ) .fallbackToDestructiveMigration(true) .build() INSTANCE = instance instance } } } } package com.example.androidroom import android.app.Application import kotlinx.coroutines.flow.Flow class SubjectRepository(application: Application) { private val subjectDao: SubjectDAO = SubjectDatabase.getDatabase(application).subjectDAO() val allSubjects: Flow<List<Subject>> = subjectDao.getAllSubjects() suspend fun insert(subject: Subject) { subjectDao.insertSubject(subject) } suspend fun update(subject: Subject) { subjectDao.updateSubject(subject) } suspend fun delete(subject: Subject) { subjectDao.deleteSubject(subject) } } package com.example.androidroom import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch class SubjectViewModel(application: Application) : AndroidViewModel(application) { private val repository = SubjectRepository(application) val allSubjects: Flow<List<Subject>> = repository.allSubjects fun insertSubject(subject: Subject) = viewModelScope.launch(Dispatchers.IO) { repository.insert(subject) } fun updateSubject(subject: Subject) = viewModelScope.launch(Dispatchers.IO) { repository.update(subject) } fun deleteSubject(subject: Subject) = viewModelScope.launch(Dispatchers.IO) { repository.delete(subject) } } package com.example.androidroom import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.weight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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.unit.dp import androidx.compose.ui.unit.sp class MainActivity : ComponentActivity() { private val viewModel: SubjectViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { MaterialTheme { SubjectScreen(subjectViewModel = viewModel) } } } } @Composable fun SubjectScreen(subjectViewModel: SubjectViewModel) { val subjects by subjectViewModel.allSubjects.collectAsState(initial = emptyList()) var name by remember { mutableStateOf( "" ) } var selectedSubject by remember { mutableStateOf<Subject?>(null) } val context = LocalContext.current Column(modifier = Modifier.padding(16.dp)) { Spacer(modifier = Modifier.height(30.dp)) Text( text = "Database Operations" , modifier = Modifier.fillMaxWidth(), fontSize = 25.sp ) Spacer(modifier = Modifier.height(10.dp)) TextField( value = name, onValueChange = { name = it }, label = { Text( "Subject Name" ) }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(10.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Button( onClick = { if (name.isBlank()) { Toast.makeText( context, "Please enter a subject" , Toast.LENGTH_SHORT ).show() } else { subjectViewModel.insertSubject(Subject(name = name)) name = "" selectedSubject = null } }, modifier = Modifier.weight(1f) ) { Text( "Add" ) } Button( onClick = { if (selectedSubject == null) { Toast.makeText( context, "Please select a subject to update" , Toast.LENGTH_SHORT ).show() } else if (name.isBlank()) { Toast.makeText( context, "Please enter a subject" , Toast.LENGTH_SHORT ).show() } else { val updated = selectedSubject!!.copy(name = name) subjectViewModel.updateSubject(updated) name = "" selectedSubject = null } }, modifier = Modifier.weight(1f) ) { Text( "Update" ) } Button( onClick = { if (selectedSubject == null) { Toast.makeText( context, "Please select a subject to delete" , Toast.LENGTH_SHORT ).show() } else { subjectViewModel.deleteSubject(selectedSubject!!) name = "" selectedSubject = null } }, modifier = Modifier.weight(1f) ) { Text( "Delete" ) } } Spacer(modifier = Modifier.height(16.dp)) LazyColumn { items(items = subjects, key = { it.uid }) { subject -> Row( modifier = Modifier .fillMaxWidth() .background( if (selectedSubject?.uid == subject.uid) { Color.LightGray } else { Color.Transparent } ) .clickable { name = subject.name selectedSubject = subject } .padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = subject.name, modifier = Modifier.weight(1f) ) } HorizontalDivider(thickness = 2.dp) } } } } package com.example.androidroom import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.weight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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.unit.dp @Composable fun SubjectDialogScreen(subjectViewModel: SubjectViewModel) { val subjects by subjectViewModel.allSubjects.collectAsState(initial = emptyList()) var selectedSubject by remember { mutableStateOf<Subject?>(null) } var showDialog by remember { mutableStateOf(false) } var isEditMode by remember { mutableStateOf(false) } var dialogText by remember { mutableStateOf( "" ) } val context = LocalContext.current Column(modifier = Modifier.padding(16.dp)) { Spacer(modifier = Modifier.height(24.dp)) Button( onClick = { isEditMode = false dialogText = "" showDialog = true }, modifier = Modifier.fillMaxWidth() ) { Text( "Add Subject" ) } Spacer(modifier = Modifier.height(16.dp)) LazyColumn { items(items = subjects, key = { it.uid }) { subject -> Row( modifier = Modifier .fillMaxWidth() .background( if (selectedSubject?.uid == subject.uid) { Color.LightGray } else { Color.Transparent } ) .clickable { selectedSubject = subject } .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = subject.name, modifier = Modifier.weight(1f) ) IconButton( onClick = { selectedSubject = subject isEditMode = true dialogText = subject.name showDialog = true } ) { Icon( imageVector = Icons.Default.Edit, contentDescription = "Edit Subject" ) } IconButton( onClick = { selectedSubject = subject subjectViewModel.deleteSubject(subject) selectedSubject = null Toast.makeText( context, "Subject deleted" , Toast.LENGTH_SHORT ).show() } ) { Icon( imageVector = Icons.Default.Delete, contentDescription = "Delete Subject" ) } } HorizontalDivider() } } } if (showDialog) { SubjectInputDialog( title = if (isEditMode) "Edit Subject" else "Add Subject" , initialValue = dialogText, onDismiss = { showDialog = false dialogText = "" }, onSave = { newName -> if (newName.isBlank()) { Toast.makeText( context, "Please enter a subject" , Toast.LENGTH_SHORT ).show() } else { if (isEditMode) { val current = selectedSubject if (current != null) { subjectViewModel.updateSubject(current.copy(name = newName)) } } else { subjectViewModel.insertSubject(Subject(name = newName)) } selectedSubject = null showDialog = false dialogText = "" } } ) } } @Composable private fun SubjectInputDialog( title: String, initialValue: String, onDismiss: () -> Unit, onSave: (String) -> Unit ) { var text by remember(initialValue) { mutableStateOf(initialValue) } AlertDialog( onDismissRequest = onDismiss, title = { Text(text = title) }, text = { OutlinedTextField( value = text, onValueChange = { text = it }, label = { Text( "Subject Name" ) }, singleLine = true, modifier = Modifier.fillMaxWidth() ) }, confirmButton = { TextButton(onClick = { onSave(text) }) { Text( "Save" ) } }, dismissButton = { TextButton(onClick = onDismiss) { Text( "Cancel" ) } } ) } setContent { MaterialTheme { SubjectScreen(subjectViewModel = viewModel) } } setContent { MaterialTheme { SubjectDialogScreen(subjectViewModel = viewModel) } }