Data Fetching & Konsumsi API (REST API)
Studi Kasus: Aplikasi Berita dengan Public API
Implementasi lengkap aplikasi berita menggunakan API:
import axios from 'axios';
const NEWS_API_KEY = 'your-api-key-here';
const BASE_URL = 'https://newsapi.org/v2';
const newsAPI = axios.create({
baseURL: BASE_URL,
timeout: 10000
});
export const newsService = {
getTopHeadlines: async (country = 'id', category = 'general') => {
try {
const response = await newsAPI.get('/top-headlines', {
params: {
country,
category,
apiKey: NEWS_API_KEY
}
});
return { success: true, data: response.data.articles };
} catch (error) {
return { success: false, error: error.message };
}
},
searchNews: async (query, page = 1) => {
try {
const response = await newsAPI.get('/everything', {
params: {
q: query,
page,
pageSize: 20,
apiKey: NEWS_API_KEY
}
});
return { success: true, data: response.data.articles };
} catch (error) {
return { success: false, error: error.message };
}
}
};
import React, { useState, useEffect } from 'react';
import {
View,
Text,
FlatList,
Image,
TouchableOpacity,
ActivityIndicator,
RefreshControl,
StyleSheet
} from 'react-native';
import { newsService } from '../services/newsService';
const NewsListScreen = ({ navigation }) => {
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState(null);
const [category, setCategory] = useState('general');
const categories = [
'general', 'business', 'technology',
'entertainment', 'sports', 'health'
];
useEffect(() => {
fetchNews();
}, [category]);
const fetchNews = async () => {
try {
setLoading(true);
const result = await newsService.getTopHeadlines('id', category);
if (result.success) {
setArticles(result.data);
setError(null);
} else {
setError(result.error);
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const onRefresh = async () => {
setRefreshing(true);
await fetchNews();
setRefreshing(false);
};
const renderArticle = ({ item }) => (
<TouchableOpacity
style={styles.card}
onPress={() => navigation.navigate('NewsDetail', { article: item })}
>
{item.urlToImage && (
<Image
source={{ uri: item.urlToImage }}
style={styles.image}
resizeMode="cover"
/>
)}
<View style={styles.content}>
<Text style={styles.title} numberOfLines={2}>
{item.title}
</Text>
<Text style={styles.description} numberOfLines={3}>
{item.description}
</Text>
<View style={styles.footer}>
<Text style={styles.source}>{item.source.name}</Text>
<Text style={styles.date}>
{new Date(item.publishedAt).toLocaleDateString('id-ID')}
</Text>
</View>
</View>
</TouchableOpacity>
);
const renderCategory = (cat) => (
<TouchableOpacity
key={cat}
style={[
styles.categoryButton,
category === cat && styles.categoryButtonActive
]}
onPress={() => setCategory(cat)}
>
<Text
style={[
styles.categoryText,
category === cat && styles.categoryTextActive
]}
>
{cat.toUpperCase()}
</Text>
</TouchableOpacity>
);
if (loading && articles.length === 0) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Loading news...</Text>
</View>
);
}
if (error && articles.length === 0) {
return (
<View style={styles.centered}>
<Text style={styles.errorText}>Error: {error}</Text>
<TouchableOpacity style={styles.retryButton} onPress={fetchNews}>
<Text style={styles.retryText}>Retry</Text>
</TouchableOpacity>
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.categoryContainer}>
<FlatList
horizontal
data={categories}
keyExtractor={item => item}
renderItem={({ item }) => renderCategory(item)}
showsHorizontalScrollIndicator={false}
/>
</View>
<FlatList
data={articles}
keyExtractor={(item, index) => `${item.url}-${index}`}
renderItem={renderArticle}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
contentContainerStyle={styles.list}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5'
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20
},
categoryContainer: {
backgroundColor: 'white',
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0'
},
categoryButton: {
paddingHorizontal: 15,
paddingVertical: 8,
marginHorizontal: 5,
borderRadius: 20,
backgroundColor: '#f0f0f0'
},
categoryButtonActive: {
backgroundColor: '#007AFF'
},
categoryText: {
fontSize: 12,
fontWeight: '600',
color: '#666'
},
categoryTextActive: {
color: 'white'
},
list: {
padding: 10
},
card: {
backgroundColor: 'white',
borderRadius: 8,
marginBottom: 15,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3
},
image: {
width: '100%',
height: 200
},
content: {
padding: 15
},
title: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 8,
lineHeight: 22
},
description: {
fontSize: 14,
color: '#666',
lineHeight: 20,
marginBottom: 10
},
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center'
},
source: {
fontSize: 12,
color: '#007AFF',
fontWeight: '600'
},
date: {
fontSize: 12,
color: '#999'
},
loadingText: {
marginTop: 10,
color: '#666'
},
errorText: {
color: 'red',
marginBottom: 20,
textAlign: 'center'
},
retryButton: {
backgroundColor: '#007AFF',
paddingHorizontal: 30,
paddingVertical: 10,
borderRadius: 5
},
retryText: {
color: 'white',
fontWeight: 'bold'
}
});
export default NewsListScreen;
import React from 'react';
import {
View,
Text,
Image,
ScrollView,
TouchableOpacity,
Linking,
StyleSheet
} from 'react-native';
const NewsDetailScreen = ({ route }) => {
const { article } = route.params;
const openArticle = () => {
Linking.openURL(article.url);
};
return (
<ScrollView style={styles.container}>
{article.urlToImage && (
<Image
source={{ uri: article.urlToImage }}
style={styles.image}
resizeMode="cover"
/>
)}
<View style={styles.content}>
<Text style={styles.title}>{article.title}</Text>
<View style={styles.meta}>
<Text style={styles.source}>{article.source.name}</Text>
<Text style={styles.date}>
{new Date(article.publishedAt).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</Text>
</View>
{article.author && (
<Text style={styles.author}>By {article.author}</Text>
)}
<Text style={styles.description}>{article.description}</Text>
<Text style={styles.body}>{article.content}</Text>
<TouchableOpacity style={styles.button} onPress={openArticle}>
<Text style={styles.buttonText}>Read Full Article</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white'
},
image: {
width: '100%',
height: 250
},
content: {
padding: 20
},
title: {
fontSize: 22,
fontWeight: 'bold',
marginBottom: 15,
lineHeight: 30
},
meta: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 10,
paddingBottom: 10,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0'
},
source: {
fontSize: 14,
color: '#007AFF',
fontWeight: '600'
},
date: {
fontSize: 14,
color: '#999'
},
author: {
fontSize: 14,
color: '#666',
fontStyle: 'italic',
marginBottom: 15
},
description: {
fontSize: 16,
lineHeight: 24,
marginBottom: 15,
color: '#333'
},
body: {
fontSize: 15,
lineHeight: 24,
color: '#666',
marginBottom: 20
},
button: {
backgroundColor: '#007AFF',
padding: 15,
borderRadius: 8,
alignItems: 'center'
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold'
}
});
export default NewsDetailScreen;