Pertemuan 12 - Aplikasi MVVM

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

Pertemuan 12 - Aplikasi MVVM
Aplikasi ini merupakan aplikasi login dan registrasi sederhana berbasis Android yang dibangun menggunakan Jetpack Compose sebagai framework UI dan Room Database sebagai penyimpanan data lokal. Arsitektur yang digunakan adalah MVVM (Model–View–ViewModel) yang dikombinasikan dengan Repository Pattern untuk memisahkan setiap bagian aplikasi menjadi layer yang jelas dan mudah dikelola. Alur data pada aplikasi dimulai dari UI (Compose), kemudian diteruskan ke ViewModel, dilanjutkan ke Repository, lalu ke DAO (Data Access Object), dan akhirnya ke Room Database yang berjalan di atas SQLite.

User.kt
package com.fajary.loginpagedatabase

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

@Entity(
indices = [
Index(value = ["username"], unique = true)
]
)
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val username: String,
val password: String
)
Class User merupakan entity yang merepresentasikan tabel di dalam database Room. Entity ini mendefinisikan struktur data yang akan disimpan, yaitu id, username, dan password. Field id dijadikan primary key dengan auto generate, sedangkan username diberi constraint unik menggunakan index agar tidak ada data user yang memiliki username yang sama. Entity ini berperan sebagai model data utama yang digunakan oleh seluruh layer aplikasi.

UserDAO.kt
package com.fajary.loginpagedatabase

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

@Dao
interface UserDAO
{
@Query("INSERT INTO User(username, password) VALUES (:username, :password)")
suspend fun insert(username: String, password: String)

@Query("SELECT * FROM User WHERE username = :username AND password = :password")
suspend fun login(username: String, password: String): User?

@Query(value = "SELECT * FROM User WHERE username = :username")
suspend fun selectOne(username: String): User?
}
Interface UserDAO merupakan Data Access Object yang berfungsi sebagai penghubung langsung ke database. DAO berisi definisi query untuk operasi data seperti insert user, login user, dan mengambil data user berdasarkan username. Semua query ini dijalankan oleh Room dan diterjemahkan menjadi operasi SQLite. DAO bertanggung jawab penuh terhadap akses data mentah tanpa mengatur logic bisnis aplikasi.

MainDatabase.kt
package com.fajary.loginpagedatabase

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

@Database(
entities = [User::class],
version = 1
)
abstract class MainDatabase : RoomDatabase()
{
abstract fun userDAO() : UserDAO
}
Class MainDatabase merupakan konfigurasi utama database Room. Di dalamnya ditentukan entity yang digunakan (User) serta versi database. Class ini juga berfungsi sebagai penghubung utama untuk mendapatkan instance DAO. Dengan adanya Room Database, aplikasi tidak perlu berinteraksi langsung dengan SQLite karena sudah disediakan abstraction layer yang lebih aman dan terstruktur.

MainDatabaseProvider.kt
package com.fajary.loginpagedatabase

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

object MainDatabaseProvider {
@Volatile
private var instance: MainDatabase? = null

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

instance = building
}

return instance!!;
}
}
}
Class MainDatabaseProvider digunakan untuk membuat dan mengelola instance database menggunakan singleton pattern. Tujuannya adalah memastikan bahwa database hanya dibuat satu kali selama aplikasi berjalan, sehingga tidak terjadi multiple instance yang dapat menyebabkan error atau pemborosan resource. Class ini juga memastikan database diakses menggunakan application context agar tidak terjadi memory leak.

MainRepository.kt
package com.fajary.loginpagedatabase

public class MainRepository
{
private val dao: UserDAO;

constructor(dao: UserDAO)
{
this.dao = dao
}

suspend fun register(username: String, password: String): User?
{
dao.insert(username, password)
return dao.selectOne(username)
}

suspend fun login(username: String, password: String): User?
{
return dao.login(username, password)
}

suspend fun selectOne(username: String): User?
{
return dao.selectOne(username)
}
}
Class MainRepository berfungsi sebagai lapisan perantara antara ViewModel dan DAO. Repository ini menggabungkan dan menyederhanakan akses data, seperti proses register, login, dan pencarian user. Dengan adanya repository, ViewModel tidak perlu mengetahui detail implementasi database, sehingga struktur kode menjadi lebih bersih dan mudah dipelihara.

MainViewModelInterface.kt
package com.fajary.loginpagedatabase

interface MainViewModelInterface {
val mainPageType: String
val username: String
val password: String
val authenticatedUser: User?
val registeredUser: User?
val isLoadingQuery: Boolean
val errorMessage: String?

fun setUsername(username: String)
fun setPassword(password: String)
fun changeToLogin()
fun changeToRegister()
fun login()
fun register()
fun clearState(clearInputs: Boolean)
}
Interface MainViewModelInterface digunakan untuk mendefinisikan kontrak semua fungsi dan state yang harus dimiliki oleh ViewModel. Interface ini mencakup data seperti username, password, status loading, error message, serta fungsi seperti login, register, dan perubahan state halaman. Tujuan utama interface ini adalah agar ViewModel bisa di-mock dan diganti implementasinya, terutama untuk keperluan testing atau preview UI.

MainViewModelMock.kt
package com.fajary.loginpagedatabase

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

public class MainViewModelMock() : MainViewModelInterface {
private var _mainPageType by mutableStateOf("login")
private var _username by mutableStateOf("")
private var _password by mutableStateOf("")
private var _authenticatedUser by mutableStateOf<User?>(null)
private var _registeredUser by mutableStateOf<User?>(null)
private var _isLoadingQuery by mutableStateOf(false)
private var _errorMessage by mutableStateOf<String?>(null)

override val mainPageType: String
get() = _mainPageType
override val username: String
get() = _username
override val password: String
get() = _password
override val authenticatedUser: User?
get() = _authenticatedUser
override val registeredUser: User?
get() = _registeredUser
override val isLoadingQuery: Boolean
get() = _isLoadingQuery
override val errorMessage: String?
get() = _errorMessage

override fun setUsername(username: String)
{

}
override fun setPassword(password: String)
{

}
override fun clearState(clearInputs: Boolean)
{

}
override fun changeToLogin()
{

}
override fun changeToRegister()
{

}
override fun login()
{

}
override fun register()
{

}
}
Class MainViewModelMock merupakan implementasi dummy dari ViewModel Interface yang digunakan khusus untuk keperluan preview di Jetpack Compose. Class ini tidak terhubung dengan database maupun repository, sehingga tidak melakukan proses nyata seperti login atau register. Tujuannya adalah agar UI tetap dapat dijalankan di Preview Android Studio tanpa membutuhkan dependency eksternal seperti database atau coroutine.

MainViewModel.kt
package com.fajary.loginpagedatabase

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 kotlinx.coroutines.delay
import kotlinx.coroutines.launch

public class MainViewModel(private val repository: MainRepository) : ViewModel(), MainViewModelInterface {
private var _mainPageType by mutableStateOf("login")
private var _username by mutableStateOf("")
private var _password by mutableStateOf("")
private var _authenticatedUser by mutableStateOf<User?>(null)
private var _registeredUser by mutableStateOf<User?>(null)
private var _isLoadingQuery by mutableStateOf(false)
private var _errorMessage by mutableStateOf<String?>(null)

override val mainPageType: String
get() = _mainPageType
override val username: String
get() = _username
override val password: String
get() = _password
override val authenticatedUser: User?
get() = _authenticatedUser
override val registeredUser: User?
get() = _registeredUser
override val isLoadingQuery: Boolean
get() = _isLoadingQuery
override val errorMessage: String?
get() = _errorMessage

override fun setUsername(username: String) {
_username = username
}

override fun setPassword(password: String) {
_password = password
}
override fun clearState(clearInputs: Boolean)
{
_errorMessage = null
_registeredUser = null
_authenticatedUser = null

if(clearInputs)
{
_username = ""
_password = ""
}
}
override fun changeToLogin()
{
clearState(true)

_mainPageType = "login"
}
override fun changeToRegister()
{
clearState(true)

_mainPageType = "register"
}
override fun login()
{
if(_isLoadingQuery)
{
return
}

clearState(false)
_isLoadingQuery = true
viewModelScope.launch {
val user = repository.login(_username, _password)
delay(2000)

if(user != null)
{
_authenticatedUser = user
}
else
{
_errorMessage = "Error login on ${username}!"
_authenticatedUser = null
}
_isLoadingQuery = false
}
}
override fun register()
{
if(_isLoadingQuery)
{
return
}

clearState(false)
_isLoadingQuery = true
viewModelScope.launch {
var user: User? = null
try {
user = repository.register(username, password)
}
catch (exception: Exception)
{
_errorMessage = "Error registering ${username}, already exists!"
}

delay(2000)
_registeredUser = user
_isLoadingQuery = false

changeToLogin()
}
}
}
Class MainViewModel merupakan pusat logika utama aplikasi. ViewModel ini mengatur seluruh state aplikasi seperti username, password, user yang login, status loading, dan pesan error menggunakan mutable state agar UI dapat bereaksi secara otomatis. ViewModel juga menjalankan proses asynchronous menggunakan coroutine melalui viewModelScope untuk mengakses repository tanpa mengganggu UI thread. Selain itu, ViewModel juga mengatur alur login, register, serta perubahan halaman login dan register.

MainActivity.kt
package com.fajary.loginpagedatabase

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
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 androidx.lifecycle.viewmodel.compose.viewModel
import com.fajary.loginpagedatabase.ui.theme.LoginPageDatabaseTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
LoginPageDatabaseTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val context = LocalContext.current
val database = remember {
MainDatabaseProvider.getDatabase(context)
}
val repository = remember {
MainRepository(database.userDAO())
}
val viewModel = remember {
MainViewModel(repository)
}

RootView(
modifier = Modifier.padding(innerPadding),
viewModel
)
}
}
}
}
}

@Composable
fun RootView(modifier: Modifier, vm: MainViewModelInterface)
{
Box(
modifier = modifier.fillMaxSize()
)
{
if(vm.authenticatedUser != null)
{
AuthenticatedView(vm)
}
else
{
if(vm.mainPageType == "login")
{
LoginPage(vm)
}
else
{
RegisterPage(vm)
}
}
}
}

@Composable
fun AuthenticatedView(vm: MainViewModelInterface)
{
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Welcome",
fontSize = 24.sp,
fontWeight = FontWeight.ExtraBold
)
Text(
text = "${vm.authenticatedUser!!.username}!",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color.Gray
)
Button(
modifier = Modifier.padding(12.dp),
onClick = {
vm.changeToLogin()
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Black
)
) {
Text(
text = "Logout",
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
}

@Composable
fun LoginPage(vm: MainViewModelInterface)
{
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp),
text = "Welcome Back!",
fontSize = 32.sp,
textAlign = TextAlign.Center,
fontWeight = FontWeight.ExtraBold
)
Column(
modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, top = 4.dp, bottom = 4.dp)
) {
Text(
text = "Username",
fontWeight = FontWeight.Bold
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = vm.username,
singleLine = true,
onValueChange = {
vm.setUsername(it)
},
placeholder = {
Text(
text = "Enter username..."
)
}
)
}
Column(
modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, top = 4.dp, bottom = 4.dp)
) {
Text(
text = "Password",
fontWeight = FontWeight.Bold
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = vm.password,
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
onValueChange = {
vm.setPassword(it)
},
placeholder = {
Text(
text = "Enter password..."
)
}
)
}

if(!vm.isLoadingQuery)
{
Button(
modifier = Modifier.padding(8.dp),
onClick = {
vm.login()
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Black
)
) {
Text(
text = "Login",
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
else
{
CircularProgressIndicator(
modifier = Modifier.padding(8.dp)
)
}

Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Don't have an account? ",
fontSize = 14.sp
)
Text(
text = "Register now!",
modifier = Modifier.clickable(
onClick = {
vm.changeToRegister()
},
enabled = !vm.isLoadingQuery
),
fontSize = 14.sp,
color = if(!vm.isLoadingQuery) Color.Blue else Color.Gray
)
}

if(vm.errorMessage != null)
{
Text(
text = vm.errorMessage!!,
fontWeight = FontWeight.Bold,
fontSize = 12.sp,
color = Color.Red,
modifier = Modifier.padding(12.dp)
)
}
}
}

@Composable
fun RegisterPage(vm: MainViewModelInterface)
{
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp),
text = "New Here?",
fontSize = 32.sp,
textAlign = TextAlign.Center,
fontWeight = FontWeight.ExtraBold
)
Column(
modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, top = 4.dp, bottom = 4.dp)
) {
Text(
text = "Username",
fontWeight = FontWeight.Bold
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = vm.username,
singleLine = true,
onValueChange = {
vm.setUsername(it)
},
placeholder = {
Text(
text = "Enter username..."
)
}
)
}
Column(
modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, top = 4.dp, bottom = 4.dp)
) {
Text(
text = "Password",
fontWeight = FontWeight.Bold
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = vm.password,
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
onValueChange = {
vm.setPassword(it)
},
placeholder = {
Text(
text = "Enter password..."
)
}
)
}

if(!vm.isLoadingQuery)
{
Button(
modifier = Modifier.padding(8.dp),
onClick = {
vm.register()
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Black
)
) {
Text(
text = "Register",
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
else
{
CircularProgressIndicator(
modifier = Modifier.padding(8.dp)
)
}

Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Already have an account? ",
fontSize = 14.sp
)
Text(
text = "Login now!",
modifier = Modifier.clickable(
onClick = {
vm.changeToLogin()
},
enabled = !vm.isLoadingQuery
),
fontSize = 14.sp,
color = if(!vm.isLoadingQuery) Color.Blue else Color.Gray
)
}

if(vm.errorMessage != null)
{
Text(
text = vm.errorMessage!!,
fontWeight = FontWeight.Bold,
fontSize = 12.sp,
color = Color.Red,
modifier = Modifier.padding(12.dp)
)
}
}
}



@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
LoginPageDatabaseTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
RootView(
modifier = Modifier.padding(innerPadding),
MainViewModelMock()
)
}
}
}
Class MainActivity merupakan entry point aplikasi Android. Di dalamnya dilakukan inisialisasi database, repository, dan ViewModel, kemudian diteruskan ke Composable root view. MainActivity juga bertugas mengatur lifecycle aplikasi dan menjadi tempat pertama UI dijalankan menggunakan Jetpack Compose.
Bagian Composable merupakan layer UI yang bertanggung jawab menampilkan tampilan aplikasi. RootView berfungsi sebagai router sederhana untuk menentukan halaman yang ditampilkan berdasarkan state ViewModel. LoginPage dan RegisterPage menampilkan form input user, sedangkan AuthenticatedView menampilkan halaman setelah user berhasil login. Semua UI bersifat reaktif, sehingga perubahan state di ViewModel akan langsung mempengaruhi tampilan tanpa perlu refresh manual.

Screenshot

Saat melakukan register

Saat credentials salah

Saat credentials benar







Comments

Popular posts from this blog

Pertemuan 1 KPPL - Software Engineer

Pertemuan 13 OOP - Abstraksi & Simulasi Fox & Rabit

Pertemuan 5 OOP - Membuat Music Organizer