Pertemuan 14
Latihan News App dengan REST API
Pada latihan ini, saya membuat aplikasi News App menggunakan Kotlin dan Jetpack Compose di Android Studio. Aplikasi ini menampilkan berita terkini dari REST API menggunakan layanan NewsAPI.org dengan menerapkan arsitektur MVVM (Model-View-ViewModel). Konsep utama yang diterapkan meliputi pengambilan data dari network menggunakan Retrofit, pemodelan UI state reaktif dengan StateFlow dan collectAsState(), pemisahan lapisan data menggunakan Repository Pattern, navigasi antar layar dengan Navigation Compose, serta pemuatan gambar secara asinkron menggunakan Coil.
com.example.newsapp, bahasa Kotlin, dan Minimum SDK API 26.app/build.gradle.kts yaitu lifecycle-viewmodel-compose:2.9.0, navigation-compose:2.9.0, retrofit:2.11.0, converter-gson:2.11.0, kotlinx-coroutines-android:1.10.2, dan coil-compose:2.7.0, kemudian melakukan Sync Gradle.AndroidManifest.xml dengan menambahkan tag <uses-permission android:name="android.permission.INTERNET"/> di atas blok <application> agar aplikasi dapat mengakses jaringan.data/model/ dan mendefinisikan dua data class: Article berisi properti title, description, content, author, urlToImage, dan publishedAt; serta NewsResponse berisi status, totalResults, dan articles: List<Article> sebagai model dari respons JSON API.data/remote/ dan mendefinisikan ApiService berupa interface Retrofit dengan anotasi @GET("top-headlines") dan fungsi suspend getTopHeadlines() yang menerima parameter country dan apiKey via @Query, mengembalikan NewsResponse.object (Singleton) dengan BASE_URL = "https://newsapi.org/v2/", menggunakan GsonConverterFactory untuk parsing JSON, dan menginisialisasi apiService secara lazy melalui Retrofit.Builder().data/repository/ dan membuat NewsRepository sebagai layer perantara antara data source dan ViewModel, berisi fungsi suspend getNews() yang memanggil RetrofitClient.apiService.getTopHeadlines() dengan API key dari NewsAPI.org.ui/state/ dan mendefinisikan NewsUiState sebagai sealed class dengan tiga kondisi: Loading (object), Success (data class berisi List<Article>), dan Error (data class berisi message: String) untuk merepresentasikan seluruh kemungkinan state UI.ui/viewmodel/ dan membuat NewsViewModel yang mengextend ViewModel. ViewModel menyimpan MutableStateFlow<NewsUiState> yang diekspos sebagai StateFlow, memanggil repository.getNews() dalam viewModelScope.launch, dan menangani exception untuk memperbarui state menjadi Error.ui/components/ dan membuat composable NewsCard berupa Card yang clickable. Setiap kartu menampilkan gambar artikel menggunakan AsyncImage dari Coil dengan tinggi 200.dp, diikuti judul artikel dengan MaterialTheme.typography.titleMedium.ui/screens/ yang mengonsumsi uiState via collectAsState() dan merender tiga kondisi: CircularProgressIndicator saat Loading, LazyColumn berisi NewsCard saat Success, serta teks error dan tombol "Retry" saat Error.Article dan menampilkan gambar penuh (250.dp), judul dengan headlineSmall, deskripsi, dan konten artikel dalam Column dengan verticalScroll agar dapat di-scroll.ui/navigation/ menggunakan NavHost dengan dua route: "home" dan "detail/{article}". Data artikel dikirim antar screen dengan cara meng-encode objek ke JSON menggunakan Gson lalu meng-encode URL-nya dengan URLEncoder.AppNavGraph() di dalam setContent { MaterialTheme { } }, menggantikan boilerplate Greeting yang dibuat secara default oleh Android Studio.Arsitektur yang memisahkan UI (View), logika bisnis (ViewModel), dan data (Model/Repository), sehingga kode lebih terstruktur, mudah diuji, dan lifecycle-aware.
Library HTTP client untuk Android yang mengubah HTTP API menjadi interface Kotlin. Mendukung fungsi suspend untuk integrasi langsung dengan Kotlin Coroutines.
StateFlow sebagai observable state holder yang reaktif. sealed class untuk merepresentasikan UI state secara exhaustive: Loading, Success, dan Error.
Layer abstraksi antara ViewModel dan sumber data, memudahkan penggantian data source (network/database) tanpa mengubah logika ViewModel.
Library navigasi resmi untuk Jetpack Compose menggunakan NavHost dan NavController. Mendukung passing data antar composable melalui route argument.
Library image loading modern untuk Android dan Compose. AsyncImage memuat gambar dari URL secara asinkron dengan dukungan placeholder, caching, dan transformasi.
package com.example.newsapp.data.model data class Article( val title: String, val description: String?, val content: String?, val author: String?, val urlToImage: String?, val publishedAt: String ) data class NewsResponse( val status: String, val totalResults: Int, val articles: List<Article> )
package com.example.newsapp.data.remote import retrofit2.http.GET import retrofit2.http.Query interface ApiService { @GET("top-headlines") suspend fun getTopHeadlines( @Query("country") country: String = "us", @Query("apiKey") apiKey: String ): NewsResponse } object RetrofitClient { private const val BASE_URL = "https://newsapi.org/v2/" val apiService: ApiService by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiService::class.java) } }
// ui/state/NewsUiState.kt sealed class NewsUiState { object Loading : NewsUiState() data class Success(val articles: List<Article>) : NewsUiState() data class Error(val message: String) : NewsUiState() } // data/repository/NewsRepository.kt class NewsRepository { suspend fun getNews() = RetrofitClient.apiService.getTopHeadlines( apiKey = "YOUR_API_KEY" ) }
package com.example.newsapp.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class NewsViewModel : ViewModel() { private val repository = NewsRepository() private val _uiState = MutableStateFlow<NewsUiState>(NewsUiState.Loading) val uiState = _uiState.asStateFlow() init { loadNews() } fun loadNews() { viewModelScope.launch { try { _uiState.value = NewsUiState.Loading val response = repository.getNews() _uiState.value = NewsUiState.Success(response.articles) } catch (e: Exception) { _uiState.value = NewsUiState.Error(e.message ?: "Unknown Error") } } } }
@Composable fun HomeScreen( viewModel: NewsViewModel, onDetailClick: (Article) -> Unit ) { val state by viewModel.uiState.collectAsState() when (state) { is NewsUiState.Loading -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } is NewsUiState.Success -> { val articles = (state as NewsUiState.Success).articles LazyColumn { items(articles) { article -> NewsCard(article = article) { onDetailClick(article) } } } } is NewsUiState.Error -> { val error = state as NewsUiState.Error Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(error.message) Button(onClick = { viewModel.loadNews() }) { Text("Retry") } } } } }
@Composable fun AppNavGraph() { val navController = rememberNavController() val viewModel = viewModel<NewsViewModel>() NavHost(navController = navController, startDestination = "home") { composable("home") { HomeScreen(viewModel = viewModel) { article -> val json = Gson().toJson(article) val encoded = URLEncoder.encode(json, "UTF-8") navController.navigate("detail/$encoded") } } composable("detail/{article}") { backStackEntry -> val json = backStackEntry.arguments?.getString("article") ?: return@composable val article = Gson().fromJson(json, Article::class.java) DetailScreen(article = article) } } }
Komentar
Posting Komentar