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