Pertemuan 13 - Registrasi Siswa

Nama: Kadek Fajar Pramartha Yasodana
NRP: 5025231185
Kelas: PPB (C)

Pertemuan 13 - Registrasi Siswa
Pada pertemuan kali ini kita diminta untuk membuat aplikasi CRUD mengenai pengelolaan data siswa. Aplikasi ini menggunakan Room Database dengan arsitektur View Model. Berikut adalah struktur dari project yang mengikuti arahan dari blog, ditambah dengan helper 


Siswa.kt
package com.fajary.registrasisiswa.data

import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey

@Entity()
data class Siswa(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val nama: String,
val email: String
)
Siswa.kt adalah file model data yang merepresentasikan tabel siswa di dalam database Room. Di dalamnya terdapat data class Siswa yang memiliki tiga properti utama yaitu id, nama, dan email. Field id dijadikan primary key dan diatur agar auto-generated sehingga nilainya dibuat otomatis oleh database setiap kali data baru ditambahkan. File ini menjadi representasi utama dari data siswa yang digunakan di seluruh aplikasi, baik di database maupun di tampilan UI.

SiswaDao.kt
package com.fajary.registrasisiswa.data

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query

@Dao
interface SiswaDao
{
@Query("INSERT INTO Siswa(nama, email) VALUES (:nama, :email)")
suspend fun insert(nama: String, email: String)

@Query(value = "SELECT * FROM Siswa")
suspend fun selectAll(): List<Siswa>
@Query(value = "SELECT * FROM Siswa WHERE id = :id")
suspend fun selectSiswa(id: Int): Siswa?

@Query(value = "DELETE FROM Siswa WHERE id = :id")
suspend fun deleteSiswa(id: Int)
@Query(value = "UPDATE Siswa SET nama = :nama, email = :email WHERE id = :id")
suspend fun updateSiswa(id: Int, nama: String, email: String)
}
SiswaDao.kt merupakan Data Access Object (DAO) yang berfungsi sebagai jembatan antara aplikasi dan database. File ini berisi berbagai query SQL untuk melakukan operasi CRUD seperti insert, select semua data, select berdasarkan id, update, dan delete. Semua fungsi dibuat suspend agar bisa dijalankan secara asynchronous menggunakan coroutine sehingga tidak mengganggu thread utama aplikasi.

AppDatabase.kt
package com.fajary.registrasisiswa.data

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(
entities = [Siswa::class],
version = 1
)
abstract class AppDatabase : RoomDatabase()
{
abstract fun siswaDao() : SiswaDao
}
AppDatabase.kt adalah file konfigurasi database Room yang mendefinisikan struktur database aplikasi. Di sini ditentukan bahwa entitas yang digunakan adalah Siswa, serta versi database yang saat ini bernilai 1. File ini juga menyediakan fungsi abstrak siswaDao() yang digunakan untuk mengakses DAO dari database sehingga ViewModel dapat berinteraksi dengan data.

AppDatabaseProvider.kt
package com.fajary.registrasisiswa.data

import android.content.Context
import androidx.room.Room

object AppDatabaseProvider {
@Volatile
private var instance: AppDatabase? = null

fun getDatabase(context: Context): AppDatabase
{
synchronized(this)
{
if (instance == null) {
val building: AppDatabase = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_db"
).build()

instance = building
}

return instance!!;
}
}
}
AppDatabaseProvider.kt berfungsi untuk membuat dan menyediakan instance database secara global menggunakan pola singleton. Hal ini dilakukan agar database hanya dibuat satu kali selama aplikasi berjalan. Dengan menggunakan Room.databaseBuilder, database diberi nama app_db dan disimpan dalam variabel instance yang bersifat thread-safe menggunakan @Volatile dan synchronized.

StudentViewModelInterface.kt
package com.fajary.registrasisiswa.viewmodel

import com.fajary.registrasisiswa.data.Siswa

interface StudentViewModelInterface {
val editingSiswa: Siswa?;
val nama: String
val email: String
val registeredSiswa: List<Siswa>
val isLoadingQuery: Boolean
val errorMessage: String?

fun setEditingSiswaId(editingSiswaId: Int?)
fun setNama(nama: String)
fun setEmail(email: String)
fun submitForm()
fun deleteSiswa(id: Int)
}
StudentViewModelInterface.kt adalah interface yang mendefinisikan kontrak untuk ViewModel dalam aplikasi. Interface ini menentukan state yang harus tersedia seperti data siswa yang sedang diedit, input nama dan email, daftar siswa yang sudah terdaftar, status loading, dan pesan error. Selain itu juga mendefinisikan fungsi penting seperti mengubah input, submit form, memilih data untuk diedit, dan menghapus data.

StudentViewModelMock.kt
package com.fajary.registrasisiswa.viewmodel

import android.widget.Toast
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fajary.registrasisiswa.data.Siswa
import com.fajary.registrasisiswa.data.SiswaDao
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

public class StudentViewModelMock : StudentViewModelInterface {
private var _editingSiswa by mutableStateOf<Siswa?>(null)
private var _nama by mutableStateOf<String>("")
private var _email by mutableStateOf<String>("")
private var _registeredSiswa by mutableStateOf<List<Siswa>>(emptyList())
private var _isLoadingQuery by mutableStateOf<Boolean>(false)
private var _errorMessage by mutableStateOf<String?>(null)

override val editingSiswa: Siswa?
get() = _editingSiswa
override val nama: String
get() = _nama
override val email: String
get() = _email
override val registeredSiswa: List<Siswa>
get() = _registeredSiswa
override val isLoadingQuery: Boolean
get() = _isLoadingQuery
override val errorMessage: String?
get() = _errorMessage

fun setSiswaList(registeredSiswa: List<Siswa>)
{
_registeredSiswa = registeredSiswa
}
fun setErrorMessage(errorMessage: String?)
{
_errorMessage = errorMessage
}
override fun setEditingSiswaId(editingSiswaId: Int?)
{

}
override fun setNama(nama: String)
{

}
override fun setEmail(email: String)
{

}
override fun submitForm()
{

}

override fun deleteSiswa(id: Int)
{

}
}
StudentViewModelMock.kt adalah implementasi dummy dari ViewModel yang digunakan khusus untuk keperluan preview UI di Jetpack Compose. File ini tidak terhubung ke database, sehingga semua fungsi seperti submit atau delete tidak memiliki implementasi nyata. Namun, file ini tetap menyediakan state menggunakan mutableStateOf agar UI tetap bisa diuji dengan data palsu.

StudentViewModel.kt
package com.fajary.registrasisiswa.viewmodel

import android.widget.Toast
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fajary.registrasisiswa.data.Siswa
import com.fajary.registrasisiswa.data.SiswaDao
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

public class StudentViewModel(private val siswaDao: SiswaDao) : ViewModel(), StudentViewModelInterface {
private var _editingSiswa by mutableStateOf<Siswa?>(null)
private var _nama by mutableStateOf<String>("")
private var _email by mutableStateOf<String>("")
private var _registeredSiswa by mutableStateOf<List<Siswa>>(emptyList())
private var _isLoadingQuery by mutableStateOf<Boolean>(false)
private var _errorMessage by mutableStateOf<String?>(null)

override val editingSiswa: Siswa?
get() = _editingSiswa
override val nama: String
get() = _nama
override val email: String
get() = _email
override val registeredSiswa: List<Siswa>
get() = _registeredSiswa
override val isLoadingQuery: Boolean
get() = _isLoadingQuery
override val errorMessage: String?
get() = _errorMessage

fun clearState(clearInput: Boolean)
{
_errorMessage = null

if(clearInput)
{
_nama = ""
_email = ""
}
}
fun refreshRegisteredSiswa()
{
_isLoadingQuery = true
viewModelScope.launch {
_registeredSiswa = siswaDao.selectAll()
_isLoadingQuery = false
}
}
override fun setEditingSiswaId(editingSiswaId: Int?)
{
if(isLoadingQuery)
{
return
}

clearState(false)
if(editingSiswaId == null)
{
_editingSiswa = null
clearState(true)
return
}

_isLoadingQuery = true

viewModelScope.launch {
val siswa = siswaDao.selectSiswa(editingSiswaId)

if(siswa != null)
{
_editingSiswa = siswa
_nama = siswa.nama
_email = siswa.email
}
else
{
_editingSiswa = null
_errorMessage = "Cannot find siswa with id ${editingSiswaId}"
}

_isLoadingQuery = false
}
}
override fun setNama(nama: String)
{
if(isLoadingQuery)
{
return
}

_nama = nama;
}
override fun setEmail(email: String)
{
if(isLoadingQuery)
{
return
}

_email = email
}
override fun submitForm()
{
if(isLoadingQuery)
{
return
}

if(nama == "" || email == "")
{
_errorMessage = "Nama atau email tidak boleh kosong!"
return
}

_isLoadingQuery = true
clearState(false)

viewModelScope.launch {
if(_editingSiswa != null)
{
siswaDao.updateSiswa(_editingSiswa!!.id, nama, email)
}
else
{
siswaDao.insert(nama, email)
}

_registeredSiswa = siswaDao.selectAll()

clearState(true)
_editingSiswa = null
_isLoadingQuery = false
}
}

override fun deleteSiswa(id: Int) {
if(isLoadingQuery)
{
return;
}

_isLoadingQuery = true
clearState(false)

viewModelScope.launch {
siswaDao.deleteSiswa(id)

if(_editingSiswa != null && _editingSiswa!!.id == id)
{
clearState(true)
}

_registeredSiswa = siswaDao.selectAll()
_isLoadingQuery = false
}
}
}
StudentViewModel.kt adalah inti logika aplikasi yang menghubungkan UI dengan database. ViewModel ini mengelola state aplikasi seperti data siswa, input form, loading state, dan error message. Semua operasi database seperti insert, update, delete, dan select dilakukan melalui coroutine viewModelScope.launch agar berjalan asynchronous. File ini juga menangani validasi input serta memastikan UI selalu mendapatkan data terbaru dari database.

StudentItem.kt
package com.fajary.registrasisiswa.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
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.padding
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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.fajary.registrasisiswa.GetTwoMainCharacter
import com.fajary.registrasisiswa.data.Siswa
import com.fajary.registrasisiswa.viewmodel.StudentViewModelInterface

@Composable
fun StudentItem(vm: StudentViewModelInterface, siswa: Siswa)
{
Box(
modifier = Modifier.fillMaxWidth()
.background(Color.Black, shape = RoundedCornerShape(8.dp))
)
{
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.background(
color = Color.White,
shape = CircleShape
).padding(12.dp)
)
{
Text(
text = GetTwoMainCharacter(siswa.nama),
fontWeight = FontWeight.Bold
)
}
Column(
modifier = Modifier.padding(start = 12.dp)
) {
Text(
text = siswa.nama,
color = Color.White,
fontWeight = FontWeight.Bold
)
Text(
text = siswa.email,
color = Color.White
)
}
Spacer(
modifier = Modifier.weight(1f)
)
IconButton(
onClick = {
vm.setEditingSiswaId(siswa.id)
}
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.Edit,
contentDescription = "Edit",
)
}
IconButton(
onClick = {
vm.deleteSiswa(siswa.id)
}
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
)
}
}
}
Spacer(
modifier = Modifier.padding(bottom = 8.dp)
)
}
StudentItem.kt adalah composable yang bertugas menampilkan satu item siswa dalam bentuk card UI. Di dalamnya ditampilkan inisial nama siswa, nama lengkap, dan email. Selain itu terdapat tombol edit dan delete yang langsung memanggil fungsi dari ViewModel untuk mengubah atau menghapus data siswa. UI ini menggunakan layout Row dan Column untuk menyusun tampilan secara horizontal dan vertikal.

FormInput.kt
package com.fajary.registrasisiswa.ui

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.fajary.registrasisiswa.viewmodel.StudentViewModelInterface

@Composable
fun FormInput(vm: StudentViewModelInterface)
{
OutlinedTextField(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
value = vm.nama,
onValueChange = {
vm.setNama(it)
},
label = {
Text(
text = "Nama"
)
}
)

OutlinedTextField(
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
value = vm.email,
onValueChange = {
vm.setEmail(it)
},
label = {
Text(
text = "Email"
)
}
)

Button(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
onClick = {
vm.submitForm()
},
shape = RoundedCornerShape(
8.dp
),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Black
)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector =
if(vm.editingSiswa != null)
Icons.Default.Edit
else
Icons.Default.Add,
contentDescription = null
)
Text(
modifier = Modifier.padding(8.dp),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
text = if(vm.editingSiswa != null) "Simpan Perubahan" else "Tambah Siswa"
)
}
}

if (vm.errorMessage != null)
Text(
text = vm.errorMessage!!,
color = Color.Red,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(top = 16.dp)
)
}
FormInput.kt adalah komponen form input yang digunakan untuk menambah atau mengedit data siswa. File ini berisi dua input field yaitu nama dan email yang terhubung langsung ke state ViewModel. Selain itu terdapat tombol submit yang berubah teks dan ikon tergantung mode (tambah atau edit). Jika terjadi error validasi, pesan error akan ditampilkan di bawah form dengan warna merah.

MainScreen,kt
package com.fajary.registrasisiswa.ui

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.fajary.registrasisiswa.viewmodel.StudentViewModelInterface

@Composable
fun MainScreen(vm: StudentViewModelInterface, modifier: Modifier)
{
Box(
modifier = modifier.fillMaxSize()
)
{
Column(
modifier = Modifier.padding(20.dp).fillMaxSize()
) {
Text(
text = if(vm.editingSiswa != null) "Megedit Data Siswa" else "Registrasi Siswa",
fontWeight = FontWeight.ExtraBold,
fontSize = 32.sp
)
Text(
text = "Kelola data siswa",
fontSize = 16.sp
)

FormInput(vm)

Text(
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp),
text = "Daftar Siswa",
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)

LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(vm.registeredSiswa){ siswa ->
StudentItem(vm, siswa)
}
}

}
}
}
MainScreen.kt adalah tampilan utama aplikasi yang menyusun seluruh UI menjadi satu halaman. Di dalamnya terdapat judul aplikasi, form input, serta daftar siswa yang ditampilkan menggunakan LazyColumn. File ini juga menyesuaikan judul berdasarkan apakah user sedang dalam mode edit atau tidak. Semua data dan aksi UI dihubungkan melalui ViewModel interface.

MainActivity.kt
package com.fajary.registrasisiswa

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Shapes
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.fajary.registrasisiswa.data.AppDatabaseProvider
import com.fajary.registrasisiswa.data.Siswa
import com.fajary.registrasisiswa.ui.MainScreen
import com.fajary.registrasisiswa.ui.theme.RegistrasiSiswaTheme
import com.fajary.registrasisiswa.viewmodel.StudentViewModel
import com.fajary.registrasisiswa.viewmodel.StudentViewModelInterface
import com.fajary.registrasisiswa.viewmodel.StudentViewModelMock

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
RegistrasiSiswaTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val context = LocalContext.current
val database = remember {
AppDatabaseProvider.getDatabase(context)
}
val viewModel = remember {
StudentViewModel(database.siswaDao())
}

LaunchedEffect(Unit) {
viewModel.refreshRegisteredSiswa()
}

RegistrasiSiswa(
modifier = Modifier.padding(innerPadding),
vm = viewModel
)
}
}
}
}
}

@Composable
fun RegistrasiSiswa(modifier: Modifier = Modifier, vm: StudentViewModelInterface) {
MainScreen(vm, modifier)
}

@Preview(showBackground = true)
@Composable
fun RegistrasiSiswaPreview() {
RegistrasiSiswaTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
var vmInstance = StudentViewModelMock()
vmInstance.setSiswaList(
mutableListOf(
Siswa(id = 1, nama = "Damn", email = "Damn@gmail.com"),
Siswa(id = 2, nama = "Test 123", email = "Damn@gmail.com")
)
)
vmInstance.setErrorMessage("Nama atau email tidak boleh kosong!")

RegistrasiSiswa(
modifier = Modifier.padding(innerPadding),
vm = vmInstance
)
}
}
}

fun GetTwoMainCharacter(name: String) : String
{
if(name.length == 0)
{
return ""
}
if(name.length == 1)
{
return name.get(0).uppercaseChar().toString()
}

val firstChar = name.get(0)
var secondChar = name.get(1)

for(i in 1..name.length - 1 step 1)
{
if(name.get(i) == ' ')
{
if(i < name.length - 1)
{
secondChar = name.get(i + 1)
break
}
}
}

return "${firstChar.uppercaseChar()}${secondChar.uppercaseChar()}"
}
MainActivity.kt adalah entry point dari aplikasi Android. File ini bertugas menginisialisasi database, membuat instance ViewModel, dan menjalankan UI menggunakan Jetpack Compose. Selain itu, data awal siswa di-load menggunakan LaunchedEffect saat aplikasi pertama kali dibuka. Terdapat juga preview function yang digunakan untuk melihat tampilan UI tanpa menjalankan aplikasi secara penuh.















Comments

Popular posts from this blog

Pertemuan 1 KPPL - Software Engineer

Pertemuan 13 OOP - Abstraksi & Simulasi Fox & Rabit

Pertemuan 5 OOP - Membuat Music Organizer