Tugas Pertemuan 14 Pemrograman Perangkat Bergerak B

Nama    : Fadaukas Daffa Tajuddin

NRP      : 5025231149

Kelas    : PPB (B)


Membuat NewsApp



Aplikasi berita ini dibangun menggunakan Jetpack Compose dengan arsitektur MVVM (Model-View-ViewModel). Aplikasi mendukung fitur pemuatan berita utama, pencarian berita, dan tampilan detail berita.

ApiService.kt
package com.newsapp.data.api

import com.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

@GET("everything")
suspend fun searchNews(
@Query("q") query: String,
@Query("apiKey") apiKey: String
): NewsResponse
}
NewsRepository.kt
package com.newsapp.data.repository

import com.newsapp.data.api.RetrofitClient

class NewsRepository {
private val apiKey = "b50b8403863240cd879bb56470e2f30d"

suspend fun getNews() = RetrofitClient.apiService.getTopHeadlines(
apiKey = apiKey
)

suspend fun searchNews(query: String) = RetrofitClient.apiService.searchNews(
query = query,
apiKey = apiKey
)
}
NewsViewModel.kt
package com.newsapp.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.newsapp.data.model.Article
import com.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()

private val _searchState = MutableStateFlow<NewsUiState>(NewsUiState.Success(emptyList()))
val searchState = _searchState.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")
}
}
}

fun searchNews(query: String) {
if (query.isEmpty()) {
_searchState.value = NewsUiState.Success(emptyList())
return
}
viewModelScope.launch {
_searchState.value = NewsUiState.Loading
try {
val response = repository.searchNews(query)
_searchState.value = NewsUiState.Success(response.articles)
} catch (e: Exception) {
_searchState.value = NewsUiState.Error(e.message ?: "Unknown Error")
}
}
}
}
NewsCard.kt
package com.newsapp.ui.components

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.newsapp.data.model.Article

@Composable
fun FeaturedNewsCard(
article: Article,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
.padding(16.dp)
.clickable { onClick() },
shape = RoundedCornerShape(16.dp)
) {
Box {
AsyncImage(
model = article.urlToImage,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.BottomStart
) {
Column {
Text(
text = article.title,
style = MaterialTheme.typography.titleLarge.copy(
color = Color.White,
fontWeight = FontWeight.Bold
),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "TECHNOLOGY", // Mock category
style = MaterialTheme.typography.labelMedium.copy(color = Color.White),
fontWeight = FontWeight.Bold
)
Text(
text = " • 2 hours ago", // Mock time
style = MaterialTheme.typography.labelMedium.copy(color = Color.White.copy(alpha = 0.7f))
)
}
}
}
}
}
}

@Composable
fun NewsCardHorizontal(
article: Article,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(16.dp, 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = article.urlToImage,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(80.dp)
.clip(RoundedCornerShape(12.dp))
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = article.title,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "NEWS", // Mock category
style = MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
)
Text(
text = " • 3 hours ago", // Mock time
style = MaterialTheme.typography.labelSmall.copy(color = Color.Gray)
)
}
}
}
}
NavGraph.kt
package com.newsapp.navigation

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.newsapp.data.model.Article
import com.newsapp.ui.screens.DetailScreen
import com.newsapp.ui.screens.HomeScreen
import com.newsapp.ui.screens.SearchScreen
import com.newsapp.viewmodel.NewsViewModel
import com.google.gson.Gson
import java.net.URLDecoder
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppNavGraph() {
val navController = rememberNavController()
val viewModel: NewsViewModel = viewModel()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

Scaffold(
topBar = {
if (currentRoute == "home" || currentRoute == "search" || currentRoute == "saved") {
Column {
TopAppBar(
title = {
Text(
"News App",
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold)
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.White,
titleContentColor = Color.Black
)
)
ScrollableTabRow(
selectedTabIndex = when (currentRoute) {
"home" -> 0
"search" -> 1
"saved" -> 2
else -> 0
},
edgePadding = 16.dp,
containerColor = Color.White,
contentColor = MaterialTheme.colorScheme.primary,
divider = {}
) {
Tab(
selected = currentRoute == "home",
onClick = { navController.navigate("home") },
text = { Text("Home", fontWeight = FontWeight.Bold) }
)
Tab(
selected = currentRoute == "search",
onClick = { navController.navigate("search") },
text = { Text("Search", fontWeight = FontWeight.Bold) }
)
Tab(
selected = currentRoute == "saved",
onClick = { /* Handle saved */ },
text = { Text("Saved", fontWeight = FontWeight.Bold) }
)
}
}
} else if (currentRoute?.startsWith("detail") == true) {
TopAppBar(
title = { Text("Detail News", fontWeight = FontWeight.Bold) },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
)
}
}
) { paddingValues ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(paddingValues)
) {
composable("home") {
HomeScreen(viewModel = viewModel) { article ->
navigateToDetail(navController, article)
}
}
composable("search") {
SearchScreen(viewModel = viewModel) { article ->
navigateToDetail(navController, article)
}
}
composable(
route = "detail/{article}",
arguments = listOf(
navArgument("article") { type = NavType.StringType }
)
) { backStackEntry ->
val articleJson = backStackEntry.arguments?.getString("article")
val article = Gson().fromJson(
URLDecoder.decode(articleJson, StandardCharsets.UTF_8.toString()),
Article::class.java
)
DetailScreen(article = article)
}
}
}
}

private fun navigateToDetail(navController: NavHostController, article: Article) {
val articleJson = URLEncoder.encode(
Gson().toJson(article),
StandardCharsets.UTF_8.toString()
)
navController.navigate("detail/$articleJson")
}
Preview Apps









Komentar

Postingan populer dari blog ini

Tugas Pertemuan 12 Pemrograman Perangkat Bergerak B

Tugas Pertemuan 2 Pemrograman Perangkat Bergerak B

Tugas Pertemuan 3 Pemrograman Perangkat Bergerak B