React Native
Fullstack Mobil — Bölüm 4: RN Ekranları ve API Entegrasyonu
React Navigation ile tab/stack yapısı, React Query ile cache'li data fetching, FlatList + pull to refresh, form ekranı ve offline/performans ipuçları — mobil UI'nin tüm yönleri bir arada.
22 Nisan 2026
6 dk okuma
19
0
IDE tarafinda: VS Code tarafinda src/screens altindaki LoginScreen ve ProductListScreen'i acip, src/api/client.ts uzerinden Axios interceptor'u yaziyoruz. Metro terminalde 'Building 100%' gorulunce uygulama cihaza/simulatore yansiyor.
Merhaba arkadaşlar, serinin dördüncü bölümünde RN tarafının UI'sini inşa ediyoruz. Backend hazır, auth hazır — şimdi ekranları kodlayacağız. React Navigation ile tab yapısı, React Query ile cache'li data fetching, FlatList ile performanslı liste, formlar ve pull to refresh — tümünü göreceğiz.Gerekli Paketleri Yükleyelim
cd mobile
npm install @@react-navigation/native @@react-navigation/native-stack @@react-navigation/bottom-tabs
npx expo install react-native-screens react-native-safe-area-context
npm install @@tanstack/react-query
npm install @@expo/vector-icons
Navigation YapısıUygulamamızda Tab Navigator içinde Stack Navigator'lar olacak:
// src/navigation/index.tsx
import { NavigationContainer } from '@@react-navigation/native';
import { createBottomTabNavigator } from '@@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@@react-navigation/native-stack';
import { Ionicons } from '@@expo/vector-icons';
import { useAuth } from '../store/auth';
import { PostsListScreen } from '../screens/PostsListScreen';
import { PostDetailScreen } from '../screens/PostDetailScreen';
import { LoginScreen } from '../screens/LoginScreen';
import { ProfileScreen } from '../screens/ProfileScreen';
import { PostCreateScreen } from '../screens/PostCreateScreen';
const Tab = createBottomTabNavigator();
const PostStack = createNativeStackNavigator();
function PostsStackNavigator() {
return (
<PostStack.Navigator>
<PostStack.Screen name="PostsList" component={PostsListScreen} options={{ title: 'Yazılar' }} />
<PostStack.Screen name="PostDetail" component={PostDetailScreen} options={{ title: 'Detay' }} />
<PostStack.Screen name="PostCreate" component={PostCreateScreen} options={{ title: 'Yeni Yazı' }} />
</PostStack.Navigator>
);
}
export function AppNavigator() {
const token = useAuth((s) => s.token);
return (
<NavigationContainer>
{!token ? (
<LoginScreen />
) : (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ color, size, focused }) => {
let icon: any = 'home';
if (route.name === 'Home') icon = focused ? 'home' : 'home-outline';
if (route.name === 'Profile') icon = focused ? 'person' : 'person-outline';
return <Ionicons name={icon} size={size} color={color} />;
},
tabBarActiveTintColor: '#f59e0b',
headerShown: false
})}>
<Tab.Screen name="Home" component={PostsStackNavigator} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
)}
</NavigationContainer>
);
}
React Query KurulumuApp.tsx'te QueryClient provider:
// App.tsx
import { QueryClient, QueryClientProvider } from '@@tanstack/react-query';
import { useEffect } from 'react';
import { AppNavigator } from './src/navigation';
import { useAuth } from './src/store/auth';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
staleTime: 60_000, // 1 dk boyunca cache'den ver
refetchOnWindowFocus: false,
}
}
});
export default function App() {
const baslat = useAuth((s) => s.baslat);
const yuklendi = useAuth((s) => s.yuklendi);
useEffect(() => { baslat(); }, []);
if (!yuklendi) return null; // splash / loading
return (
<QueryClientProvider client={queryClient}>
<AppNavigator />
</QueryClientProvider>
);
}
API Hook'larıuseQuery ve useMutation ile sarılmış API çağrıları:// src/hooks/usePosts.ts
import { useQuery, useMutation, useQueryClient } from '@@tanstack/react-query';
import { api } from '../api/client';
export type Post = {
id: number;
baslik: string;
kapakResmi?: string;
yayinlanmaTarihi?: string;
yazarAdi: string;
yorumSayisi: number;
};
export type PostDetail = Post & { icerik: string };
export function usePostsList() {
return useQuery({
queryKey: ['posts'],
queryFn: async () => (await api.get<Post[]>('/api/posts')).data
});
}
export function usePost(id: number) {
return useQuery({
queryKey: ['post', id],
queryFn: async () => (await api.get<PostDetail>(`/api/posts/${id}`)).data,
enabled: !!id
});
}
export function useCreatePost() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (data: { baslik: string; icerik: string }) =>
(await api.post('/api/posts', data)).data,
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['posts'] });
}
});
}
Liste Ekranı — FlatList + Pull to Refresh// src/screens/PostsListScreen.tsx
import { FlatList, View, Text, Image, TouchableOpacity, RefreshControl, ActivityIndicator, StyleSheet } from 'react-native';
import { usePostsList, type Post } from '../hooks/usePosts';
export function PostsListScreen({ navigation }: any) {
const { data, isLoading, isRefetching, refetch } = usePostsList();
if (isLoading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#f59e0b" />
</View>
);
}
return (
<FlatList
data={data ?? []}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={{ padding: 12 }}
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={refetch} tintColor="#f59e0b" />}
ListEmptyComponent={<Text style={styles.empty}>Henüz yazı yok.</Text>}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.card}
onPress={() => navigation.navigate('PostDetail', { id: item.id })}>
{item.kapakResmi && (
<Image source={{ uri: item.kapakResmi }} style={styles.kapak} />
)}
<View style={styles.body}>
<Text style={styles.baslik} numberOfLines={2}>{item.baslik}</Text>
<Text style={styles.meta}>
{item.yazarAdi} · {item.yorumSayisi} yorum
</Text>
</View>
</TouchableOpacity>
)}
/>
);
}
const styles = StyleSheet.create({
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
empty: { textAlign: 'center', marginTop: 40, color: '#64748b' },
card: {
backgroundColor: '#fff',
borderRadius: 12,
marginBottom: 12,
overflow: 'hidden',
shadowColor: '#000',
shadowOpacity: 0.08,
shadowRadius: 6,
elevation: 2
},
kapak: { width: '100%', height: 180 },
body: { padding: 14 },
baslik: { fontSize: 17, fontWeight: '700', color: '#0f172a', marginBottom: 4 },
meta: { fontSize: 12, color: '#64748b' }
});
Detay Ekranı// src/screens/PostDetailScreen.tsx
import { ScrollView, View, Text, Image, ActivityIndicator, StyleSheet } from 'react-native';
import { usePost } from '../hooks/usePosts';
export function PostDetailScreen({ route }: any) {
const { id } = route.params;
const { data: post, isLoading, error } = usePost(id);
if (isLoading) return <ActivityIndicator style={{ flex: 1 }} size="large" color="#f59e0b" />;
if (error) return <Text style={styles.error}>Yüklenemedi</Text>;
if (!post) return null;
return (
<ScrollView style={{ flex: 1, backgroundColor: '#fff' }}>
{post.kapakResmi && <Image source={{ uri: post.kapakResmi }} style={styles.hero} />}
<View style={{ padding: 20 }}>
<Text style={styles.baslik}>{post.baslik}</Text>
<Text style={styles.meta}>{post.yazarAdi}</Text>
<Text style={styles.icerik}>{post.icerik}</Text>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
hero: { width: '100%', height: 240 },
baslik: { fontSize: 26, fontWeight: '800', color: '#0f172a', marginBottom: 8 },
meta: { fontSize: 12, color: '#94a3b8', marginBottom: 16 },
icerik: { fontSize: 16, lineHeight: 26, color: '#334155' },
error: { textAlign: 'center', padding: 40, color: '#dc2626' }
});
Yeni Yazı Ekranı (Form)// src/screens/PostCreateScreen.tsx
import { useState } from 'react';
import { View, TextInput, Button, Alert, StyleSheet } from 'react-native';
import { useCreatePost } from '../hooks/usePosts';
export function PostCreateScreen({ navigation }: any) {
const [baslik, setBaslik] = useState('');
const [icerik, setIcerik] = useState('');
const mutation = useCreatePost();
const kaydet = async () => {
if (!baslik.trim() || icerik.length < 10) {
Alert.alert('Eksik', 'Başlık ve en az 10 karakter içerik girin.');
return;
}
try {
await mutation.mutateAsync({ baslik, icerik });
navigation.goBack();
} catch (e: any) {
Alert.alert('Hata', e.response?.data?.error ?? 'Kaydedilemedi');
}
};
return (
<View style={styles.container}>
<TextInput
placeholder="Başlık"
value={baslik}
onChangeText={setBaslik}
maxLength={300}
style={styles.input}
/>
<TextInput
placeholder="İçerik..."
value={icerik}
onChangeText={setIcerik}
multiline
textAlignVertical="top"
style={[styles.input, { height: 240 }]}
/>
<Button title={mutation.isPending ? 'Kaydediliyor...' : 'Kaydet'} onPress={kaydet} disabled={mutation.isPending} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: '#fff' },
input: { borderWidth: 1, borderColor: '#e2e8f0', borderRadius: 8, padding: 12, marginBottom: 12, fontSize: 16 }
});
Offline SenaryosuAğ yoksa React Query cache'lenmiş veriyi göstermeye devam eder. Bununla birlikte:
-
NetInfo ile ağ durumunu izleyin- Offline olduğunda üste bir uyarı bandı gösterin
- Mutation'ları
persistQueryClient ile queue'ya alabilirsiniz (ileri seviye)Performans İpuçları
- FlatList vs ScrollView: Uzun listelerde her zaman FlatList, ScrollView tüm item'ları bellekte tutar
- Image cache:
expo-image paketi ile daha iyi cache kontrolü- memo & useCallback: Liste item'larında gereksiz re-render için
- Pagination:
useInfiniteQuery ile sonsuz kaydırmaÖzet
Bu bölümde tam bir mobil UI inşa ettik: Tab + Stack navigation, React Query ile data fetching + cache + invalidation, pull to refresh, form, loading/error state'leri. Elinizde artık çalışan bir end-to-end mobil uygulamanız var — backend'ten verileri çekiyor, kullanıcı giriş yapıyor, yazı oluşturuyor.
Son bölümde hepsini production'a taşıyacağız: API'yi Azure/Railway'e, mobil uygulamayı TestFlight ve Play Store'a. Herkese kolay gelsin, sorular yoruma...
Yorumlar (0)
Henüz yorum yok. İlk yorumu sen yap!