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