Wednesday, June 10, 2026

PPB Pertemuan 13 - Aplikasi CRUD Siswa

Penerapan Room Database dan Jetpack Compose pada Aplikasi "CRUDSiswa"

Muhammad Risyad Himawan Putra - 5025231205

 

Aplikasi ini berfungsi sebagai manajemen data siswa yang mengimplementasikan penyimpanan lokal secara permanen menggunakan Room Database. Komponen UI berinteraksi secara asinkron langsung dengan DAO menggunakan Kotlin Coroutines, memastikan aplikasi berjalan cepat, ringan, dan mudah dipelihara.


Source Code: https://github.com/MRHP1/Mobile-Programming/tree/main/CRUDSiswa

 

1. Deskripsi Aplikasi

Aplikasi ini dirancang menggunakan Jetpack Compose. Fitur dan implementasi utamanya meliputi:

  • Room Database & SQLite: Menyimpan data siswa (nama dan email) secara lokal menggunakan abstraction layer Room yang aman dan efisien di atas SQLite.
  • Kotlin Coroutines: Menjalankan operasi database (baca, tambah, ubah, dan hapus) secara asinkron menggunakan Coroutine Scope (seperti rememberCoroutineScope) agar UI tetap lancar tanpa memblokir Main Thread.
  • Akses DAO Langsung (Tanpa ViewModel): Penyederhanaan arsitektur untuk data binding, di mana UI Compose mengamati perubahan data secara reaktif dari DAO melalui Flow Kotlin yang dicollect sebagai Compose State.

2. Color.kt

// Dark theme palette val DarkBackground = Color(0xFF121212) val DarkSurface = Color(0xFF1E1E2E) val DarkCard = Color(0xFF2A2A3C) val AccentPurple = Color(0xFF7C4DFF) val AccentPurpleLight = Color(0xFFB388FF) val AccentRed = Color(0xFFFF5252) val AccentGreen = Color(0xFF69F0AE) val TextWhite = Color(0xFFF5F5F5) val TextGrey = Color(0xFFB0B0C0) val InputBorder = Color(0xFF3A3A4C) // Avatar colors val AvatarBlue = Color(0xFF42A5F5) val AvatarGreen = Color(0xFF66BB6A) val AvatarOrange = Color(0xFFFFA726) val AvatarPink = Color(0xFFEC407A) val AvatarTeal = Color(0xFF26A69A) val AvatarIndigo = Color(0xFF5C6BC0)

Penjelasan: Berisi pendefinisian warna untuk tema gelap modern aplikasi.


3. Theme.kt

private val DarkColorScheme = darkColorScheme( primary = AccentPurple, onPrimary = Color.White, primaryContainer = AccentPurple, onPrimaryContainer = Color.White, secondary = AccentPurpleLight, onSecondary = Color.Black, background = DarkBackground, onBackground = TextWhite, surface = DarkSurface, onSurface = TextWhite, surfaceVariant = DarkCard, onSurfaceVariant = TextGrey, outline = InputBorder, error = AccentRed, onError = Color.White ) @Composable fun CRUDSiswaTheme( content: @Composable () -> Unit ) { MaterialTheme( colorScheme = DarkColorScheme, typography = Typography, content = content ) }

Penjelasan: Sebagai pemetaan tema aplikasi dan warna aplikasi.


4. Siswa.kt

@Entity(tableName = "siswa") data class Siswa( @PrimaryKey(autoGenerate = true) val id: Int = 0, val nama: String, val email: String )

Penjelasan: Entitas model data Room Siswa didefinisikan dengan tabel bernama siswa. Atribut yang disimpan disederhanakan hanya mencakup kolom nama dan email berformat teks, serta id auto-increment sebagai Primary Key unik.


5. SiswaDao.kt

@Dao interface SiswaDao { @Query("SELECT * FROM siswa ORDER BY id DESC") fun getAllSiswa(): Flow<List<Siswa>> @Insert suspend fun insertSiswa(siswa: Siswa) @Update suspend fun updateSiswa(siswa: Siswa) @Delete suspend fun deleteSiswa(siswa: Siswa) }

Penjelasan: Interface SiswaDao mendefinisikan seluruh operasi interaksi ke database. Fungsi query getAllSiswa mengembalikan aliran data bertipe Flow<List<Siswa>> dari Kotlin Coroutines, yang memancarkan data terbaru secara reaktif setiap kali terjadi perubahan data pada tabel. Fungsi penulisan data (insertSiswa, updateSiswa, deleteSiswa) dideklarasikan sebagai suspend fun agar aman dieksekusi di background thread.


6. AppDatabase.kt

@Database( entities = [Siswa::class], version = 2, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { abstract fun siswaDao(): SiswaDao 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, "siswa_db" ) .fallbackToDestructiveMigration(true) .build() INSTANCE = instance instance } } } }

Penjelasan: SQLite lokal dikelola oleh class singleton AppDatabase. Karena struktur entitas disederhanakan (pengurangan field dari skema sebelumnya), versi database ditingkatkan menjadi version = 2 dengan opsi .fallbackToDestructiveMigration(true). Hal ini mengizinkan Room untuk secara otomatis membersihkan tabel usang dan merekonstruksi database dengan versi baru tanpa memicu crash migrasi.


7. MainScreen.kt

package com.example.crudsiswa.ui import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.example.crudsiswa.data.Siswa import com.example.crudsiswa.data.SiswaDao import com.example.crudsiswa.ui.theme.* import kotlinx.coroutines.launch @Composable fun MainScreen(dao: SiswaDao) { val siswaList by dao.getAllSiswa().collectAsState(initial = emptyList()) val scope = rememberCoroutineScope() var nama by remember { mutableStateOf("") } var email by remember { mutableStateOf("") } var editingSiswa by remember { mutableStateOf<Siswa?>(null) } val isEditing = editingSiswa != null val textFieldColors = OutlinedTextFieldDefaults.colors( focusedBorderColor = AccentPurple, unfocusedBorderColor = InputBorder, focusedLabelColor = AccentPurple, unfocusedLabelColor = TextGrey, cursorColor = AccentPurple, focusedTextColor = TextWhite, unfocusedTextColor = TextWhite, focusedContainerColor = DarkCard, unfocusedContainerColor = DarkCard ) Scaffold( containerColor = MaterialTheme.colorScheme.background ) { innerPadding -> LazyColumn( modifier = Modifier .fillMaxSize() .padding(innerPadding) .padding(horizontal = 20.dp), verticalArrangement = Arrangement.spacedBy(0.dp) ) { // Header item { Spacer(modifier = Modifier.height(48.dp)) Text( text = "Registrasi Siswa", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = TextWhite ) Text( text = "Kelola data siswa", style = MaterialTheme.typography.bodyMedium, color = TextGrey ) Spacer(modifier = Modifier.height(24.dp)) } // Form Fields item { OutlinedTextField( value = nama, onValueChange = { nama = it }, label = { Text("Nama") }, singleLine = true, shape = RoundedCornerShape(14.dp), colors = textFieldColors, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(12.dp)) } item { OutlinedTextField( value = email, onValueChange = { email = it }, label = { Text("Email") }, singleLine = true, shape = RoundedCornerShape(14.dp), colors = textFieldColors, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) } // Add / Update Button item { Button( onClick = { if (nama.isNotBlank() && email.isNotBlank()) { scope.launch { if (isEditing) { editingSiswa?.let { current -> dao.updateSiswa( current.copy(nama = nama, email = email) ) } editingSiswa = null } else { dao.insertSiswa(Siswa(nama = nama, email = email)) } nama = "" email = "" } } }, modifier = Modifier .fillMaxWidth() .height(52.dp), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors( containerColor = AccentPurple ), enabled = nama.isNotBlank() && email.isNotBlank() ) { Icon( imageVector = Icons.Default.Add, contentDescription = null, modifier = Modifier.padding(end = 8.dp) ) Text( text = if (isEditing) "Update Siswa" else "Tambah Siswa", fontWeight = FontWeight.SemiBold ) } Spacer(modifier = Modifier.height(28.dp)) } // Section Title item { Text( text = "Daftar Siswa", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = TextWhite ) Spacer(modifier = Modifier.height(12.dp)) } // Student List if (siswaList.isEmpty()) { item { Text( text = "Belum ada data siswa.", style = MaterialTheme.typography.bodyMedium, color = TextGrey, modifier = Modifier.padding(vertical = 16.dp) ) } } else { items(siswaList, key = { it.id }) { siswa -> StudentItem( siswa = siswa, onEdit = { editingSiswa = siswa nama = siswa.nama email = siswa.email }, onDelete = { scope.launch { dao.deleteSiswa(siswa) } } ) Spacer(modifier = Modifier.height(10.dp)) } } // Bottom Spacing item { Spacer(modifier = Modifier.height(32.dp)) } } } }

Penjelasan: MainScreen mengelola layout utama secara inline. Input form ditaruh langsung di bagian atas layar agar pengisian data lebih cepat, diikuti dengan daftar siswa yang reaktif. Menggunakan rememberCoroutineScope() untuk membuat scope asinkron langsung dari interface UI Compose, sehingga kita dapat melakukan update data via DAO secara langsung saat tombol diklik tanpa memerlukan ViewModel.


8. StudentItem.kt

package com.example.crudsiswa.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.crudsiswa.data.Siswa import com.example.crudsiswa.ui.theme.* private val avatarColors = listOf( AvatarBlue, AvatarGreen, AvatarOrange, AvatarPink, AvatarTeal, AvatarIndigo ) private fun getInitials(nama: String): String { val words = nama.trim().split("\\s+".toRegex()) return when { words.size >= 2 -> "${words[0].first().uppercaseChar()}${words[1].first().uppercaseChar()}" words.isNotEmpty() && words[0].isNotEmpty() -> "${words[0].first().uppercaseChar()}" else -> "?" } } @Composable fun StudentItem( siswa: Siswa, onEdit: () -> Unit, onDelete: () -> Unit, modifier: Modifier = Modifier ) { val colorIndex = (siswa.id % avatarColors.size) val avatarColor = avatarColors[colorIndex] val initials = getInitials(siswa.nama) Card( modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( containerColor = DarkCard ), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { // Avatar circle with initials Box( modifier = Modifier .size(48.dp) .clip(CircleShape) .background(avatarColor), contentAlignment = Alignment.Center ) { Text( text = initials, color = TextWhite, fontSize = 16.sp, fontWeight = FontWeight.Bold ) } // Name and email Column( modifier = Modifier .weight(1f) .padding(horizontal = 14.dp), verticalArrangement = Arrangement.spacedBy(2.dp) ) { Text( text = siswa.nama, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold, color = TextWhite ) Text( text = siswa.email, style = MaterialTheme.typography.bodyMedium, color = TextGrey ) } // Edit & Delete buttons Row( horizontalArrangement = Arrangement.spacedBy(0.dp) ) { IconButton(onClick = onEdit) { Icon( imageVector = Icons.Default.Edit, contentDescription = "Edit", tint = AccentPurple, modifier = Modifier.size(20.dp) ) } IconButton(onClick = onDelete) { Icon( imageVector = Icons.Default.Delete, contentDescription = "Hapus", tint = AccentRed, modifier = Modifier.size(20.dp) ) } } } } }

Penjelasan: Komponen card per siswa (StudentItem). Menampilkan inisial avatar yang dibuat otomatis via fungsi helper getInitials. Warna avatar dipilih secara deterministik dari list warna berdasarkan ID siswa agar warna yang dihasilkan tidak berubah-ubah namun bervariasi antar baris kartu.


9. MainActivity.kt

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() val dao = AppDatabase.getDatabase(this).siswaDao() setContent { CRUDSiswaTheme { MainScreen(dao = dao) } } } }

Penjelasan: MainActivity bertindak sebagai main entry. Pada siklus onCreate, database Room diinisialisasi untuk mengambil interface DAO-nya (SiswaDao). Komponen DAO ini langsung dilewatkan (di-dependency inject-kan secara langsung) ke fungsi reaktif MainScreen(dao = dao) tanpa menggunakan perantara ViewModel, menyederhanakan siklus pertukaran data untuk kasus penggunaan CRUD sederhana.


Tampilan Aplikasi

No comments:

Post a Comment

PPB EAS - Aplikasi Member Timezone

Manajemen Keanggotaan & Loyalty Program Timezone dengan Room Database dan Jetpack Compose Aryaka Leorgi Epridaka - 5025231117 Muhammad ...