Tuesday, June 16, 2026

PPB EAS - Aplikasi Member Timezone

Manajemen Keanggotaan & Loyalty Program Timezone dengan Room Database dan Jetpack Compose

Aryaka Leorgi Epridaka - 5025231117
Muhammad Risyad Himawan Putra - 5025231205 

 

Aplikasi TimezoneMembership ini berfungsi sebagai sistem manajemen keanggotaan digital untuk pusat hiburan Timezone. Aplikasi mendukung dua peran pengguna: Member (yang dapat melakukan top-up saldo Tizo, bermain mesin arcade, menukarkan reward points, dan melihat riwayat transaksi) serta Admin (yang memiliki dashboard untuk mengelola seluruh data member). Seluruh data disimpan secara lokal menggunakan Room Database (SQLite). UI dirancang secara deklaratif menggunakan Jetpack Compose dengan Material 3, dan arsitektur aplikasi menerapkan pola MVVM (Model-View-ViewModel) agar kode program menjadi terstruktur, modular, serta memisahkan logika bisnis dari tampilan. 

 Source Code GitHub: https://github.com/AryakaLeorgi/TimezoneMembership 

 Link Project Requirement: https://drive.google.com/file/d/1nmoltS56WvYHeTYYmfm3NbdxOTZ_FyRd/view?usp=sharing

 Link PPT: https://canva.link/qsfw5yh1cqmy3fp

 Link Aplikasi: https://github.com/AryakaLeorgi/TimezoneMembership/releases/download/apk/TimezoneMembership.apk

 Link Infografis: https://canva.link/o43x2oh4pywauij

 Link YouTube: https://youtu.be/Qy8J54NoRQU

1. Deskripsi & Alur Utama Jalannya Aplikasi

Secara garis besar, aplikasi ini bekerja dengan alur sebagai berikut:

  • Autentikasi Pengguna: Aplikasi menyediakan sistem login dan registrasi. Terdapat fitur "Remember Me" yang memungkinkan auto-login di sesi berikutnya tanpa perlu memasukkan ulang kredensial.
  • Manajemen Saldo & Poin: Member dapat melakukan top-up saldo Tizo melalui 6 tier paket, bermain mesin arcade (simulasi) yang mengurangi saldo dan menambah reward points, serta menukarkan reward points dengan berbagai hadiah.
  • Pencatatan Transaksi: Setiap operasi (top-up, bermain, redeem) dijalankan dalam Room @Transaction untuk menjamin atomicity, yaitu update saldo member dan insert catatan transaksi berjalan sebagai satu kesatuan.
  • Navigasi Halaman: Menggunakan Navigation Compose untuk berpindah antar halaman, seperti AuthScreen, Admin Dashboard, Member Detail, Top-Up, Play, Redeem, dan History.
  • Pengelolaan State UI: Seluruh perubahan data dari database diamati secara reaktif melalui Flow dan StateFlow, sehingga UI selalu menampilkan data terkini tanpa perlu di-refresh manual.

2. Entity Data Model - Member.kt

package com.example.timezonemembership.data.model import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "members") data class Member( @PrimaryKey(autoGenerate = true) val id: Int = 0, val name: String, val email: String, val phone: String, val tizoBalance: Int = 0, val rewardPoints: Int = 0, val password: String, val role: String = "MEMBER", // "MEMBER" or "ADMIN" val isRemembered: Boolean = false )

Penjelasan: File Member.kt mendefinisikan entitas tabel members di Room Database. Anotasi @Entity menandai kelas ini sebagai tabel SQLite, sedangkan @PrimaryKey(autoGenerate = true) membuat kolom id terisi otomatis secara inkremental. Setiap member menyimpan data profil (nama, email, telepon), saldo Tizo dan reward points untuk sistem loyalitas, password untuk autentikasi, kolom role yang membedakan antara "MEMBER" dan "ADMIN", serta flag isRemembered yang mendukung fitur auto-login agar pengguna tidak perlu login ulang setiap kali membuka aplikasi.


3. Entity Data Model - Transaction.kt

package com.example.timezonemembership.data.model import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey @Entity( tableName = "transactions", foreignKeys = [ ForeignKey( entity = Member::class, parentColumns = ["id"], childColumns = ["memberId"], onDelete = ForeignKey.CASCADE ) ], indices = [Index(value = ["memberId"])] ) data class Transaction( @PrimaryKey(autoGenerate = true) val id: Int = 0, val memberId: Int, val transactionType: String, // "TOPUP", "PLAY", "REDEEM" val description: String, val tizoMutation: Int, // positive for credit, negative for debit val pointMutation: Int, // positive for credit, negative for debit val timestamp: Long = System.currentTimeMillis() )

Penjelasan: Entitas Transaction mencatat seluruh riwayat transaksi yang dilakukan member. Relasi antar tabel dibangun menggunakan ForeignKey yang menghubungkan kolom memberId di tabel transactions dengan kolom id di tabel members. Parameter onDelete = ForeignKey.CASCADE memastikan ketika sebuah record member dihapus, seluruh transaksi miliknya juga ikut terhapus secara otomatis. Anotasi @Index pada kolom memberId berfungsi mempercepat query pencarian transaksi berdasarkan member tertentu. Kolom tizoMutation dan pointMutation menggunakan nilai positif untuk kredit (penambahan) dan negatif untuk debit (pengurangan), sehingga satu model bisa merepresentasikan semua jenis transaksi.


4. Constants.kt - Konstanta & Aturan Bisnis

package com.example.timezonemembership.data.model object Constants { // ==================== TOP-UP TIERS ==================== data class TopUpTier( val name: String, val priceRp: Int, val tizoAmount: Int, val rewardPoints: Int ) val TOP_UP_TIERS = listOf( TopUpTier("Tier 1", 100_000, 100, 10), TopUpTier("Tier 2", 200_000, 200, 25), TopUpTier("Tier 3", 300_000, 300, 40), TopUpTier("Tier 4", 500_000, 500, 75), TopUpTier("Tier 5", 1_000_000, 1000, 200), TopUpTier("Tier 6", 2_000_000, 2000, 500), ) // ==================== MACHINE CATEGORIES ==================== data class MachineCategory( val name: String, val description: String, val tizoCost: Int, val rewardPoints: Int ) val MACHINE_CATEGORIES = listOf( MachineCategory("Mesin Ringan", "Capit boneka kecil, dsb", 5, 2), MachineCategory("Mesin Sedang", "Arcade standard, balapan", 10, 3), MachineCategory("Mesin Berat", "VR, mesin tiket besar", 15, 4), ) // ==================== REDEEM REWARDS ==================== // Berisi 3 kategori hadiah: Gratis Bermain, Merchandise, dan Diskon Top-Up // Total 9 jenis hadiah yang dapat ditukarkan dengan reward points val ALL_REWARDS = REDEEM_FREE_PLAY + REDEEM_MERCHANDISE + REDEEM_DISCOUNT }

Penjelasan: File Constants.kt berperan sebagai Single Source of Truth untuk seluruh aturan bisnis aplikasi. Menggunakan keyword object menjadikannya Singleton yang bisa diakses dari mana saja tanpa perlu membuat instance. Di dalamnya terdefinisi 6 tier paket top-up (dari Rp100.000 hingga Rp2.000.000) dengan masing-masing memberikan jumlah Tizo dan bonus reward points yang berbeda, 3 kategori mesin arcade dengan biaya dan poin yang bervariasi, serta 9 jenis hadiah penukaran yang terbagi ke dalam tiga kategori (Gratis Bermain, Merchandise, dan Diskon). Dengan memusatkan semua konstanta di satu tempat, perubahan aturan bisnis cukup dilakukan di file ini tanpa perlu mengubah kode di banyak tempat.


5. MemberDao.kt - Data Access Object

package com.example.timezonemembership.data.dao import androidx.room.* import com.example.timezonemembership.data.model.Member import kotlinx.coroutines.flow.Flow @Dao interface MemberDao { @Query("SELECT * FROM members ORDER BY id DESC") fun getAllMembers(): Flow<List<Member>> @Query("SELECT * FROM members WHERE role = :role ORDER BY id DESC") fun getAllMembersByRole(role: String): Flow<List<Member>> @Query("SELECT * FROM members WHERE id = :id") fun getMemberById(id: Int): Flow<Member?> @Query("SELECT * FROM members WHERE id = :id") suspend fun getMemberByIdOnce(id: Int): Member? @Query("SELECT * FROM members WHERE email = :email AND password = :password LIMIT 1") suspend fun login(email: String, password: String): Member? @Query("SELECT * FROM members WHERE isRemembered = 1 LIMIT 1") suspend fun getRememberedMember(): Member? @Query("UPDATE members SET isRemembered = 0") suspend fun clearAllRemembered() @Query("UPDATE members SET isRemembered = :isRemembered WHERE id = :id") suspend fun setRemembered(id: Int, isRemembered: Boolean) @Query("SELECT COUNT(*) FROM members WHERE role = 'MEMBER'") fun getMemberCount(): Flow<Int> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertMember(member: Member): Long @Update suspend fun updateMember(member: Member) }

Penjelasan: Interface MemberDao mendefinisikan seluruh operasi akses database untuk tabel members. Anotasi @Dao memberi tahu Room untuk men-generate implementasi SQL secara otomatis. Terdapat dua pola return type yang digunakan: fungsi yang mengembalikan Flow (seperti getAllMembers dan getMemberById) bersifat reaktif dan akan otomatis mengirim data terbaru setiap kali tabel berubah, sedangkan fungsi bertanda suspend (seperti login dan getMemberByIdOnce) hanya mengambil data sekali di dalam coroutine. Perhatikan juga fungsi getRememberedMember, clearAllRemembered, dan setRemembered yang secara khusus menangani mekanisme auto-login. Selain itu, terdapat juga TransactionDao yang menangani operasi CRUD untuk tabel transaksi.


6. TimezoneDatabase.kt - Room Database Singleton

package com.example.timezonemembership.data.database import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import com.example.timezonemembership.data.dao.MemberDao import com.example.timezonemembership.data.dao.TransactionDao import com.example.timezonemembership.data.model.Member import com.example.timezonemembership.data.model.Transaction @Database( entities = [Member::class, Transaction::class], version = 2, exportSchema = false ) abstract class TimezoneDatabase : RoomDatabase() { abstract fun memberDao(): MemberDao abstract fun transactionDao(): TransactionDao companion object { @Volatile private var INSTANCE: TimezoneDatabase? = null fun getInstance(context: Context): TimezoneDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, TimezoneDatabase::class.java, "timezone_membership_db" ) .fallbackToDestructiveMigration() .build() INSTANCE = instance instance } } } }

Penjelasan: Kelas TimezoneDatabase merupakan konfigurasi utama Room Database. Anotasi @Database mendaftarkan kedua entity (Member dan Transaction) ke dalam database ini. Pola Singleton diterapkan melalui companion object dengan anotasi @Volatile dan blok synchronized untuk memastikan hanya satu instance database yang aktif di seluruh aplikasi, sekaligus menjamin thread-safety ketika diakses dari berbagai coroutine secara bersamaan. Metode fallbackToDestructiveMigration() dipilih agar database direset otomatis jika terjadi perubahan skema antar versi, cocok untuk fase pengembangan dan demo.


7. TimezoneRepository.kt - Repository Layer

package com.example.timezonemembership.data.repository import com.example.timezonemembership.data.dao.MemberDao import com.example.timezonemembership.data.dao.TransactionDao import com.example.timezonemembership.data.model.Constants import com.example.timezonemembership.data.model.Member import com.example.timezonemembership.data.model.Transaction class TimezoneRepository( private val memberDao: MemberDao, private val transactionDao: TransactionDao ) { // ==================== AUTH OPERATIONS ==================== suspend fun login(email: String, password: String, rememberMe: Boolean): Member? { val member = memberDao.login(email, password) if (member != null) { if (rememberMe) { memberDao.clearAllRemembered() memberDao.setRemembered(member.id, true) return member.copy(isRemembered = true) } else { memberDao.clearAllRemembered() return member.copy(isRemembered = false) } } return null } // ==================== TOP-UP ==================== @androidx.room.Transaction suspend fun performTopUp(memberId: Int, tier: Constants.TopUpTier): Boolean { val member = memberDao.getMemberByIdOnce(memberId) ?: return false val updatedMember = member.copy( tizoBalance = member.tizoBalance + tier.tizoAmount, rewardPoints = member.rewardPoints + tier.rewardPoints ) memberDao.updateMember(updatedMember) val transaction = Transaction( memberId = memberId, transactionType = "TOPUP", description = "Top-Up ${tier.name} (Rp${formatCurrency(tier.priceRp)})", tizoMutation = tier.tizoAmount, pointMutation = tier.rewardPoints ) transactionDao.insertTransaction(transaction) return true } // Pola yang sama diterapkan untuk performPlay() dan performRedeem() // dengan validasi saldo/poin sebelum eksekusi transaksi. }

Penjelasan: Kelas TimezoneRepository berperan sebagai layer pemisah data (Data Layer) yang mengenkapsulasi seluruh logika akses database. ViewModel tidak perlu tahu detail teknis bagaimana data diambil atau dimanipulasi. Fungsi login mencocokkan email dan password, lalu menangani mekanisme "Remember Me" dengan membersihkan flag lama dan menyimpan flag baru. Fungsi performTopUp menggunakan anotasi @Transaction dari Room untuk memastikan dua operasi (update saldo member dan insert catatan transaksi) berjalan secara atomik. Artinya, jika salah satu operasi gagal, seluruh perubahan akan di-rollback sehingga data tetap konsisten. Pola yang sama diterapkan untuk operasi bermain (performPlay) dan penukaran hadiah (performRedeem), masing-masing dengan validasi kecukupan saldo atau poin.


8. MemberViewModel.kt - ViewModel

package com.example.timezonemembership.viewmodel import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.example.timezonemembership.data.database.TimezoneDatabase import com.example.timezonemembership.data.model.Constants import com.example.timezonemembership.data.model.Member import com.example.timezonemembership.data.repository.TimezoneRepository import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch class MemberViewModel(application: Application) : AndroidViewModel(application) { private val repository: TimezoneRepository private val _currentUser = MutableStateFlow<Member?>(null) val currentUser: StateFlow<Member?> = _currentUser.asStateFlow() private val _isCheckingAuth = MutableStateFlow(true) val isCheckingAuth: StateFlow<Boolean> = _isCheckingAuth.asStateFlow() val allMembers: StateFlow<List<Member>> val memberCount: StateFlow<Int> private val _uiMessage = MutableStateFlow<String?>(null) val uiMessage: StateFlow<String?> = _uiMessage.asStateFlow() private val _authError = MutableStateFlow<String?>(null) val authError: StateFlow<String?> = _authError.asStateFlow() init { val database = TimezoneDatabase.getInstance(application) repository = TimezoneRepository(database.memberDao(), database.transactionDao()) allMembers = repository.getAllMembersByRole("MEMBER") .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) memberCount = repository.getMemberCount() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0) checkRememberedUser() } fun login(email: String, password: String, rememberMe: Boolean) { viewModelScope.launch { _authError.value = null val member = repository.login(email, password, rememberMe) if (member != null) { _currentUser.value = member _uiMessage.value = "Login berhasil! Selamat datang, ${member.name}." } else { _authError.value = "Email atau password salah!" } } } fun performTopUp(memberId: Int, tier: Constants.TopUpTier) { viewModelScope.launch { val success = repository.performTopUp(memberId, tier) if (success) { _uiMessage.value = "Top-Up ${tier.name} berhasil! +${tier.tizoAmount} Tizo" // Refresh currentUser jika yang di-update adalah user yang sedang login if (_currentUser.value?.id == memberId) { val updated = repository.getMemberByEmail(_currentUser.value!!.email) _currentUser.value = updated } } else { _uiMessage.value = "Top-Up gagal. Silakan coba lagi." } } } // Fungsi serupa: register(), logout(), performPlay(), performRedeem() }

Penjelasan: MemberViewModel menjadi penghubung utama antara data dan tampilan. Kelas ini meng-extend AndroidViewModel (bukan ViewModel biasa) karena membutuhkan Application context untuk menginisialisasi Room Database. Beberapa MutableStateFlow digunakan untuk menyimpan state UI yang berbeda: currentUser melacak pengguna yang sedang login, isCheckingAuth menandai proses pengecekan auto-login, uiMessage menyimpan pesan feedback, dan authError menyimpan error autentikasi. Flow dari repository dikonversi menjadi StateFlow menggunakan operator stateIn dengan SharingStarted.WhileSubscribed(5000), yang berarti Flow tetap aktif selama 5 detik setelah subscriber terakhir berhenti mengamati, mengoptimalkan penggunaan sumber daya saat rotasi layar. Setiap operasi bisnis dijalankan di dalam viewModelScope.launch agar berjalan secara asinkron tanpa memblokir thread UI utama.


9. NavGraph.kt - Navigasi Antar Halaman

package com.example.timezonemembership.ui.navigation import androidx.compose.runtime.* import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.* import androidx.navigation.compose.* import com.example.timezonemembership.ui.screen.* import com.example.timezonemembership.viewmodel.MemberViewModel import com.example.timezonemembership.viewmodel.TransactionViewModel object Routes { const val SPLASH = "splash" const val AUTH = "auth" const val ADMIN_DASHBOARD = "adminDashboard" const val ADD_MEMBER = "addMember" const val MEMBER_DETAIL = "memberDetail/{memberId}" const val TOP_UP = "topUp/{memberId}" const val PLAY = "play/{memberId}" const val REDEEM = "redeem/{memberId}" const val HISTORY = "history/{memberId}" fun memberDetail(memberId: Int) = "memberDetail/$memberId" fun topUp(memberId: Int) = "topUp/$memberId" fun play(memberId: Int) = "play/$memberId" fun redeem(memberId: Int) = "redeem/$memberId" fun history(memberId: Int) = "history/$memberId" } @Composable fun NavGraph() { val navController = rememberNavController() val memberViewModel: MemberViewModel = viewModel() val transactionViewModel: TransactionViewModel = viewModel() val currentUser by memberViewModel.uiState.collectAsState() val isCheckingAuth by memberViewModel.isCheckingAuth.collectAsState() // Auto-login: Navigasi otomatis berdasarkan status autentikasi LaunchedEffect(currentUser, isCheckingAuth) { if (!isCheckingAuth) { val user = currentUser if (user != null) { if (user.role == "ADMIN") { navController.navigate(Routes.ADMIN_DASHBOARD) { /* popUpTo ... */ } } else { navController.navigate(Routes.memberDetail(user.id)) { /* popUpTo ... */ } } } else { navController.navigate(Routes.AUTH) { popUpTo(0) { inclusive = true } } } } } NavHost(navController = navController, startDestination = Routes.SPLASH) { composable(Routes.AUTH) { AuthScreen(viewModel = memberViewModel, onAuthSuccess = { role, id -> val dest = if (role == "ADMIN") Routes.ADMIN_DASHBOARD else Routes.memberDetail(id) navController.navigate(dest) { popUpTo(Routes.AUTH) { inclusive = true } } }) } composable(Routes.ADMIN_DASHBOARD) { HomeScreen(viewModel = memberViewModel, onAddMember = { navController.navigate(Routes.ADD_MEMBER) }, onMemberClick = { id -> navController.navigate(Routes.memberDetail(id)) }, onLogout = { navController.navigate(Routes.AUTH) { /* popUpTo ... */ } } ) } composable(Routes.MEMBER_DETAIL, arguments = listOf(navArgument("memberId") { type = NavType.IntType }) ) { backStackEntry -> val memberId = backStackEntry.arguments?.getInt("memberId") ?: return@composable MemberDetailScreen(viewModel = memberViewModel, memberId = memberId, onTopUp = { navController.navigate(Routes.topUp(memberId)) }, onPlay = { navController.navigate(Routes.play(memberId)) }, onRedeem = { navController.navigate(Routes.redeem(memberId)) }, onHistory = { navController.navigate(Routes.history(memberId)) }, onNavigateBack = { navController.popBackStack() }, onLogout = { navController.navigate(Routes.AUTH) { /* popUpTo ... */ } } ) } // Route serupa untuk TopUp, Play, Redeem, dan History screen } }

Penjelasan: Kita mengontrol alur perpindahan halaman menggunakan library Jetpack Navigation. Objek Routes menyimpan seluruh definisi rute sebagai konstanta string, termasuk rute dengan parameter dinamis seperti "memberDetail/{memberId}". Fungsi helper seperti Routes.memberDetail(id) memudahkan pembuatan rute dengan parameter. Bagian paling menarik adalah blok LaunchedEffect yang memantau perubahan status autentikasi secara reaktif: jika pengguna yang diingat ditemukan di database, navigasi otomatis dialihkan ke dashboard (untuk Admin) atau halaman profil (untuk Member) tanpa perlu login ulang. Setiap rute dengan parameter {memberId} didefinisikan menggunakan navArgument bertipe IntType, dan nilai parameter tersebut diekstrak dari backStackEntry.arguments untuk diteruskan ke screen terkait.


10. Halaman Login - AuthScreen.kt

@Composable private fun LoginPage( viewModel: MemberViewModel, registrationSuccess: Boolean, onDismissSuccess: () -> Unit, onNavigateToRegister: () -> Unit ) { var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } var rememberMe by remember { mutableStateOf(false) } var emailError by remember { mutableStateOf(false) } var passwordError by remember { mutableStateOf(false) } val authError by viewModel.authError.collectAsState() Box(modifier = Modifier.fillMaxSize().background(LightBackground)) { Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { // Brand Header Text(text = "TIMEZONE", style = MaterialTheme.typography.displayMedium, color = BluePrimary, fontWeight = FontWeight.Black, letterSpacing = 3.sp) Text(text = "Membership & Loyalty", color = OnLightSurfaceVariant) // Login Form Card Card(shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors(containerColor = LightCard)) { Column(modifier = Modifier.padding(24.dp)) { Text(text = "Masuk ke Akun", fontWeight = FontWeight.Bold) // Email Field OutlinedTextField(value = email, onValueChange = { email = it; emailError = false }, leadingIcon = { Icon(Icons.Default.Email, tint = BluePrimary) }, isError = emailError, shape = RoundedCornerShape(12.dp)) // Password Field OutlinedTextField(value = password, onValueChange = { password = it; passwordError = false }, leadingIcon = { Icon(Icons.Default.Lock, tint = BluePrimary) }, isError = passwordError, visualTransformation = PasswordVisualTransformation()) // Remember Me Checkbox Row { Checkbox(checked = rememberMe, onCheckedChange = { rememberMe = it }) Text(text = "Ingat Akun Saya") } // Login Button Button(onClick = { emailError = email.isBlank() passwordError = password.isBlank() if (!emailError && !passwordError) { viewModel.login(email.trim(), password, rememberMe) } }, colors = ButtonDefaults.buttonColors(containerColor = BluePrimary) ) { Text("Masuk", fontWeight = FontWeight.Bold) } } } // Link ke halaman registrasi TextButton(onClick = onNavigateToRegister) { Text("Daftar Sekarang", color = BluePrimary, fontWeight = FontWeight.Bold) } } } }

Penjelasan: Composable LoginPage merupakan bagian dari AuthScreen yang menangani alur masuk pengguna. State lokal seperti email, password, rememberMe, dan flag error dikelola menggunakan remember dan mutableStateOf agar Compose dapat melakukan recomposition secara efisien ketika nilainya berubah. Error autentikasi dari ViewModel diamati melalui collectAsState() untuk menampilkan pesan kesalahan secara reaktif. Saat tombol "Masuk" ditekan, validasi sederhana dilakukan terlebih dahulu (memeriksa apakah field kosong), kemudian fungsi viewModel.login() dipanggil. Halaman registrasi menggunakan pola yang serupa namun dengan validasi yang lebih ketat meliputi format email via regex, nomor telepon harus diawali "08" atau "+62", password minimal 6 karakter, dan pencocokan konfirmasi password.


11. Kartu Member Digital - MemberDetailScreen.kt

// ==================== MEMBER CARD ==================== Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(24.dp), colors = CardDefaults.cardColors(containerColor = BluePrimaryDark), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) ) { Box( modifier = Modifier.fillMaxWidth().background( Brush.verticalGradient( colors = listOf(BluePrimaryDark, BluePrimary, BluePrimaryLight) ) ) ) { Column(modifier = Modifier.fillMaxWidth().padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text(text = "TIMEZONE", color = GoldSecondary, fontWeight = FontWeight.Bold, letterSpacing = 2.sp) Text(text = if (m.role == "ADMIN") "ADMIN CARD" else "MEMBERSHIP CARD", color = LightSurface.copy(alpha = 0.7f), letterSpacing = 4.sp) // Avatar Inisial Box(modifier = Modifier.size(80.dp).clip(CircleShape) .background(LightSurface.copy(alpha = 0.15f)), contentAlignment = Alignment.Center ) { Text(text = m.name.take(1).uppercase(), color = LightSurface, style = MaterialTheme.typography.displayMedium, fontWeight = FontWeight.Bold) } Text(text = m.name, color = LightSurface, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineMedium) Row { Icon(Icons.Default.Email, tint = LightSurface.copy(alpha = 0.7f)) Text(text = m.email, color = LightSurface.copy(alpha = 0.7f)) } Row { Icon(Icons.Default.Phone, tint = LightSurface.copy(alpha = 0.7f)) Text(text = m.phone, color = LightSurface.copy(alpha = 0.7f)) } // QR Placeholder Box(modifier = Modifier.size(100.dp).clip(RoundedCornerShape(12.dp)) .background(LightSurface.copy(alpha = 0.15f)) ) { Text(text = "QR", color = LightSurface, fontWeight = FontWeight.Black) Text(text = "ID: ${m.id}", color = LightSurface.copy(alpha = 0.7f)) } } } }

Penjelasan: Cuplikan ini menunjukkan bagian kartu keanggotaan digital yang ditampilkan di MemberDetailScreen. Desain kartu menggunakan Brush.verticalGradient dengan tiga warna biru (gelap ke terang) untuk menciptakan efek gradient yang menyerupai kartu fisik. Di bagian atas, terdapat teks branding "TIMEZONE" dengan warna emas (GoldSecondary) dan label "MEMBERSHIP CARD" atau "ADMIN CARD" yang menyesuaikan role pengguna. Avatar berbentuk lingkaran menampilkan inisial huruf pertama nama member menggunakan m.name.take(1).uppercase(). Informasi kontak (email dan nomor telepon) ditampilkan dengan opacity rendah (alpha = 0.7f) agar tidak terlalu dominan. Di bagian bawah terdapat placeholder QR code yang menampilkan ID member. Selain kartu ini, halaman juga menampilkan dua card saldo (Tizo dan Reward Points) serta empat tombol menu layanan (Top-Up, Bermain, Redeem, Riwayat).


12. Activity Utama Aplikasi - MainActivity.kt

package com.example.timezonemembership import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import com.example.timezonemembership.ui.navigation.NavGraph import com.example.timezonemembership.ui.theme.TimezoneMembershipTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { TimezoneMembershipTheme { NavGraph() } } } }

Penjelasan: MainActivity bertindak sebagai entry point sistem operasi Android untuk menjalankan aplikasi. Fungsi enableEdgeToEdge() dipasang untuk membuat area status bar dan navigation bar menjadi transparan, sehingga tampilan UI terasa penuh dan modern. Di dalam blok setContent, seluruh UI dibungkus dalam TimezoneMembershipTheme yang mengaplikasikan palet warna light theme kustom (Royal Blue, Gold, Teal) dan konfigurasi typography ke seluruh komponen Compose. Komponen NavGraph() kemudian mengambil alih pengelolaan navigasi antar seluruh halaman aplikasi.


Screenshot 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 ...