CoffeeBliss Loyalty Membership App - EAS Pemrograman Perangkat Bergerak B

Anggota Kelompok:

Triana Velia Hutabalian - 5025231190

Rafaela Shyra Ashma' Ramadhani - 5025231217

Project EAS Pemrograman Perangkat Bergerak B

 

Source Code: GitHub

App: Google Drive

Hasil Aplikasi:

 

Penjelasan: 

Aplikasi Coffee Bliss adalah sistem kartu member digital untuk sebuah coffee shop yang menggantikan kartu fisik dengan solusi berbasis Android. Seperti yang dijelaskan di materi dosen, project ini mengimplementasikan Room Database sebagai solusi penyimpanan data lokal yang berperan sebagai abstraction layer di atas SQLite. Seluruh arsitektur dibangun menggunakan pola MVVM (Model-View-ViewModel) dengan Kotlin, Jetpack Compose untuk UI, dan Navigation Compose untuk perpindahan antar layar. Pelanggan dapat mendaftar sebagai member, mengumpulkan poin dari setiap transaksi, melihat kartu member digital, serta menukarkan poin dengan berbagai hadiah minuman.
 
Member.kt & Transaction.kt — Entity Room Database
Kedua file ini adalah entity Room yang merepresentasikan struktur tabel di dalam database SQLite. Anotasi @Entity(tableName = "members") pada Member.kt memberitahu Room untuk membuat tabel bernama "members", sementara @Entity(tableName = "transactions") pada Transaction.kt membuat tabel "transactions". Setiap properti dalam data class ini menjadi kolom di tabel. Kolom id pada keduanya diberi anotasi @PrimaryKey(autoGenerate = true) yang berarti Room akan otomatis mengisi nilai id secara berurutan tanpa perlu diatur manual. Member menyimpan data nama, email, nomor HP, password untuk autentikasi, dan total poin yang terakumulasi. Transaction menyimpan referensi ke member melalui memberId, nominal transaksi, poin yang diperoleh, tanggal, jenis transaksi (purchase atau redeem), serta catatan tambahan yang digunakan untuk menyimpan nama reward saat penukaran poin.
 
MemberDao.kt & TransactionDao.kt — Data Access Object
DAO adalah antarmuka yang berisi semua operasi database yang bisa dilakukan terhadap masing-masing tabel. MemberDao memiliki beragam fungsi: getAllMembers() menggunakan @Query dengan SQL SELECT * FROM members dan mengembalikan Flow<List<Member>> sehingga UI otomatis diperbarui setiap ada perubahan data. Fungsi findByCredentials() digunakan untuk autentikasi login dengan mencocokkan email dan password. Fungsi addPoints() dan deductPoints() menggunakan query UPDATE langsung ke kolom poin tanpa perlu load seluruh objek Member, sehingga lebih efisien. Semua fungsi yang mengubah data menggunakan kata kunci suspend karena dijalankan secara asinkron melalui coroutine agar tidak membekukan tampilan. TransactionDao lebih sederhana dengan fungsi insertTransaction() untuk menyimpan transaksi baru, dan getTransactionsForMember() yang mengembalikan flow transaksi terurut dari terbaru berdasarkan memberId.
 
AppDatabase.kt — Konfigurasi Room Database
File ini adalah inti dari konfigurasi Room, sesuai dengan penjelasan di materi dosen. Anotasi @Database(entities = [Member::class, Transaction::class], version = 3, exportSchema = false) mendeklarasikan bahwa database memiliki dua tabel dan berada di versi 3. Class ini bersifat abstract dan mewarisi RoomDatabase, serta memiliki dua fungsi abstrak yang menjadi jembatan akses ke masing-masing DAO. Di dalamnya terdapat pola Singleton menggunakan companion object yang memastikan hanya ada satu instance database selama aplikasi berjalan. Anotasi @Volatile memastikan perubahan pada variabel Instance langsung terlihat oleh semua thread, dan blok synchronized mencegah pembuatan dua instance sekaligus saat beberapa thread mengakses database bersamaan. Opsi fallbackToDestructiveMigration() digunakan agar jika terjadi perubahan versi database, data lama dihapus dan schema baru dibuat ulang, berguna selama fase pengembangan.
 
SessionManager.kt — Manajemen Sesi Login
File ini bertanggung jawab menyimpan dan mengelola status login pengguna menggunakan SharedPreferences. Ketika member berhasil login, saveSession(memberId) menyimpan ID member ke SharedPreferences dengan key KEY_MEMBER_ID. Saat aplikasi dibuka kembali, getMemberId() membaca nilai tersebut, jika bernilai -1, berarti tidak ada sesi aktif dan pengguna harus login. Fungsi isLoggedIn() adalah shortcut untuk mengecek apakah sesi ada. Fungsi logout() menghapus seluruh data di SharedPreferences menggunakan clear().apply(). Pendekatan ini memungkinkan pengguna tetap terlogin meski aplikasi ditutup, tanpa perlu login ulang setiap saat.
 
CoffeeRepository.kt — Single Source of Truth
Repository berperan sebagai jembatan antara ViewModel dan sumber data (Room Database), sekaligus menjadi single source of truth untuk semua operasi data. Fungsi loginMember() memanggil DAO untuk mencari member berdasarkan email dan password yang sudah dinormalisasi ke lowercase. Fungsi insertTransaction() melakukan dua operasi sekaligus: menyimpan transaksi baru dan menambah poin ke member yang bersangkutan melalui memberDao.addPoints(), memastikan keduanya selalu konsisten. Fungsi redeemReward() bekerja sebaliknya, mengurangi poin member dan menyimpan transaksi bertipe "redeem" dengan nilai pointEarned negatif dan nama reward di kolom note. Pemisahan logika di Repository ini membuat ViewModel tidak perlu tahu detail implementasi database, sehingga kode lebih bersih dan mudah diuji.
 
CoffeeViewModel.kt — ViewModel dengan StateFlow
ViewModel bertugas sebagai jembatan antara Repository dan UI, mengelola semua logika bisnis dan state aplikasi. Class ini menerima CoffeeRepository dan SessionManager sebagai dependency melalui CoffeeViewModelFactory, ini adalah implementasi pola Factory untuk ViewModel yang membutuhkan parameter konstruktor. allMembers adalah Flow<List<Member>> yang langsung mengalir dari Repository ke UI. StateFlow _loggedInMemberId menyimpan ID member yang sedang aktif dan di-expose sebagai StateFlow yang immutable ke UI. Fungsi validateSession() adalah fungsi suspend yang mengecek apakah session yang tersimpan masih valid di database, ini penting karena setelah fallbackToDestructiveMigration(), session lama bisa merujuk ke ID yang sudah tidak ada. Fungsi loginMember() dan registerMember() mengembalikan Int sebagai result code: nilai positif berarti sukses (ID member), -1 berarti gagal (email salah atau sudah terdaftar), dan -2 berarti error lain. Setiap operasi write dijalankan di dalam viewModelScope.launch untuk memastikan coroutine dibatalkan otomatis saat ViewModel dihancurkan.
 
Screen.kt — Definisi Rute Navigasi
File ini mendefinisikan semua rute navigasi menggunakan sealed class Screen. Sealed class dipilih agar semua rute terdefinisi di satu tempat dan compiler bisa memvalidasi kelengkapannya. Rute yang membutuhkan parameter (seperti MemberDetail, MemberCard, Transaction, Rewards, History, Profile) menggunakan template {memberId} di dalam route string, dan menyediakan fungsi createRoute(memberId: Int) untuk membangun URL navigasi yang lengkap. Pendekatan ini membuat navigasi antar screen yang memerlukan data member ID menjadi type-safe dan terstruktur rapi.
 
Color.kt & Theme.kt & Type.kt — Tema Aplikasi
Ketiga file ini membentuk design system aplikasi Coffee Bliss. Color.kt mendefinisikan palet warna bertema kopi yang konsisten: EspressoDark (#25160E) sebagai warna utama yang gelap seperti espresso, GoldBorder (#E9C349) sebagai aksen gold untuk elemen penting, MochaCream (#FBF9F7) sebagai warna background, dan berbagai varian warna untuk text, divider, dan status level member (Silver, Gold, Platinum). File ini juga menyediakan legacy aliases agar kode lama yang masih referensi nama warna lama tetap bisa dikompilasi. Theme.kt menggunakan lightColorScheme dari Material 3 untuk memetakan semua warna custom ke slot warna Material Design, lalu membungkusnya dalam CoffeeBlissTheme composable yang diterapkan ke seluruh aplikasi dari MainActivity. Type.kt menyediakan konfigurasi tipografi dasar menggunakan Typography dari Material 3.
 
SplashScreen.kt — Layar Pembuka dengan Animasi
SplashScreen adalah layar pertama yang muncul saat aplikasi dibuka, menampilkan logo dan nama aplikasi dengan animasi yang menarik. Digunakan beberapa Animatable untuk mengontrol alpha, scale, dan posisi elemen secara terpisah, sehingga setiap elemen muncul dengan timing yang berbeda, logo muncul pertama dengan efek spring bounce, kemudian nama brand slide up, dan terakhir loading indicator muncul. rememberInfiniteTransition digunakan untuk animasi pulsing pada ring luar logo yang berjalan terus-menerus. Di dalam LaunchedEffect(Unit), setelah animasi selesai, fungsi viewModel.validateSession() dipanggil untuk mengecek apakah ada sesi login yang valid. Jika ada, langsung navigasi ke MemberDetail; jika tidak, navigasi ke Login. Navigasi dilakukan dengan popUpTo(Screen.Splash.route) { inclusive = true } agar layar splash dihapus dari back stack sehingga pengguna tidak bisa kembali ke sana.
 
LoginScreen.kt & RegisterScreen.kt — Autentikasi Member
LoginScreen menampilkan form login dengan dua field: email dan password dengan toggle visibility. Saat tombol login ditekan, fungsi viewModel.loginMember() dipanggil menggunakan scope.launch dari rememberCoroutineScope() karena ini adalah suspend function yang dipanggil dari UI. Jika berhasil, navigasi ke MemberDetail dengan menghapus Login dari back stack menggunakan popUpTo(Screen.Login.route) { inclusive = true }. RegisterScreen memiliki alur yang lebih panjang dengan validasi: nama minimal 2 karakter, format email valid, password minimal 6 karakter, dan konfirmasi password harus cocok. Validasi dilakukan secara real-time menggunakan mutableStateOf yang bereaksi setiap kali input berubah. Kedua screen menggunakan fungsi helper inputColors() yang dikondisikan untuk tema dark (input berteks putih di atas background gelap). Jika registrasi berhasil, langsung auto-login dan navigasi ke MemberDetail.
 
HomeScreen.kt — Dashboard Admin
HomeScreen berfungsi sebagai dashboard manajemen member dari sisi admin coffee shop. Data semua member diambil melalui viewModel.allMembers.collectAsState() yang secara otomatis memperbarui tampilan saat ada member baru atau data berubah. Banner hero menampilkan total member aktif beserta distribusi level (Platinum, Gold, Silver) yang dihitung real-time dari list member menggunakan fungsi count{}. NotificationsDialog menampilkan member terbaru dan member yang hampir naik level. MemberListItem menampilkan setiap member dengan avatar berwarna yang ditentukan secara deterministik dari hash nama member, sehingga warna avatar konsisten setiap kali. FAB (Floating Action Button) mengarahkan ke AddMemberScreen untuk menambah member baru.
 
AddMemberScreen.kt — Formulir Tambah Member
Screen ini menyediakan form pendaftaran member baru dengan validasi langsung pada setiap field. Validasi dilakukan menggunakan computed values: nameError bernilai true jika nama sudah diketik namun kurang dari 2 karakter, emailError menggunakan android.util.Patterns.EMAIL_ADDRESS untuk validasi format email, dan phoneError mengecek panjang minimal 8 digit. formValid bernilai true hanya jika semua field terisi dan tidak ada error, sehingga tombol daftar baru aktif ketika form valid. Komponen FormField adalah composable private yang menerima state dan callback sebagai parameter, mengimplementasikan prinsip reusability, field ini dipakai berulang untuk ketiga input dengan konfigurasi berbeda melalui KeyboardOptions.
 
MemberDetailScreen.kt — Dashboard Member
Screen ini adalah halaman utama yang dilihat member setelah login, menampilkan semua informasi dan aksi yang tersedia. Fungsi-fungsi helper getMemberLevel() dan getLevelColor() didefinisikan di sini sebagai top-level functions sehingga bisa digunakan oleh screen lain (HistoryScreen, RewardScreen, dll.). Mini member card yang bisa diklik mengarahkan ke MemberCardScreen. Bento grid stats menampilkan total poin, level, reward tersedia, dan jumlah transaksi dalam layout 2x2. LazyColumn digunakan agar performa tetap baik meski konten panjang. ModalBottomSheet digunakan untuk menampilkan notifikasi personal yang dibangun secara kontekstual dari data member, berisi progress menuju level berikutnya, reward yang bisa ditukar, dan transaksi terakhir.
 
MemberCardScreen.kt — Kartu Member Digital
Screen ini menampilkan kartu member digital lengkap yang bisa ditunjukkan ke kasir. DigitalMemberCard adalah composable yang merender kartu dengan gradient background menggunakan Brush.linearGradient, border gold, nama member, nomor ID yang diformat seperti kartu kredit, badge level, dan QR code placeholder menggunakan Icons.Default.QrCode2. Data member dan transaksi diambil via viewModel.getMember() dan viewModel.getTransactions() yang masing-masing mengembalikan Flow dan di-collect menggunakan collectAsState(). Stats di bawah kartu menampilkan jumlah reward yang bisa ditukar (dihitung dengan count { member.points >= it }) dan total kunjungan dari jumlah transaksi. Kartu tier progress di bagian bawah menjelaskan syarat poin untuk setiap level (Silver, Gold, Platinum) dengan indikator "KAMU" pada level aktif.
 
TransactionScreen.kt — Tambah Transaksi
Screen ini digunakan untuk mencatat transaksi baru. Input nominal difilter hanya menerima angka digit menggunakan it.filter { c -> c.isDigit() }. Kalkulasi poin dilakukan secara real-time: pointsToEarn = amount?.let { (it / 10000).toInt() } ?: 0, menerapkan aturan bisnis Rp 10.000 = 1 poin. UI menampilkan estimasi poin yang akan didapat secara langsung saat pengguna mengetik nominal. Fitur pilihan metode pembayaran (Cash, Digital Wallet, Kartu Kredit, Bliss Balance) ditampilkan dalam chip grid 2 kolom menggunakan chunked(2). Setelah transaksi berhasil disimpan via viewModel.addTransaction(), PointUpdatedDialog muncul menampilkan poin yang baru didapat dan total poin terkini, dengan opsi langsung ke halaman reward.
 
HistoryScreen.kt — Riwayat Transaksi
Screen ini menampilkan seluruh riwayat transaksi member yang dikelompokkan berdasarkan tanggal. Pengelompokan dilakukan menggunakan groupBy pada tanggal yang diambil dari bagian pertama string tanggal sebelum koma. Setiap transaksi ditampilkan dalam HistoryTransactionCard yang secara visual membedakan transaksi pembelian (warna espresso) dengan penukaran reward (warna oranye) menggunakan kondisi isRedeem = tx.type == "redeem". Ketuk pada kartu transaksi menampilkan ModalBottomSheet berisi TransactionDetailSheet yang menampilkan detail lengkap termasuk nomor resi, tanggal, waktu, metode bayar, dan poin yang terlibat. Nomor resi diformat menggunakan tx.id.toString().padStart(6, '0') agar selalu 6 digit.
 
RewardScreen.kt — Tukar Poin dengan Reward
Screen ini menampilkan katalog reward yang tersedia beserta status apakah member bisa menukarnya. Daftar reward didefinisikan sebagai list konstanta berisi tiga item: Espresso (50 poin), Cappuccino (100 poin), dan Cafe Latte (150 poin). StitchRewardCard merender setiap reward dengan tampilan berbeda tergantung canAfford, jika cukup poin, tombol "Redeem" aktif dengan warna espresso; jika tidak, tombol diganti dengan indikator "Terkunci" berwarna abu-abu. Saat tombol ditekan, ConfirmRedeemDialog muncul menampilkan sisa poin setelah penukaran agar pengguna bisa mempertimbangkan. Jika dikonfirmasi, viewModel.redeemReward() dipanggil yang akan mengurangi poin dan mencatat transaksi redeem, lalu RedeemSuccessDialog muncul sebagai feedback positif. Fitur bonus "Refer a Friend" menggunakan Intent.ACTION_SEND untuk berbagi kode referral member ke aplikasi lain.
 
ProfileScreen.kt — Profil Member
ProfileScreen menampilkan informasi lengkap member beserta progress menuju level berikutnya. Avatar bulat dengan inisial nama menggunakan warna yang dipilih deterministik dari palette menggunakan modulo hash nama, sama seperti di HomeScreen, memastikan konsistensi warna avatar di seluruh aplikasi. LinearProgressIndicator menampilkan progress visual menuju level berikutnya, dengan kalkulasi berbeda tergantung level saat ini, untuk Silver menuju Gold progress dihitung points/100f, untuk Gold menuju Platinum dihitung (points-100)/(300-100)f. Card digital member shortcut memungkinkan navigasi cepat ke MemberCardScreen. Tombol logout menampilkan AlertDialog konfirmasi sebelum memanggil viewModel.logout() dan navigasi kembali ke Login dengan popUpTo(0) { inclusive = true } untuk membersihkan seluruh back stack.
 
BottomNavBar.kt — Navigasi Bawah Animasi
File ini mengimplementasikan bottom navigation bar dengan animasi yang halus. Enum class MemberTab mendefinisikan lima tab yang tersedia. Setiap item navigasi menggunakan animateColorAsState untuk transisi warna ikon dan background secara smooth, serta animateDpAsState dengan spring(dampingRatio = DampingRatioMediumBouncy) untuk efek pill yang melebar/menyempit saat tab aktif berpindah, memberikan feedback visual yang responsif. Navigasi menggunakan launchSingleTop = true untuk mencegah duplikasi screen di back stack saat tab yang sama diklik berulang. Klik menggunakan MutableInteractionSource dengan indication = null untuk menghilangkan efek ripple default, digantikan oleh animasi pill yang lebih elegan.
 
Kesimpulan
Melalui project Coffee Bliss ini, implementasi Room Database sebagai solusi penyimpanan lokal dapat dipahami secara menyeluruh. Room menyederhanakan pengelolaan SQLite dengan menyediakan abstraksi melalui Entity, DAO, dan Database class, sehingga operasi CRUD dapat dilakukan tanpa menulis SQL secara manual (kecuali untuk query khusus). Arsitektur MVVM memisahkan tanggung jawab dengan jelas: Entity dan DAO mengurus data mentah, Repository menjadi jembatan dan titik koordinasi logika bisnis, ViewModel mengelola state dan expose data ke UI melalui Flow dan StateFlow, sementara Composable Screen hanya bertugas menampilkan apa yang diberikan oleh ViewModel. Jetpack Compose dengan Navigation Compose membuat pembuatan UI yang reaktif dan navigasi multi-screen menjadi lebih terstruktur. SessionManager dengan SharedPreferences melengkapi sistem dengan kemampuan menyimpan state login antar sesi aplikasi, menjadikan keseluruhan aplikasi terasa seperti produk membership digital yang lengkap dan fungsional.

 

Kumpulan Link Lainnya:

File PPT : Google Drive

Video Demo : Google Drive

Deskripsi Aplikasi & Fitur : Google Drive

Infografis : Google Drive 

Komentar

Postingan populer dari blog ini

Komponen Button (Aplikasi Dice Roller)

Daily Task Manager - ETS Pemrograman Perangkat Bergerak B