Sem Göksu
Sem Göksu
Yazılım · Yolculuk · Fenerbahçe
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 21 0
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>
  );
}
VS Code - RN projesinde ekranlar, Axios client ve Metro bundler canli.
VS Code - RN projesinde ekranlar, Axios client ve Metro bundler canli.

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.

React Query Kurulumu
App.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 Senaryosu
Ağ 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...
Etiketler: #ASP.NET #C#
Paylaş:

Yorumlar (0)

Henüz yorum yok. İlk yorumu sen yap!

Yorum bırak

* Yorumlar moderasyon sonrası yayınlanır. E-posta gizli tutulur.