Belajar Mobile Developer - Architecture Patterns (MVVM/MVI)
Episode 8 of 28

Belajar Mobile Developer - Architecture Patterns (MVVM/MVI)

Mengorganisasi kode mobile dengan arsitektur yang matang: MVVM dan MVI, Repository pattern untuk memisahkan sumber data, dependency injection untuk modularitas, serta satu arah aliran data — sambil menyusun arsitektur clean untuk Fitku

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Sejauh ini kita sudah bisa membuat aplikasi di berbagai platform — tetapi aplikasi yang tumbuh akan kacau tanpa arsitektur. Di episode 8 ini kita belajar bagaimana tim profesional mengorganisasi kode mobile: MVVM/MVI, Repository pattern, dan dependency injection. Inilah yang membedakan demo app dari aplikasi production yang bisa dipelihara bertahun-tahun.

Mengapa arsitektur penting? Karena 90% biaya aplikasi ada di maintenance. Fitur baru, ganti API, atau tambah anggota tim — semua lebih murah jika kode terstruktur rapi. Arsitektur bukan teori belaka: ia adalah keputusan praktis untuk mengendalikan kompleksitas.

Masalah yang Ingin Dipecahkan

Tanpa arsitektur, aktivitas seperti "simpan workout" menyebar: sebagian di UI, sebagian di ViewModel, sebagian langsung ke database. Konsekuensinya: sulit diuji, sulit diganti backend, dan bug bermunculan di tempat tak terduga.

100%

Tujuannya satu arah aliran data (unidirectional) dan pemisahan tanggung jawab yang jelas antar lapisan.

MVVM (Model-View-ViewModel)

MVVM adalah standar de facto arsitektur mobile (dan dipakai bawaan oleh Android ViewModel):

  • View: UI murni, menampilkan state, meneruskan event.
  • ViewModel: menjembatani UI dan data; mengekspos state dan event yang bisa dipanggil UI.
  • Model: data & logika bisnis (repository, use case).

Contoh di Android:

KotlinViewModel Fitku
class HomeViewModel(
    private val repository: WorkoutRepository,
) : ViewModel() {
 
    private val _uiState = MutableStateFlow(HomeUiState())
    val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
 
    fun loadToday() {
        viewModelScope.launch {
            _uiState.update {
                it.copy(
                    steps = repository.fetchStepsToday(),
                    workouts = repository.fetchWorkoutsToday()
                )
            }
        }
    }
 
    fun startWorkout(name: String) {
        viewModelScope.launch {
            repository.saveWorkout(Workout(name = name, startedAt = clock.now()))
        }
    }
}
 
data class HomeUiState(
    val steps: Int = 0,
    val workouts: List<Workout> = emptyList(),
    val isLoading: Boolean = false,
)

View hanya membaca uiState dan memanggil fungsi ViewModel — tanpa logika bisnis di UI. Di SwiftUI, padanannya ObservableObject + @MainActor.

MVI (Model-View-Intent)

MVI memperketat MVVM dengan satu arah aliran data yang eksplisit: UI mengirim Intent, reducer menghasilkan State baru.

KotlinReducer MVI di KMP
sealed interface HomeIntent {
    data object Refresh : HomeIntent
    data class StartWorkout(val name: String) : HomeIntent
}
 
data class HomeState(
    val steps: Int = 0,
    val workouts: List<Workout> = emptyList(),
    val error: String? = null,
)
 
fun homeReducer(state: HomeState, intent: HomeIntent): HomeState = when (intent) {
    HomeIntent.Refresh -> state.copy(steps = loadSteps())
    is HomeIntent.StartWorkout -> state.copy(
        workouts = state.workouts + saveWorkout(intent.name)
    )
}

Kelebihan MVI: aliran data dapat diprediksi dan mudah di-debug (setiap perubahan state bisa dilacak). Kekurangan: boilerplate lebih banyak. Di ekosistem Flutter, pola ini diimplementasikan Bloc; di web, Anda mengenalnya sebagai Redux.

Note

MVVM dan MVI bukan "musuh". Banyak tim memakai MVVM sebagai dasar dan elemen MVI (sealed interface intent, immutable state) sebagai penyempurna. Pilih yang sesuai kompleksitas fitur, bukan yang paling fashionable.

Repository Pattern

Repository menjadi satu-satunya pintu data untuk lapisan di atasnya. UI/ViewModel tidak perlu tahu data berasal dari API atau database:

KotlinRepository Fitku
interface WorkoutRepository {
    suspend fun fetchWorkouts(userId: String): List<Workout>
    suspend fun saveWorkout(workout: Workout)
}
 
class WorkoutRepositoryImpl(
    private val api: WorkoutApi,
    private val localDb: WorkoutDao,
) : WorkoutRepository {
    override suspend fun fetchWorkouts(userId: String): List<Workout> =
        localDb.fetchCached() ?: api.fetchWorkouts(userId).also { localDb.insert(it) }
 
    override suspend fun saveWorkout(workout: Workout) {
        localDb.insert(workout)      // offline-first: simpan dulu
        api.uploadWorkout(workout)   // sinkronisasi
    }
}

Manfaatnya langsung terasa: episode 10 (networking), 11 (database), dan 23 (offline sync) semuanya hidup di balik interface repository ini — lapisan UI tidak berubah saat sumber data diganti.

Dependency Injection (DI)

Dependency Injection menyerahkan pembuatan objek ke "container" terpusat, sehingga tiap class menerima dependensinya — bukan menciptakannya sendiri. Tool populer: Hilt/Dagger (Android), Koin, dan di KMP Koin/Ktor + koin-annotations.

KotlinModul DI dengan Koin
val appModule = module {
    single<WorkoutRepository> { WorkoutRepositoryImpl(get(), get()) }
    single<WorkoutApi> { RetrofitFitkuApi(createRetrofit()) }
    single<WorkoutDao> { FitkuDatabase.get(context).workoutDao() }
    viewModel { HomeViewModel(get()) }
}

Keuntungan: kode mudah diuji (mock dependensi), mudah diganti, dan ketergantungannya transparan. Tanpa DI, class-class saling meng-instantiate sendiri dan jadilah spaghetti.

Praktik: Susun Arsitektur Fitku

Struktur akhir fitur "home" di Fitku menjadi:

Lapisan arsitektur Fitku
com.fitku/
├── ui/            → composable/SwiftUI views (murni tampilan)
│   ├── home/
│   │   ├── HomeScreen.kt
│   │   └── HomeViewModel.kt
├── domain/        → model & use case (tidak tahu framework)
│   ├── model/Workout.kt
│   └── repository/WorkoutRepository.kt
└── data/          → implementasi data (API, DB)
    ├── remote/WorkoutApi.kt
    └── local/WorkoutDao.kt

Aturan sederhananya: UI → domain → data, ketergantungan hanya satu arah. Lapisan data boleh berubah total tanpa menyentuh UI.

Tip

Jangan terburu-buru menaruh semua di "clean architecture" 5 lapis untuk aplikasi kecil. Mulai dari tiga lapisan (UI/domain/data) dengan MVVM + Repository — lalu tambah use case dan layer tambahan saat benar-benar dibutuhkan. Arsitektur adalah alat, bukan tujuan.

Common Pitfalls Arsitektur

  • ViewModel menumpuk logika networking + database + UI: pindahkan ke repository/use case.
  • UI memegang domain model yang "bocor" ke API: pisahkan model API (WorkoutDto) dari model domain (Workout).
  • State mutable dibagikan lintas thread: pakai StateFlow/StateObject dengan thread yang jelas.
  • DI dianggap opsional: tanpa DI, testing dan refactor menjadi mimpi buruk begitu app membesar.

Penutup

Pada episode 8 ini, kalian telah mengorganisasi kode dengan arsitektur profesional:

  • MVVM memisahkan View/ViewModel/Model; MVI menambahkan aliran data satu arah.
  • Repository pattern menyembunyikan sumber data dari UI.
  • Dependency injection membuat kode modular dan mudah diuji.
  • Struktur UI → domain → data siap menopang fitur-fitur kompleks berikutnya.

Di episode 9 selanjutnya kita masuk ke jantung interaktivitas: State Management Mobile — perbandingan Zustand/Redux di RN, Provider/Riverpod/Bloc di Flutter, dan pola state di Android/iOS native untuk aplikasi skala menengah. Sampai jumpa!

Belajar Mobile Developer - Architecture Patterns (MVVM/MVI) | Belajar Mobile Developer