/** * RoomStudentApp — Student Registry with Room + ViewModel + Compose. * Shows: Entity, DAO, Database, Repository, ViewModel, StateFlow, LazyColumn, Form. * * To run inside a real Android Studio project, add: * plugins { id("com.google.devtools.ksp") } * implementation("androidx.room:room-runtime:2.6.1") * implementation("androidx.room:room-ktx:2.6.1") * ksp("androidx.room:room-compiler:2.6.1") * implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") */ import android.content.Context import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import androidx.room.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch // ----- Data Layer ----- @Entity(tableName = "students") data class Student( @PrimaryKey(autoGenerate = true) val id: Int = 0, val name: String, val course: String, val year: Int ) @Dao interface StudentDao { @Insert suspend fun insert(student: Student) @Delete suspend fun delete(student: Student) @Query("SELECT * FROM students ORDER BY name ASC") fun all(): Flow> } @Database(entities = [Student::class], version = 1, exportSchema = false) abstract class AppDb : RoomDatabase() { abstract fun dao(): StudentDao companion object { @Volatile private var I: AppDb? = null fun get(ctx: Context): AppDb = I ?: synchronized(this) { Room.databaseBuilder(ctx, AppDb::class.java, "app.db").build().also { I = it } } } } class StudentRepository(private val dao: StudentDao) { val all: Flow> = dao.all() suspend fun add(s: Student) = dao.insert(s) suspend fun remove(s: Student) = dao.delete(s) } // ----- ViewModel ----- class StudentViewModel(private val repo: StudentRepository) : ViewModel() { val students: StateFlow> = repo.all .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) fun add(name: String, course: String, year: Int) = viewModelScope.launch { repo.add(Student(name = name, course = course, year = year)) } fun remove(s: Student) = viewModelScope.launch { repo.remove(s) } class Factory(private val repo: StudentRepository) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = StudentViewModel(repo) as T } } // ----- UI ----- @OptIn(ExperimentalMaterial3Api::class) @Composable fun StudentRegistryScreen(vm: StudentViewModel) { val students by vm.students.collectAsState() var showDialog by remember { mutableStateOf(false) } Scaffold( topBar = { TopAppBar(title = { Text("Students") }) }, floatingActionButton = { FloatingActionButton(onClick = { showDialog = true }) { Icon(Icons.Default.Add, "Add student") } } ) { padding -> if (students.isEmpty()) { Box(Modifier.fillMaxSize().padding(padding), Alignment.Center) { Text("No students yet. Tap + to add one.") } } else { LazyColumn( modifier = Modifier.padding(padding), contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { items(students, key = { it.id }) { s -> StudentRow(s, onDelete = { vm.remove(s) }) } } } } if (showDialog) { AddStudentDialog( onDismiss = { showDialog = false }, onSave = { name, course, year -> vm.add(name, course, year) showDialog = false } ) } } @Composable fun StudentRow(student: Student, onDelete: () -> Unit) { ElevatedCard(modifier = Modifier.fillMaxWidth()) { Row( modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically ) { Column(Modifier.weight(1f)) { Text(student.name, fontWeight = FontWeight.Bold) Text("${student.course} • Year ${student.year}") } IconButton(onClick = onDelete) { Icon(Icons.Default.Delete, "Delete") } } } } @Composable fun AddStudentDialog(onDismiss: () -> Unit, onSave: (String, String, Int) -> Unit) { var name by remember { mutableStateOf("") } var course by remember { mutableStateOf("") } var year by remember { mutableStateOf("1") } val valid = name.isNotBlank() && course.isNotBlank() && year.toIntOrNull() in 1..4 AlertDialog( onDismissRequest = onDismiss, title = { Text("Add Student") }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("Name") }) OutlinedTextField(value = course, onValueChange = { course = it }, label = { Text("Course") }) OutlinedTextField(value = year, onValueChange = { year = it }, label = { Text("Year (1-4)") }) } }, confirmButton = { TextButton( onClick = { onSave(name.trim(), course.trim(), year.toInt()) }, enabled = valid ) { Text("Save") } }, dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } } ) }