BankingApp
Modular Clean Architecture Starter
Base project Android modern untuk membangun aplikasi perbankan dengan
arsitektur modular, Jetpack Compose, MVVM + Clean Architecture, dan
Hilt
Dokumentasi Teknis
Version 2.0 - 2025
Rilis v2 (2.0)
Ringkasan perubahan utama dari dd6473d → 3cdc6cc:
- Registrasi + OTP (request & verify) di feature-auth.
- Screen Result Passing untuk mengirim hasil antar layar dengan aman.
- Deep Linking & Notification Actions (navigasi dari tautan/deeplink dan aksi notifikasi).
- Fitur Notifikasi end-to-end: permission, FCM token registration, daftar notifikasi dengan pull-to-refresh.
- Integrasi Firebase Cloud Messaging (FCM).
- Caching berbasis Room + pembersihan otomatis cache.
- Penanganan Session Expiration terpusat.
- Refactor BaseViewModel + RetryManager + Global Snackbar & Bottom Sheet error.
- Dokumentasi ONBOARDING.md dan CONTRIBUTING.md diperbarui.
Panduan Migrasi Singkat dari v1
- Turunkan ViewModel dari BaseViewModel untuk loading/error standar.
- Gunakan RetryManager untuk operasi yang dapat diulang (network).
- Manfaatkan cache Room di repository untuk data yang sering diakses.
- Daftarkan token FCM saat login/refresh (lihat feature-notification).
- Untuk navigasi yang mengembalikan nilai, gunakan Screen Result Passing.
1. Fitur Utama
-
Autentikasi (Login) dengan validasi domain dan
penanganan error yang rapi
-
Dashboard (kerangka), Transfer, Gold, Loan,
Payment, Profile, Support (terstruktur per-feature)
-
Splash → Routing awal berdasarkan status sesi
(token) ke Login atau Dashboard
-
Profil: menampilkan header profil dan Logout yang
benar-benar menghapus sesi
-
Loading Global dengan reference counter (tidak
gampang "nyangkut")
-
Internationalization (i18n): semua string UI di
strings.xml; ViewModel memakai StringProvider
-
Navigasi Compose yang dipusatkan (Navigator +
Destination), aman untuk modular
-
Design System: palet warna, tipografi, dan komponen
re-usable di lib-designsystem
- Registrasi + OTP (request & verify) terintegrasi di auth
- Notifikasi in-app dengan FCM (permission, token registration, list + pull-to-refresh)
- Deep linking & aksi notifikasi yang menavigasi ke layar terkait
- Global Snackbar untuk notifikasi in-app & Bottom Sheet untuk error fatal
- Caching Room dengan auto-clearing untuk data basi
- Session expiration handling terpusat yang konsisten di seluruh fitur
2. Teknologi dan Prinsip
- Kotlin, Coroutines + Flow
-
Jetpack Compose (Material 3), collectAsStateWithLifecycle untuk
lifecycle-aware
- Hilt (Dependency Injection)
- MVVM + Clean Architecture
- Modularization per feature + core + library
- Coil untuk pemuatan gambar (contoh di Profile)
-
Result/Failure sebagai kontrak domain untuk error yang kuat secara
tipe
- Room untuk caching data
- Firebase Cloud Messaging (FCM) untuk push notification
3. Arsitektur: MVVM + Clean
Lapisan Utama
📱 Presentation (Feature Module)
UI Compose, ViewModel, pemetaan domain → UI, event satu-kali
(one-off), i18n via StringProvider
🎯 Domain (core-domain + domain di tiap feature)
UseCase, Entity/Model domain, kontrak Repository, Result dan
DomainFailure
💾 Data (di feature dan/atau core)
Implementasi Repository (network/local/security), mapper DTO ↔
domain
🔧 Platform/Library
core-ui (LoadingManager, StringProvider), lib-designsystem
(tema/komponen), lib-navigation (Navigator/Destination)
Aliran Data
Contoh submit form:
UI → ViewModel → UseCase (domain) → Repository →
Result dikembalikan → ViewModel map ke state/error → UI render
4. Struktur Proyek
├── app
├── core
│ ├── core-domain
│ ├── core-ui
│ ├── core-network
│ ├── core-database
│ ├── core-security
│ ├── core-analytics
│ └── core-testing
├── lib
│ ├── lib-designsystem
│ ├── lib-navigation
│ ├── lib-storage
│ └── lib-notification
└── feature-*
├── feature-auth
├── feature-dashboard
├── feature-transfer
├── feature-gold
├── feature-loan
├── feature-payment
├── feature-profile
├── feature-support
└── feature-notification
Struktur dalam Satu Feature (Auth)
co.id.why.feature.auth
├── domain/ (UseCase, entity, failure, kontrak Repository)
├── data/ (RepositoryImpl, DTO, mapper)
├── login/ (UI Compose + ViewModel + mapper UI)
└── navigation/ (route, destination, graph)
5. Komponen Penting
5.1 Result dan DomainFailure
Result adalah sealed class untuk keberhasilan/kegagalan:
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(
val message: String = "",
val code: Int? = null,
val failure: DomainFailure? = null
) : Result<Nothing>()
}
DomainFailure adalah marker interface untuk kegagalan domain yang
bertipe kuat.
5.2 Validasi dan Use Case
LoginUseCase menaruh aturan domain:
class LoginUseCase(
private val repo: AuthRepository,
dispatcher: CoroutineDispatcher
) : UseCase<LoginUseCase.Param, User>(dispatcher) {
data class Param(val username: String, val password: String)
override suspend fun execute(param: Param): Result<User> {
val username = param.username.trim()
val password = param.password
if (username.isBlank())
return Result.Error(failure = LoginFailure.UsernameEmpty)
if (password.isBlank())
return Result.Error(failure = LoginFailure.PasswordEmpty)
val usernameValid = username.length >= 3
val pwdValid = password.length >= 6
if (!usernameValid)
return Result.Error(failure = LoginFailure.UsernameTooShort)
if (!pwdValid)
return Result.Error(failure = LoginFailure.PasswordTooShort)
return repo.login(username, password)
}
}
Keuntungan: Domain bebas dari string UI/i18n;
ViewModel yang memetakan kegagalan ke pesan UI.
5.3 ViewModel dan Event Satu-Kali
LoginViewModel mengelola FormState + FormErrorState:
private val _events = MutableSharedFlow<LoginEvent>(replay = 1)
val events: SharedFlow<LoginEvent> = _events
// on success:
_events.emit(LoginEvent.LoginSuccess(user))
5.4 i18n: StringProvider
Abstraksi untuk mengambil string resource di ViewModel:
interface StringProvider {
fun get(id: Int): String
}
class AndroidStringProvider @Inject constructor(
@ApplicationContext ctx: Context
) : StringProvider {
override fun get(id: Int) = ctx.getString(id)
}
5.5 Loading Global
LoadingManager menggunakan reference counter:
fun show() { /* ++counter → isLoading true */ }
fun hide() { /* --counter → isLoading false jika 0 */ }
5.6 Navigasi, Splash, dan Sesi
- AppNavViewModel mengamati status login dari SessionRepository
-
Splash menavigasi ke Dashboard atau Login berdasarkan isLoggedIn
-
Profile logout: memanggil clearSession() lalu navigate ke Login
dengan clear back stack
5.7 Design System
Warna brand (BrandRed, dsb), tema Material3, komponen seperti
ShimmerPlaceholder. Komponen UI diselaraskan dengan MaterialTheme
untuk konsistensi.
5.8 Komponen Baru v2
- BaseViewModel: pola loading, error mapping (Failure → UI), Global Snackbar.
- RetryManager: util untuk mengulang operasi gagal (network) dengan backoff ringan.
- Session Expiration Handler: observer global yang mengarahkan ke Login saat token kadaluarsa.
- Screen Result Passing: pola aman untuk mengembalikan hasil antar layar/fitur.
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val useCase: DoSomethingUseCase,
private val retry: RetryManager,
) : BaseViewModel() {
fun submit() = launchWithLoading {
retry.run { emitSnackbarOnError = true }
.execute { useCase() }
.onSuccess { /* update state */ }
.onFailure { /* optional handling */ }
}
}
6. Praktik Baik di UI
-
Gunakan
collectAsStateWithLifecycle untuk semua
StateFlow
-
Gunakan one-off event (SharedFlow/Channel) untuk tindakan yang tidak
idempotent
- Reset error saat input berubah di ViewModel, bukan di UI
-
Semua string UI melalui strings.xml; ViewModel pakai StringProvider
-
Disable input saat loading; sembunyikan keyboard ketika submit
7. Contoh Alur Login
-
LoginScreen memanggil vm.login() pada IME Done /
klik tombol
-
LoginViewModel:
- show() LoadingManager
- loginUseCase(Param) → Result
- Success → emit(LoginEvent.LoginSuccess)
-
Error → map LoginFailure ke usernameError/passwordError atau
generalError
- hide() LoadingManager
- UI mengamati events dan melakukan navigasi
8. Menjalankan Proyek
- Buka dengan Android Studio (Hedgehog+ direkomendasikan)
- Sync Gradle, pastikan JDK yang sesuai
- Jalankan app pada emulator/perangkat
- Modul yang diperlukan akan ter-build otomatis
💡 Catatan Splash Screen: Android 12+ memakai
AndroidX SplashScreen API; Android 11 ke bawah menggunakan emulasi via
core-splashscreen.
9. Konvensi Kode
Konvensi Penamaan
| Komponen |
Format |
Contoh |
| UseCase |
VerbNounUseCase |
LoginUseCase |
| Repository Interface |
XxxRepository |
AuthRepository |
| Repository Impl |
XxxRepositoryImpl |
AuthRepositoryImpl |
| ViewModel |
XxxViewModel |
LoginViewModel |
| State |
XxxScreenState / XxxFormState |
LoginFormState |
| Failure |
FeatureFailure.SpecificCase |
LoginFailure.UsernameEmpty |
Dependency Injection (Hilt)
@Module
@InstallIn(SingletonComponent::class)
interface AuthDataModule {
@Binds
fun bindAuthRepository(impl: AuthRepositoryImpl): AuthRepository
}
Error Handling & Mapping
when (val res = loginUseCase(param)) {
is Result.Success ->
_events.emit(LoginEvent.LoginSuccess(res.data))
is Result.Error -> when (res.failure) {
LoginFailure.UsernameEmpty ->
_state.update {
it.copy(usernameError = sp.get(R.string.error_username_empty))
}
LoginFailure.PasswordTooShort ->
_state.update {
it.copy(passwordError = sp.get(R.string.error_password_short))
}
else ->
_state.update {
it.copy(generalError = sp.get(R.string.error_generic))
}
}
}
10. Panduan Testing
Unit Test UseCase
class LoginUseCaseTest {
private val repo = FakeAuthRepository()
private val useCase = LoginUseCase(repo, StandardTestDispatcher())
@Test
fun blank_username_returns_failure() = runTest {
val res = useCase(LoginUseCase.Param(" ", "123456"))
assertTrue(res is Result.Error &&
res.failure == LoginFailure.UsernameEmpty)
}
}
Unit Test ViewModel
- Gunakan FakeStringProvider
- Gunakan UnconfinedTestDispatcher
- Gunakan fake/mocked repository
Perintah Testing
# Jalankan semua unit test
./gradlew testDebugUnitTest
# Jalankan lint
./gradlew lint
Cara Membuat Feature Baru
- Buat modul baru
feature-xyz
- Buat paket: domain, data, ui/screen, navigation
-
Definisikan Failure, UseCase, dan kontrak Repository di domain
- Implementasikan RepositoryImpl + mapper di data
- Buat XyzScreen + XyzViewModel di presentation
- Tambah route di navigation dan daftarkan di AppNavHost
- Tambah dependency modul di build.gradle.kts
- Tulis minimal 1 unit test UseCase dan 1 unit test ViewModel
Template ViewModel
@HiltViewModel
class XyzViewModel @Inject constructor(
private val useCase: XyzUseCase,
private val sp: StringProvider,
private val loading: LoadingManager,
) : ViewModel() {
// StateFlow, events, intent handlers
}
Build Configuration
Build Variants & Environment
android {
buildTypes {
debug {
buildConfigField("String", "BASE_URL", '"https://dev.api"')
}
release {
buildConfigField("String", "BASE_URL", '"https://prod.api"')
}
}
}
Secrets Management
⚠️ Penting: Jangan commit kunci/credential. Simpan di
local.properties atau gradle.properties (lokal).
val API_KEY: String by project
buildTypes {
debug {
buildConfigField("String", "API_KEY", '"' + API_KEY + '"')
}
}
Quality & Accessibility
Logging & Analytics
- Gunakan wrapper logging (Timber)
- Timber.d untuk debug non-sensitif
- Timber.e untuk error (tanpa PII)
- Nonaktifkan verbose log di release
Aksesibilitas & i18n
- Semua komponen punya contentDescription bila relevan
- Hindari teks hardcoded; gunakan strings.xml
- Gunakan semantics heading() untuk judul list/section
- Pastikan kontras warna mengikuti Material3
Definition of Done
Minimal Requirements
- ✅ Unit test untuk UseCase dan ViewModel lulus
- ✅ Tidak ada literal string di domain/data
- ✅ Loading tidak nyangkut; navigasi idempotent
- ✅ Dokumentasi route/fitur baru ditambahkan
PR Checklist
- ☑️ Nama branch sesuai konvensi
- ☑️ Screenshot UI (light/dark) bila ada perubahan UI
- ☑️ Test lulus, lint lulus
- ☑️ Update strings + aksesibilitas
11. Troubleshooting & FAQ
Gradle Sync Gagal (JDK Mismatch)
Gunakan JDK bawaan Android Studio:
Settings → Build, Execution, Deployment → Gradle → Gradle JDK
Hilt Error "Cannot be Provided"
Pastikan:
- @Binds module benar
- @Inject constructor benar
- Module @InstallIn komponen yang tepat
Compose Compiler Mismatch
Sesuaikan versi Kotlin/Compose dengan libs.versions.toml atau
konfigurasi versi centralized.
Build Lambat
- Aktifkan Gradle build cache
- Aktifkan parallel build
- Jalankan
./gradlew --scan untuk diagnosa
Event Navigasi Dobel
Gunakan SharedFlow untuk one-off event (replay = 1) dan konsumsi di
LaunchedEffect.
Peta Dependensi Modul
app
├─ depends on: lib-navigation, core-ui, core-domain, lib-designsystem
└─ hosts: NavHost root, Splash, App-level Hilt entries
feature-*/
├─ depends on: core-domain, core-ui, lib-designsystem, lib-navigation
└─ optional: core-network, core-database, core-security
core-domain ← digunakan oleh semua fitur
core-ui ← digunakan oleh app + fitur (LoadingManager, StringProvider)
lib-designsystem ← dipakai di semua modul UI
lib-navigation ← dipakai oleh app + fitur
Aturan: Fitur tidak mengetahui fitur lain secara
langsung; komunikasi melalui kontrak domain atau jalur navigasi.
Pedoman Navigasi
-
Route didefinisikan di lib-navigation atau paket navigation tiap
feature
-
Setelah login sukses: navigate ke Dashboard dan clear back stack
Login
-
Logout dari Profile: clear hingga Dashboard, lalu navigate ke Login
Contoh Pemakaian
navController.navigate(Routes.DASHBOARD) {
popUpTo(Routes.LOGIN) { inclusive = true }
}
Tambahan v2
- Deep Link: definisikan
uriPattern di graph tiap feature; gunakan helper di lib-navigation untuk memetakan deep link → Destination.
- Notification Actions: intent dari notifikasi memicu deep link yang sama; lihat feature-notification dan AppNavHost contoh implementasinya.
Ringkasan Teknologi
| Kategori |
Teknologi |
Penggunaan |
| Language |
Kotlin |
Bahasa utama development |
| UI Framework |
Jetpack Compose |
Modern declarative UI |
| Architecture |
MVVM + Clean |
Separation of concerns |
| DI |
Hilt |
Dependency injection |
| Async |
Coroutines + Flow |
Asynchronous operations |
| Image Loading |
Coil |
Efficient image loading |
| Design |
Material 3 |
Design system |
| Database |
Room |
Caching & akses offline ringan |
| Push Notification |
Firebase Cloud Messaging |
Notifikasi & tindakan |
Best Practices Summary
✅ DO
- Gunakan sealed class untuk state dan result
- Pisahkan domain logic dari Android framework
- Implementasikan proper error handling dengan typed failures
- Tulis unit test untuk UseCase dan ViewModel
- Gunakan StringProvider untuk i18n di ViewModel
- Implementasikan one-off events dengan SharedFlow
- Gunakan collectAsStateWithLifecycle untuk StateFlow
❌ DON'T
- Jangan hardcode string di domain/data layer
- Jangan akses Context langsung di ViewModel
- Jangan gunakan LiveData untuk one-off events
- Jangan buat dependency antar feature modules
- Jangan commit secrets ke repository
- Jangan skip unit testing
- Jangan gunakan magic numbers
Workflow Development
1. Membuat Feature Baru
- Buat module
feature-xyz
- Setup struktur: domain, data, ui, navigation
- Definisikan contracts (UseCase, Repository, Failure)
- Implementasi domain logic
- Implementasi data layer
- Buat UI dengan Compose
- Setup ViewModel dengan proper state management
- Tambahkan route ke navigation graph
- Tulis unit tests
- Update documentation
2. Code Review Checklist
- Architecture compliance (MVVM + Clean)
- Proper error handling
- i18n implementation
- Unit test coverage
- No hardcoded strings
- Proper dependency injection
- Memory leak prevention
- Accessibility support
Glossary
| Term |
Definisi |
| UseCase |
Single business logic operation di domain layer |
| Repository |
Abstraction untuk data source (network, database, etc) |
| Entity |
Pure domain model tanpa framework dependency |
| DTO |
Data Transfer Object - model untuk transport layer |
| Mapper |
Function untuk konversi antar layer (DTO ↔ Entity) |
| DomainFailure |
Typed error representation di domain layer |
| StringProvider |
Abstraction untuk string resources di ViewModel |
| LoadingManager |
Global loading state dengan reference counting |
| One-off Event |
Event yang hanya terjadi sekali (navigasi, toast, dll) |
Resources & Links
Documentation
-
Jetpack Compose:
developer.android.com/jetpack/compose
-
Hilt:
developer.android.com/training/dependency-injection/hilt-android
-
Kotlin Coroutines:
kotlinlang.org/docs/coroutines-overview.html
- Material 3: m3.material.io
Best Practices
- Clean Architecture Guide
- Android Architecture Components
- Kotlin Coding Conventions
- Compose API Guidelines
Version History
| Version |
Date |
Changes |
| 2.0 |
2025-11-26 |
Registrasi + OTP, Notifikasi & FCM, Deep Linking & Actions, Room caching,
BaseViewModel + RetryManager, Global Snackbar & error sheet, session expiration,
screen result passing, update dokumentasi.
|
| 1.0 |
2025 |
Initial release dengan core features dan modular architecture
|
Terima Kasih
Dokumentasi ini dibuat untuk membantu developer memahami dan
menggunakan
BankingApp dengan lebih efektif.
BankingApp - Modular Clean Architecture Starter
Version 2.0 - 2025