Tugas PPB Pertemuan 12

Nathanael Valen Susilo
5025231099

Aplikasi Login dengan Arsitektur MVVM




Aplikasi Login User - MVVM 

Project ini adalah aplikasi Android sederhana untuk studi kasus login dan register user. Aplikasi dibuat dengan Kotlin, Jetpack Compose, arsitektur MVVM, dan Room Database sebagai penyimpanan lokal.

Fitur utama:

- Register user memakai username, password, dan konfirmasi password.
- Login user memakai username dan password.
- Validasi input kosong, password minimal 4 karakter, dan konfirmasi password.
- Pesan berhasil atau gagal ditampilkan langsung di UI.
- Data user disimpan di Room Database.
- Password disimpan sebagai hash SHA-256, bukan teks mentah.

Struktur Folder

com.example.loginmvvm
|
|-- data
|   |-- local
|       |-- dao
|       |   |-- UserDao.kt
|       |-- database
|       |   |-- AppDatabase.kt
|       |-- entity
|           |-- UserEntity.kt
|
|-- repository
|   |-- UserRepository.kt
|
|-- ui
|   |-- screen
|   |   |-- LoginScreen.kt
|   |-- theme
|       |-- Color.kt
|       |-- Theme.kt
|       |-- Type.kt
|
|-- viewmodel
|   |-- LoginViewModel.kt
|
|-- MainActivity.kt

Arsitektur MVVM

Alur aplikasi:
UI Compose
    -> LoginViewModel
    -> UserRepository
    -> UserDao
    -> Room Database
    -> SQLite

Penjelasan tiap layer:
- `UI Compose`: menampilkan form login/register dan mengirim aksi user ke ViewModel.
- `LoginViewModel`: menyimpan state UI, menjalankan validasi, dan memanggil repository.
- `UserRepository`: menjadi penghubung antara ViewModel dan data lokal.
- `UserDao`: berisi operasi database seperti insert user, cari username, dan login.
- `AppDatabase`: konfigurasi Room Database.
- `SQLite`: storage lokal yang digunakan Room di bawahnya.

Entity

`UserEntity` adalah representasi tabel `users` di Room Database.

@Entity(
    tableName = "users",
    indices = [Index(value = ["username"], unique = true)]
)
data class UserEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val username: String,
    val passwordHash: String
)

Kolom `username` dibuat unik agar satu username tidak bisa dipakai dua kali.

DAO

`UserDao` berisi query database untuk register dan login.

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.ABORT)
    suspend fun insertUser(user: UserEntity)

    @Query("SELECT * FROM users WHERE username = :username LIMIT 1")
    suspend fun findByUsername(username: String): UserEntity?

    @Query("SELECT * FROM users WHERE username = :username AND passwordHash = :passwordHash LIMIT 1")
    suspend fun login(username: String, passwordHash: String): UserEntity?
}

Room Database

`AppDatabase` membuat database lokal bernama `login_mvvm.db`.

@Database(
    entities = [UserEntity::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Repository

`UserRepository` menangani proses register, login, dan hashing password.

class UserRepository(
    private val userDao: UserDao
) {
    suspend fun register(username: String, password: String): RegisterResult {
        val normalizedUsername = username.trim()
        if (userDao.findByUsername(normalizedUsername) != null) {
            return RegisterResult.UsernameTaken
        }

        userDao.insertUser(
            UserEntity(
                username = normalizedUsername,
                passwordHash = password.sha256()
            )
        )
        return RegisterResult.Success
    }

    suspend fun login(username: String, password: String): UserEntity? {
        return userDao.login(username.trim(), password.sha256())
    }
}

ViewModel

`LoginViewModel` menyimpan state UI dengan `StateFlow`.

class LoginViewModel(
    private val repository: UserRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(LoginUiState())
    val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()

    fun login() {
        val currentState = _uiState.value
        val validationMessage = validateLogin(currentState)
        if (validationMessage != null) {
            _uiState.update { it.copy(message = UiMessage.Error(validationMessage)) }
            return
        }

        viewModelScope.launch {
            val user = repository.login(currentState.username, currentState.password)
            _uiState.update {
                if (user != null) {
                    it.copy(loggedInUsername = user.username, message = UiMessage.Success("Login berhasil"))
                } else {
                    it.copy(message = UiMessage.Error("Username atau password salah"))
                }
            }
        }
    }
}

UI Compose

`LoginScreen` menerima state dari ViewModel dan mengirim event user kembali ke ViewModel.

LoginScreen(
    uiState = uiState,
    onUsernameChange = viewModel::onUsernameChange,
    onPasswordChange = viewModel::onPasswordChange,
    onConfirmPasswordChange = viewModel::onConfirmPasswordChange,
    onShowLogin = viewModel::showLogin,
    onShowRegister = viewModel::showRegister,
    onLogin = viewModel::login,
    onRegister = viewModel::register,
    onLogout = viewModel::logout
)

Dengan pola ini, UI tidak langsung mengakses database. UI hanya membaca state dan memanggil fungsi dari ViewModel.

Alur Login

1. User mengisi username dan password.
2. User menekan tombol `LOGIN`.
3. `LoginScreen` memanggil `LoginViewModel.login()`.
4. ViewModel memvalidasi input.
5. ViewModel memanggil `UserRepository.login()`.
6. Repository membuat hash password dan memanggil `UserDao.login()`.
7. Room mencari data user di SQLite.
8. Jika data cocok, UI menampilkan pesan `Login berhasil`.
9. Jika data tidak cocok, UI menampilkan pesan `Username atau password salah`.

Alur Register

1. User membuka tab `Register`.
2. User mengisi username, password, dan konfirmasi password.
3. ViewModel memvalidasi input.
4. Repository mengecek apakah username sudah dipakai.
5. Jika username belum ada, data user disimpan ke Room Database.
6. UI kembali ke mode login dan menampilkan pesan register berhasil.

Demo Aplikasi :



Komentar

Postingan populer dari blog ini

Tugas PPB Pertemuan 1

Tugas PBB Pertemuan 5

ETS PPB (Pertemuan 8)