Wednesday, June 10, 2026

PPB Pertemuan 14 - Aplikasi News REST API

Integrasi Web API pada Aplikasi "News App"

Muhammad Risyad Himawan Putra - 5025231205

 

Aplikasi NewsApp ini berfungsi untuk menampilkan daftar berita secara real-time. Data berita diambil langsung dari internet menggunakan News API. Request HTTP ditangani oleh library Retrofit, sedangkan untuk UI dirancang secara deklaratif menggunakan Jetpack Compose. Arsitektur aplikasi ini menerapkan pola MVVM (Model-View-ViewModel) agar kode program menjadi lebih terstruktur, modular, dan memisahkan logika bisnis dari tampilan utama.

 Source Code GitHub: https://github.com/MRHP1/Mobile-Programming/tree/main/NewsApp

1. Deskripsi & Alur Utama Jalannya Aplikasi

Secara garis besar, aplikasi ini bekerja dengan alur sebagai berikut:

  • Pengambilan Data API: Aplikasi melakukan request ke endpoint News API di background thread agar aplikasi terlihat seamless saat memuat data.
  • Pengelolaan State UI: Hasil request dari API dipetakan ke dalam 3 state utama di ViewModel, yaitu: Loading (saat mengunduh data), Success (ketika data berhasil didapatkan), dan Error (jika koneksi internet/APIbermasalah).
  • Navigasi Halaman: Menggunakan Navigation Compose untuk berpindah dari halaman daftar berita (HomeScreen) ke halaman detail berita (DetailScreen).

2. ApiService.kt

package com.example.newsapp.data.api import com.example.newsapp.data.model.NewsResponse 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 }

Penjelasan: File ApiService berupa interface yang berfungsi mendefinisikan rute atau endpoint API yang akan dipanggil. Di sini kita membuat fungsi getTopHeadlines dengan metode HTTP @GET("top-headlines"). Parameter query seperti asal negara (country) dan token API (apiKey) ditambahkan menggunakan anotasi @Query. Kata kunci suspend digunakan agar fungsi ini bisa dipanggil di dalam Coroutine secara asinkron tanpa memblokir thread UI utama.


3. RetrofitClient.kt

package com.example.newsapp.data.api import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory 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) } }

Penjelasan: Kita membuat RetrofitClient sebagai sebuah Singleton (menggunakan keyword object). Singleton ini bertugas menginisialisasi library Retrofit dengan menentukan alamat dasar server (BASE_URL) serta menyematkan GsonConverterFactory agar data respons berformat JSON dari API secara otomatis diubah menjadi objek Kotlin. Pemanggilan delegasi by lazy bertujuan agar inisialisasi ini hanya memakan memori ketika pertama kali diakses.


4. NewsRepository.kt

package com.example.newsapp.data.repository import com.example.newsapp.BuildConfig import com.example.newsapp.data.api.RetrofitClient class NewsRepository { suspend fun getNews() = RetrofitClient.apiService.getTopHeadlines( apiKey = BuildConfig.API_KEY ) }

Penjelasan: Kelas NewsRepository ini berperan sebagai layer pemisah data (Data Layer). Dengan memisahkan pemanggilan API di dalam repositori ini, ViewModel tidak perlu tahu detail teknis bagaimana data diambil. Keamanan token API juga tetap terjaga karena kita memuat API_KEY langsung dari konfigurasi lokal gradle (BuildConfig).


5. NewsViewModel.kt

package com.example.newsapp.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.newsapp.data.model.Article import com.example.newsapp.data.repository.NewsRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch sealed class NewsUiState { object Loading : NewsUiState() data class Success(val articles: List<Article>) : NewsUiState() data class Error(val message: String) : NewsUiState() } 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 ?: "Terjadi kesalahan koneksi" ) } } } }

Penjelasan: Di dalam NewsViewModel, kita membagi kondisi layar menjadi tiga status menggunakan sealed class NewsUiState. ViewModel memantau perubahan status ini melalui MutableStateFlow. Saat pertama kali program berjalan, blok init akan langsung menjalankan fungsi loadNews() secara asinkron di dalam lingkup viewModelScope. Jika data berhasil diambil, state berubah menjadi Success, dan jika gagal (karena koneksi mati atau salah URL), state berganti ke Error agar UI dapat menampilkan instruksi selanjutnya kepada pengguna.


6. NavGraph.kt

package com.example.newsapp.navigation import androidx.compose.runtime.* import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.example.newsapp.ui.screens.DetailScreen import com.example.newsapp.ui.screens.HomeScreen import com.example.newsapp.viewmodel.NewsUiState import com.example.newsapp.viewmodel.NewsViewModel @Composable fun AppNavGraph() { val navController = rememberNavController() val viewModel: NewsViewModel = viewModel() NavHost( navController = navController, startDestination = "home" ) { composable("home") { HomeScreen( viewModel = viewModel, onDetailClick = { articleIndex -> navController.navigate("detail/$articleIndex") } ) } composable( route = "detail/{articleIndex}", arguments = listOf( navArgument("articleIndex") { type = NavType.IntType } ) ) { backStackEntry -> val articleIndex = backStackEntry.arguments?.getInt("articleIndex") ?: 0 val state by viewModel.uiState.collectAsState() if (state is NewsUiState.Success) { val articles = (state as NewsUiState.Success).articles if (articleIndex in articles.indices) { DetailScreen( article = articles[articleIndex], onBackClick = { navController.popBackStack() } ) } } } } }

Penjelasan: Kita mengontrol alur perpindahan halaman menggunakan library Jetpack Navigation. Objek navController bertindak sebagai pengatur navigasi. Di halaman utama, ketika salah satu kartu berita diklik, kita akan berpindah rute ke halaman detail dengan menyelipkan parameter indeks artikel (articleIndex). Pada blok rute "detail/{articleIndex}", parameter tersebut ditangkap untuk menampilkan konten berita spesifik sesuai dengan data yang dipilih.


7. NewsCard.kt

@Composable fun FeaturedNewsCard( article: Article, onClick: () -> Unit ) { var visible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { visible = true } AnimatedVisibility( visible = visible, enter = fadeIn() + slideInVertically { -40 } ) { Card( modifier = Modifier .fillMaxWidth() .height(220.dp) .clickable { onClick() }, shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) ) { Box(modifier = Modifier.fillMaxSize()) { AsyncImage( model = article.urlToImage, contentDescription = article.title, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) Box( modifier = Modifier .fillMaxSize() .background( Brush.verticalGradient( colors = listOf( GradientStart.copy(alpha = 0.3f), GradientEnd.copy(alpha = 0.85f) ) ) ) ) Column( modifier = Modifier .fillMaxSize() .padding(20.dp), verticalArrangement = Arrangement.Bottom ) { Text( text = article.title, style = MaterialTheme.typography.titleLarge, color = Color.White, fontWeight = FontWeight.Bold, maxLines = 3, overflow = TextOverflow.Ellipsis ) } } } } }

Penjelasan: Kita membuat komponen kartu utama (FeaturedNewsCard) untuk menampilkan berita terpopuler di bagian paling atas.


8. Halaman Utama Feed Berita (HomeScreen.kt)

package com.example.newsapp.ui.screens import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.example.newsapp.ui.components.CompactNewsCard import com.example.newsapp.ui.components.FeaturedNewsCard import com.example.newsapp.viewmodel.NewsUiState import com.example.newsapp.viewmodel.NewsViewModel @Composable fun HomeScreen( viewModel: NewsViewModel, onDetailClick: (Int) -> Unit ) { val state by viewModel.uiState.collectAsState() Scaffold { innerPadding -> when (state) { is NewsUiState.Loading -> { Box( modifier = Modifier.fillMaxSize().padding(innerPadding), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } is NewsUiState.Success -> { val articles = (state as NewsUiState.Success).articles LazyColumn( modifier = Modifier.fillMaxSize().padding(innerPadding), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { if (articles.isNotEmpty()) { item { FeaturedNewsCard(article = articles[0], onClick = { onDetailClick(0) }) } } itemsIndexed(articles.drop(1)) { index, article -> CompactNewsCard(article = article, onClick = { onDetailClick(index + 1) }) } } } is NewsUiState.Error -> { val error = state as NewsUiState.Error Box( modifier = Modifier.fillMaxSize().padding(innerPadding), contentAlignment = Alignment.Center ) { Button(onClick = { viewModel.loadNews() }) { Text("Coba Lagi") } } } } } }

Penjelasan: Layar utama HomeScreen memproses status data yang diteruskan oleh ViewModel. Menggunakan struktur percabangan when (state) untuk menampilkan komponen UI yang sesuai: memutar spinner pemuatan data (CircularProgressIndicator) saat loading, merender baris list secara efisien menggunakan LazyColumn ketika sukses, atau menyodorkan tombol "Coba Lagi" (Retry) ketika terjadi error jaringan.


9. Activity Utama Aplikasi (MainActivity.kt)

package com.example.newsapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import com.example.newsapp.navigation.AppNavGraph import com.example.newsapp.ui.theme.NewsAppTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { NewsAppTheme { AppNavGraph() } } } }

Penjelasan: MainActivity bertindak sebagai entry point sistem operasi Android untuk menjalankan aplikasi. Fungsi enableEdgeToEdge() dipasang untuk membuat area status bar dan navigation bar di bawah layar menjadi transparan agar UI terlihat penuh dan bersih. Pada setContent, kita melakukan navigasi rute aplikasi (AppNavGraph()) di dalam tema global NewsAppTheme.


Demonstrasi

1. Kondisi Error Loading
Tampilan error saat aplikasi gagal mengambil data JSON dari News API.    

2. Berita Berhasil Tampil
Berita utama dipajang di kartu besar teratas, sementara berita pelengkap lainnya disusun ke bawah secara vertikal.

3. Layar Detail Artikel
Saat item diklik, detail halaman memuat isi teks berita secara penuh lengkap dengan tombol kembali.

No comments:

Post a Comment

PPB EAS - Aplikasi Member Timezone

Manajemen Keanggotaan & Loyalty Program Timezone dengan Room Database dan Jetpack Compose Aryaka Leorgi Epridaka - 5025231117 Muhammad ...