Oke, mari kita buat implementasi lengkap yang menggabungkan semua konsep yang kita bahas: Retrofit untuk ambil data, Flow sebagai pipanya, Sealed Class untuk status UI, dan WhileSubscribed(5000) sebagai satpamnya.
Anggaplah kita mau ambil daftar "Popular Movies".
1. UI State (Sealed Class)
Ini adalah cara modern untuk memberi tahu Compose apa yang harus digambar. Cuma ada 3 kemungkinan: Loading, Sukses (bawa data), atau Error.
sealed class MovieUiState {
object Loading : MovieUiState()
data class Success(val movies: List<Movie>) : MovieUiState()
data class Error(val message: String) : MovieUiState()
}
2. Retrofit API Service
Standar Retrofit, tapi perhatikan kita pakai suspend function.
interface MovieApiService {
@GET("movie/popular")
suspend fun getPopularMovies(
@Query("api_key") apiKey: String
): MovieResponse // MovieResponse adalah POJO/Data Class hasil JSON
}
3. Repository (Si Producer Flow)
Di sini tempat kita membuat "Pipa" (Flow). Kita pakai blok flow { ... } dan emit().
class MovieRepository(private val apiService: MovieApiService) {
fun fetchPopularMovies(): Flow<MovieUiState> = flow {
emit(MovieUiState.Loading) // 1. Kasih tahu UI lagi loading
try {
val response = apiService.getPopularMovies("YOUR_API_KEY")
emit(MovieUiState.Success(response.results)) // 2. Kirim data kalau sukses
} catch (e: Exception) {
emit(MovieUiState.Error(e.message ?: "Unknown Error")) // 3. Kirim error kalau gagal
}
}.flowOn(Dispatchers.IO) // Pastikan jalan di background thread (seperti subscribeOn)
}
4. ViewModel (Si Pengelola State)
Ini bagian paling krusial. Kita ubah Flow dari Repository menjadi StateFlow agar bisa dibaca Compose dan tahan banting saat rotasi layar (pakai angka 5000 tadi).
class MovieViewModel(private val repository: MovieRepository) : ViewModel() {
val uiState: StateFlow<MovieUiState> = repository.fetchPopularMovies()
.stateIn(
scope = viewModelScope,
// Si "Magic Number" 5 detik agar tidak fetch ulang saat rotasi HP
started = SharingStarted.WhileSubscribed(5000),
initialValue = MovieUiState.Loading
)
}
5. Jetpack Compose (Si Consumer)
Di Compose, kita tinggal "mengamati" (collect) aliran datanya.
@Composable
fun MovieScreen(viewModel: MovieViewModel) {
// collectAsStateWithLifecycle adalah cara paling aman di Android
val state by viewModel.uiState.collectAsStateWithLifecycle()
when (state) {
is MovieUiState.Loading -> {
CircularProgressIndicator() // Tampilkan muter-muter
}
is MovieUiState.Success -> {
val movies = (state as MovieUiState.Success).movies
LazyColumn {
items(movies) { movie ->
Text(text = movie.title)
}
}
}
is MovieUiState.Error -> {
val msg = (state as MovieUiState.Error).message
Text(text = "Waduh Error: $msg", color = Color.Red)
}
}
}
Kenapa Struktur Ini "Mantap"?
Anti-Leak: Karena pakai
viewModelScope, kalau ViewModel hancur, Flow otomatis berhenti. Tidak ada memori bocor.Hemat Baterai: Berkat
WhileSubscribed(5000), kalau user buka WhatsApp sebentar terus balik lagi, kita nggak perlu panggil API ulang. Tapi kalau user tutup app kelamaan, koneksi diputus otomatis.Thread Safe: Kamu nggak perlu pusing
postValueatausetValue.flowOn(Dispatchers.IO)sudah mengurus perpindahan jalur kabel di balik layar.Readable: Dengan
Sealed Class, kodenya gampang dibaca. "Kalau Loading lakuin ini, kalau Success lakuin itu."
#Mapping
Pilihan yang sangat bagus! Memisahkan Response Model (apa yang datang dari API) dan UI Model (apa yang dibutuhkan layar) adalah standar profesional yang disebut sebagai Data Mapping.
Satu tips penting: Jangan gunakan extend (inheritance). Dalam Kotlin modern, kita lebih suka menggunakan komposisi atau fungsi konversi.
Berikut adalah cara terbaik melakukannya:
1. Kenapa Jangan Pakai Extend?
Response Model itu "Kotor": Isinya seringkali mengikuti struktur JSON yang aneh (misal:
poster_path,release_date).UI Model itu "Bersih": Kamu hanya butuh data yang mau ditampilkan. Misalnya, daripada menyimpan
poster_path(string pendek), kamu lebih baik menyimpanfullUrlyang sudah siap ditampilkan oleh library Image (seperti Coil).Keterikatan: Kalau API berubah, tapi UI Model kamu extend ke Response Model, maka UI kamu bisa ikut rusak/error secara otomatis.
2. Cara Terbaik: Gunakan "Mapper Function"
Buatlah sebuah Extension Function untuk mengubah model API menjadi model UI.
Step 1: Definisi Model
// Model dari API (Sesuai JSON)
data class MovieResponse(
@SerializedName("id") val id: Int,
@SerializedName("title") val title: String,
@SerializedName("poster_path") val posterPath: String?
)
// Model untuk UI (Siap pakai di Compose)
data class MovieUiModel(
val id: Int,
val title: String,
val imageUrl: String // Sudah berbentuk URL lengkap
)
Step 2: Buat Mapper
fun MovieResponse.toUiModel(): MovieUiModel {
return MovieUiModel(
id = this.id,
title = this.title,
// Kita olah datanya di sini, bukan di Compose!
imageUrl = "https://image.tmdb.org/t/p/w500${this.posterPath}"
)
}
3. Di Mana Melakukan Mapping? (Level Repository)
Lakukan mapping di dalam Flow di level Repository. Dengan begitu, ViewModel dan UI sudah menerima data yang "matang".
class MovieRepository(private val api: MovieApiService) {
fun getPopularMovies(): Flow<List<MovieUiModel>> = flow {
val response = api.getPopularMovies("API_KEY")
// Kita ubah List<MovieResponse> menjadi List<MovieUiModel>
val uiModels = response.results.map { it.toUiModel() }
emit(uiModels)
}.flowOn(Dispatchers.IO)
}
Keuntungan Cara Ini:
UI Lebih Simple: Di Jetpack Compose, kamu tinggal panggil
movie.imageUrl. Kamu tidak perlu tahu cara gabungin base URL-nya lagi.Unit Test Mudah: Kamu bisa mengetes fungsi
toUiModel()secara terpisah untuk memastikan logika penggabungan string-nya benar.Default Value: Jika ada data dari API yang
null, kamu bisa langsung kasih nilai default di mapper:title = this.title ?: "No Title"
Analogi:
Response Model: Sayuran mentah dari pasar yang masih ada tanahnya.
Mapper: Proses mencuci dan memotong sayur.
UI Model: Sayur yang sudah siap dimasak/dimakan.
Kamu nggak mau kan masak sayur yang masih ada tanahnya di dapur (Compose)? Lebih baik bersihkan dulu di gudang (Repository/Mapper).

Komentar
Posting Komentar