Skip to content

Commit 22bcd92

Browse files
feat(samples): Add Spaceflight News screen with article fetching (#4657)
- Introduced a new screen for displaying spaceflight news articles using the Spaceflight News API. - Integrated `@shopify/flash-list` for efficient rendering of articles. - Added `ArticleCard` component for individual article display. - Included navigation to the new screen from the Performance screen. - Updated dependencies in package.json for `@shopify/flash-list` and `axios`. Co-authored-by: Antonis Lilis <[email protected]>
1 parent ea85ff8 commit 22bcd92

File tree

7 files changed

+327
-2
lines changed

7 files changed

+327
-2
lines changed

samples/react-native/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
"@react-navigation/stack": "^7.1.1",
3030
"@sentry/core": "8.54.0",
3131
"@sentry/react-native": "6.9.1",
32+
"@shopify/flash-list": "^1.7.3",
33+
"axios": "^1.8.3",
3234
"delay": "^6.0.0",
3335
"react": "18.3.1",
3436
"react-native": "0.77.1",

samples/react-native/src/App.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import HeavyNavigationScreen from './Screens/HeavyNavigationScreen';
3737
import WebviewScreen from './Screens/WebviewScreen';
3838
import { isTurboModuleEnabled } from '@sentry/react-native/dist/js/utils/environment';
3939
import * as ImagePicker from 'react-native-image-picker';
40+
import SpaceflightNewsScreen from './Screens/SpaceflightNewsScreen';
4041

4142
/* false by default to avoid issues in e2e tests waiting for the animation end */
4243
const RUNNING_INDICATOR = false;
@@ -81,6 +82,7 @@ Sentry.init({
8182
Sentry.reactNativeTracingIntegration({
8283
// The time to wait in ms until the transaction will be finished, For testing, default is 1000 ms
8384
idleTimeoutMs: 5_000,
85+
traceFetch: false, // Creates duplicate span for axios requests
8486
}),
8587
Sentry.httpClientIntegration({
8688
// These options are effective only in JS.
@@ -208,6 +210,10 @@ const PerformanceTabNavigator = Sentry.withProfiler(
208210
component={PerformanceScreen}
209211
options={{ title: 'Performance' }}
210212
/>
213+
<Stack.Screen
214+
name="SpaceflightNewsScreen"
215+
component={SpaceflightNewsScreen}
216+
/>
211217
<Stack.Screen name="Tracker" component={TrackerScreen} />
212218
<Stack.Screen
213219
name="ManualTracker"

samples/react-native/src/Screens/PerformanceScreen.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ const PerformanceScreen = (props: Props) => {
4040
<>
4141
<StatusBar barStyle="dark-content" />
4242
<ScrollView style={styles.mainView}>
43+
<Button
44+
title="Open Spaceflight News"
45+
onPress={() => {
46+
props.navigation.navigate('SpaceflightNewsScreen');
47+
}}
48+
/>
4349
<Button
4450
title="Auto Tracing Example"
4551
onPress={() => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/* eslint-disable react/no-unstable-nested-components */
2+
import React, { useState, useCallback } from 'react';
3+
import { View, ActivityIndicator, StyleSheet, RefreshControl, Text, Pressable } from 'react-native';
4+
import { FlashList } from '@shopify/flash-list';
5+
import axios from 'axios';
6+
import { ArticleCard } from '../components/ArticleCard';
7+
import type { Article } from '../types/api';
8+
import { useFocusEffect } from '@react-navigation/native';
9+
10+
const ITEMS_PER_PAGE = 2; // Small limit to create more spans
11+
const AUTO_LOAD_LIMIT = 1; // One auto load at the end of the list then shows button
12+
const API_URL = 'https://api.spaceflightnewsapi.net/v4/articles';
13+
14+
export default function NewsScreen() {
15+
const [articles, setArticles] = useState<Article[]>([]);
16+
const [loading, setLoading] = useState(true);
17+
const [refreshing, setRefreshing] = useState(false);
18+
const [page, setPage] = useState(1);
19+
const [hasMore, setHasMore] = useState(true);
20+
const [autoLoadCount, setAutoLoadCount] = useState(0);
21+
22+
const fetchArticles = async (pageNumber: number, refresh = false) => {
23+
try {
24+
const response = await axios.get(API_URL, {
25+
params: {
26+
limit: ITEMS_PER_PAGE,
27+
offset: (pageNumber - 1) * ITEMS_PER_PAGE,
28+
},
29+
});
30+
31+
const newArticles = response.data.results;
32+
setHasMore(response.data.next !== null);
33+
34+
if (refresh) {
35+
setArticles(newArticles);
36+
setAutoLoadCount(0);
37+
} else {
38+
setArticles((prev) => [...prev, ...newArticles]);
39+
}
40+
} catch (error) {
41+
console.error('Error fetching articles:', error);
42+
} finally {
43+
setLoading(false);
44+
setRefreshing(false);
45+
}
46+
};
47+
48+
useFocusEffect(
49+
useCallback(() => {
50+
fetchArticles(1, true);
51+
}, [])
52+
);
53+
54+
const handleLoadMore = () => {
55+
if (!loading && hasMore) {
56+
setPage((prev) => prev + 1);
57+
fetchArticles(page + 1);
58+
setAutoLoadCount((prev) => prev + 1);
59+
}
60+
};
61+
62+
const handleManualLoadMore = () => {
63+
handleLoadMore();
64+
};
65+
66+
const handleRefresh = () => {
67+
setRefreshing(true);
68+
setPage(1);
69+
fetchArticles(1, true);
70+
};
71+
72+
const handleEndReached = () => {
73+
if (autoLoadCount < AUTO_LOAD_LIMIT) {
74+
handleLoadMore();
75+
}
76+
};
77+
78+
const LoadMoreButton = () => {
79+
if (!hasMore) {return null;}
80+
if (loading) {
81+
return (
82+
<View style={styles.loadMoreContainer}>
83+
<ActivityIndicator size="small" color="#007AFF" />
84+
</View>
85+
);
86+
}
87+
return (
88+
<Pressable
89+
style={({ pressed }) => [
90+
styles.loadMoreButton,
91+
pressed && styles.loadMoreButtonPressed,
92+
]}
93+
onPress={handleManualLoadMore}>
94+
<Text style={styles.loadMoreText}>Load More Articles</Text>
95+
</Pressable>
96+
);
97+
};
98+
99+
if (loading && !refreshing) {
100+
return (
101+
<View style={styles.centered}>
102+
<ActivityIndicator size="large" color="#007AFF" />
103+
</View>
104+
);
105+
}
106+
107+
return (
108+
<View style={styles.container}>
109+
<FlashList
110+
data={articles}
111+
renderItem={({ item }) => <ArticleCard article={item} />}
112+
estimatedItemSize={350}
113+
onEndReached={handleEndReached}
114+
onEndReachedThreshold={0.5}
115+
ListFooterComponent={autoLoadCount >= AUTO_LOAD_LIMIT ? LoadMoreButton : null}
116+
refreshControl={
117+
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
118+
}
119+
/>
120+
</View>
121+
);
122+
}
123+
124+
const styles = StyleSheet.create({
125+
container: {
126+
flex: 1,
127+
backgroundColor: '#f5f5f5',
128+
},
129+
centered: {
130+
flex: 1,
131+
justifyContent: 'center',
132+
alignItems: 'center',
133+
},
134+
loadMoreContainer: {
135+
paddingVertical: 20,
136+
alignItems: 'center',
137+
},
138+
loadMoreButton: {
139+
backgroundColor: '#007AFF',
140+
paddingVertical: 12,
141+
paddingHorizontal: 24,
142+
borderRadius: 8,
143+
marginVertical: 20,
144+
marginHorizontal: 16,
145+
alignItems: 'center',
146+
shadowColor: '#000',
147+
shadowOffset: { width: 0, height: 2 },
148+
shadowOpacity: 0.1,
149+
shadowRadius: 4,
150+
elevation: 3,
151+
},
152+
loadMoreButtonPressed: {
153+
opacity: 0.8,
154+
transform: [{ scale: 0.98 }],
155+
},
156+
loadMoreText: {
157+
color: '#fff',
158+
fontSize: 16,
159+
fontWeight: '600',
160+
},
161+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
import { View, Text, Image, StyleSheet, Pressable } from 'react-native';
3+
import type { Article } from '../types/api';
4+
import * as Sentry from '@sentry/react-native';
5+
interface ArticleCardProps {
6+
article: Article;
7+
}
8+
9+
export function ArticleCard({ article }: ArticleCardProps) {
10+
return (
11+
<View>
12+
<Sentry.TimeToFullDisplay record={true} />
13+
<Sentry.Profiler name="ArticleCard">
14+
<Pressable style={styles.card}>
15+
<Image source={{ uri: article.image_url }} style={styles.image} />
16+
<View style={styles.content}>
17+
<Text style={styles.source}>{article.news_site}</Text>
18+
<Text style={styles.title} numberOfLines={2}>
19+
{article.title}
20+
</Text>
21+
<Text style={styles.summary} numberOfLines={3}>
22+
{article.summary}
23+
</Text>
24+
<Text style={styles.date}>
25+
{new Date(article.published_at).toLocaleDateString()}
26+
</Text>
27+
</View>
28+
</Pressable>
29+
</Sentry.Profiler>
30+
</View>
31+
);
32+
}
33+
34+
const styles = StyleSheet.create({
35+
card: {
36+
backgroundColor: '#fff',
37+
borderRadius: 12,
38+
marginHorizontal: 16,
39+
marginVertical: 8,
40+
overflow: 'hidden',
41+
elevation: 3,
42+
shadowColor: '#000',
43+
shadowOffset: { width: 0, height: 2 },
44+
shadowOpacity: 0.1,
45+
shadowRadius: 4,
46+
},
47+
image: {
48+
width: '100%',
49+
height: 200,
50+
resizeMode: 'cover',
51+
},
52+
content: {
53+
padding: 16,
54+
},
55+
source: {
56+
fontSize: 14,
57+
color: '#666',
58+
marginBottom: 4,
59+
fontWeight: '600',
60+
},
61+
title: {
62+
fontSize: 18,
63+
fontWeight: 'bold',
64+
marginBottom: 8,
65+
color: '#1a1a1a',
66+
},
67+
summary: {
68+
fontSize: 16,
69+
color: '#444',
70+
lineHeight: 22,
71+
marginBottom: 12,
72+
},
73+
date: {
74+
fontSize: 14,
75+
color: '#666',
76+
},
77+
});

samples/react-native/src/types/api.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface Article {
2+
id: number;
3+
title: string;
4+
url: string;
5+
image_url: string;
6+
news_site: string;
7+
summary: string;
8+
published_at: string;
9+
updated_at: string;
10+
featured: boolean;
11+
}
12+
13+
export interface ArticlesResponse {
14+
count: number;
15+
next: string | null;
16+
previous: string | null;
17+
results: Article[];
18+
}

0 commit comments

Comments
 (0)