Skip to content
Published on

React Native vs Flutter 2025: The Complete Cross-Platform Mobile Development Guide

Authors

Table of Contents

1. Cross-Platform Mobile Development Landscape 2025

Market Overview

In 2025, the market share of cross-platform frameworks in mobile development continues to grow. Cost savings and faster time-to-market compared to native development are the key drivers.

FrameworkGitHub StarsWeekly Downloads/UsageKey Companies
React Native120k+npm 2M+/weekMeta, Microsoft, Shopify
Flutter165k+pub.dev activeGoogle, BMW, Alibaba
KMP (Compose)GrowingJetBrains ecosystemNetflix, Cash App

Key Selection Criteria

  • Team tech stack: React Native for web dev teams, Flutter if willing to learn Dart
  • UI customization: RN for platform-native look, Flutter for fully custom UI
  • Performance requirements: Flutter for animation-heavy apps, both sufficient for general apps
  • Existing code: RN for web code sharing, Flutter is also great for greenfield projects
  • Hiring market: Larger JavaScript developer pool makes RN easier for hiring

2. React Native 2025

New Architecture (GA)

React Native's New Architecture reached GA (Generally Available) in 2024 and has become the de facto standard in 2025.

Core Components:

  1. Fabric: New rendering system. Synchronous rendering for improved UI responsiveness
  2. TurboModules: Lazy loading of native modules. Reduces app startup time
  3. JSI (JavaScript Interface): Direct communication between JavaScript and native code. Bridge eliminated
  4. Codegen: Auto-generates type-safe native code
// TurboModule definition (New Architecture)
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  getConstants(): {
    platform: string;
    version: number;
  };
  multiply(a: number, b: number): Promise<number>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('Calculator');

Hermes Engine

Hermes is React Native's default JavaScript engine, optimized for mobile environments.

  • Bytecode pre-compilation: Reduces app startup time
  • Memory efficiency: Optimized garbage collection
  • Size reduction: Smaller app bundle size
  • Static Hermes (experimental): Additional performance via AOT compilation

Core React Native Patterns

// Functional component + Hooks
import React, { useState, useCallback } from 'react';
import { View, Text, FlatList, StyleSheet, Pressable } from 'react-native';

interface User {
  id: string;
  name: string;
  email: string;
}

const UserListScreen: React.FC = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [refreshing, setRefreshing] = useState(false);

  const onRefresh = useCallback(async () => {
    setRefreshing(true);
    try {
      const response = await fetch('https://api.example.com/users');
      const data = await response.json();
      setUsers(data);
    } finally {
      setRefreshing(false);
    }
  }, []);

  const renderItem = useCallback(({ item }: { item: User }) => (
    <Pressable style={styles.card}>
      <Text style={styles.name}>{item.name}</Text>
      <Text style={styles.email}>{item.email}</Text>
    </Pressable>
  ), []);

  return (
    <FlatList
      data={users}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
      refreshing={refreshing}
      onRefresh={onRefresh}
    />
  );
};

const styles = StyleSheet.create({
  card: {
    padding: 16,
    marginHorizontal: 16,
    marginVertical: 8,
    backgroundColor: '#fff',
    borderRadius: 8,
    elevation: 2,
  },
  name: { fontSize: 18, fontWeight: '600' },
  email: { fontSize: 14, color: '#666', marginTop: 4 },
});

3. Flutter 2025

Impeller Rendering Engine

Impeller is Flutter's new rendering engine, replacing Skia.

Key Improvements:

  • Pre-compiled shaders: Eliminates first-run jank (stutter)
  • Predictable performance: Minimized frame drops
  • Metal/Vulkan native: Direct use of platform GPU APIs
  • Default on iOS, also Stable on Android

Dart 3

Dart 3 significantly improved Flutter's development experience.

// Pattern matching (Dart 3)
sealed class Result<T> {
  const Result();
}
class Success<T> extends Result<T> {
  final T data;
  const Success(this.data);
}
class Error<T> extends Result<T> {
  final String message;
  const Error(this.message);
}
class Loading<T> extends Result<T> {
  const Loading();
}

// Switch expression with pattern matching
Widget buildContent(Result<User> result) => switch (result) {
  Success(:final data) => UserCard(user: data),
  Error(:final message) => ErrorWidget(message: message),
  Loading() => const CircularProgressIndicator(),
};

// Records
(String name, int age) getUser() => ('Kim', 30);

// Usage
final (name, age) = getUser();
print('$name is $age years old');

// Named fields in records
({String name, int age}) getUserNamed() => (name: 'Kim', age: 30);

Core Flutter Patterns

// StatelessWidget + state management
class UserListScreen extends StatelessWidget {
  const UserListScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Users')),
      body: Consumer<UserNotifier>(
        builder: (context, notifier, child) {
          return switch (notifier.state) {
            LoadingState() => const Center(
              child: CircularProgressIndicator(),
            ),
            ErrorState(:final message) => Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(message),
                  const SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: notifier.retry,
                    child: const Text('Retry'),
                  ),
                ],
              ),
            ),
            SuccessState(:final data) => ListView.builder(
              itemCount: data.length,
              itemBuilder: (context, index) {
                final user = data[index];
                return UserCard(user: user);
              },
            ),
          };
        },
      ),
    );
  }
}

class UserCard extends StatelessWidget {
  final User user;
  const UserCard({super.key, required this.user});

  
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: ListTile(
        title: Text(user.name,
          style: Theme.of(context).textTheme.titleMedium),
        subtitle: Text(user.email),
        trailing: const Icon(Icons.chevron_right),
      ),
    );
  }
}

4. Feature Comparison

Performance

MetricReact NativeFlutter
RenderingNative componentsCustom rendering (Impeller)
AnimationReanimated (60fps)Built-in (120fps capable)
App startupImproved with HermesFast with AOT compilation
Memory usageMediumMedium-High
Bundle sizeSmaller (~7MB)Larger (~15MB)
JS BridgeRemoved via JSIN/A (native)

Developer Experience (DX)

MetricReact NativeFlutter
LanguageJavaScript/TypeScriptDart
Hot ReloadFast RefreshHot Reload/Hot Restart
IDEVS Code, WebStormVS Code, Android Studio
DebuggingFlipper, Chrome DevToolsDevTools, Observatory
Package ecosystemnpm (massive)pub.dev (growing)
Learning curveLow for web devsRequires learning Dart

Native Module Integration

MetricReact NativeFlutter
Native APIsTurboModules + JSIPlatform Channels / FFI
Cameraexpo-cameracamera package
Mapsreact-native-mapsgoogle_maps_flutter
Push Notificationsexpo-notificationsfirebase_messaging
Biometricsexpo-local-authenticationlocal_auth

5. Expo Deep Dive

Expo Overview

Expo is a platform that dramatically simplifies React Native development. In 2025, most React Native projects use Expo.

Expo Router v4

File-based routing providing a Next.js-like experience.

app/
  _layout.tsx        <- Root layout
  index.tsx          <- Home screen (/)
  (tabs)/
    _layout.tsx      <- Tab navigation
    home.tsx         <- /home
    profile.tsx      <- /profile
  users/
    [id].tsx         <- /users/123 (dynamic route)
    index.tsx        <- /users
  settings/
    _layout.tsx      <- Settings layout
    index.tsx        <- /settings
// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="users/[id]" options={{ title: 'User Detail' }} />
    </Stack>
  );
}

// app/users/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';

export default function UserDetail() {
  const { id } = useLocalSearchParams<{ id: string }>();

  return (
    <View>
      <Text>User ID: {id}</Text>
    </View>
  );
}

// Type-safe navigation
import { router } from 'expo-router';

function navigateToUser(userId: string) {
  router.push(`/users/${userId}`);
}

EAS (Expo Application Services)

# EAS Build - cloud builds
npx eas-cli build --platform ios --profile production
npx eas-cli build --platform android --profile production

# EAS Submit - store submission
npx eas-cli submit --platform ios
npx eas-cli submit --platform android

# EAS Update - OTA updates (code only)
npx eas-cli update --branch production --message "Bug fix"

Expo SDK Modules

// expo-camera
import { CameraView, useCameraPermissions } from 'expo-camera';

function CameraScreen() {
  const [permission, requestPermission] = useCameraPermissions();

  if (!permission?.granted) {
    return <Button title="Grant Permission" onPress={requestPermission} />;
  }

  return <CameraView style={{ flex: 1 }} facing="back" />;
}

// expo-notifications
import * as Notifications from 'expo-notifications';

async function schedulePushNotification() {
  await Notifications.scheduleNotificationAsync({
    content: {
      title: "Reminder",
      body: 'Check your tasks!',
    },
    trigger: { seconds: 60 },
  });
}

6. Performance Benchmarks

App Startup Time (ms)

ScenarioReact Native (Hermes)Flutter (AOT)Native
Cold Start (Android)~350ms~280ms~200ms
Cold Start (iOS)~400ms~300ms~250ms
Warm Start~150ms~120ms~100ms

Scroll Performance (FPS)

ScenarioReact NativeFlutter
Simple list (1000 items)58-60 fps59-60 fps
Image list55-60 fps58-60 fps
Complex card layout50-58 fps56-60 fps

Animation Performance

ScenarioReact Native (Reanimated)Flutter
Simple transitions60 fps60 fps
Complex gestures55-60 fps58-60 fps
Particle systems45-55 fps55-60 fps

Memory Usage (MB)

ScenarioReact NativeFlutter
Empty app~40 MB~50 MB
List view (100 items)~65 MB~75 MB
Image gallery~120 MB~130 MB

Conclusion: Flutter has a slight edge in animation/rendering; RN is better in memory and bundle size. For general apps, the difference is minimal.


7. State Management

React Native State Management

Zustand (most popular choice)

import { create } from 'zustand';

interface UserStore {
  users: User[];
  isLoading: boolean;
  error: string | null;
  fetchUsers: () => Promise<void>;
}

const useUserStore = create<UserStore>((set) => ({
  users: [],
  isLoading: false,
  error: null,
  fetchUsers: async () => {
    set({ isLoading: true, error: null });
    try {
      const response = await fetch('https://api.example.com/users');
      const users = await response.json();
      set({ users, isLoading: false });
    } catch (error) {
      set({ error: (error as Error).message, isLoading: false });
    }
  },
}));

// Usage in component
function UserList() {
  const { users, isLoading, fetchUsers } = useUserStore();

  useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);

  if (isLoading) return <ActivityIndicator />;

  return (
    <FlatList
      data={users}
      renderItem={({ item }) => <UserCard user={item} />}
    />
  );
}

TanStack Query (server state)

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (newUser: CreateUserRequest) =>
      fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
      }).then(r => r.json()),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

Flutter State Management

Riverpod (most recommended)

// Provider definition

class UserNotifier extends _$UserNotifier {
  
  FutureOr<List<User>> build() async {
    return await ref.read(userRepositoryProvider).getUsers();
  }

  Future<void> addUser(CreateUserRequest request) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      await ref.read(userRepositoryProvider).createUser(request);
      return ref.read(userRepositoryProvider).getUsers();
    });
  }
}

// Usage in widget
class UserListScreen extends ConsumerWidget {
  const UserListScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(userNotifierProvider);

    return usersAsync.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => Center(child: Text('Error: $error')),
      data: (users) => ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) => UserCard(user: users[index]),
      ),
    );
  }
}

Bloc (enterprise choice)

// Event
sealed class UserEvent {}
class LoadUsers extends UserEvent {}
class CreateUser extends UserEvent {
  final CreateUserRequest request;
  CreateUser(this.request);
}

// State
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
  final List<User> users;
  UserLoaded(this.users);
}
class UserError extends UserState {
  final String message;
  UserError(this.message);
}

// Bloc
class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository repository;

  UserBloc(this.repository) : super(UserInitial()) {
    on<LoadUsers>((event, emit) async {
      emit(UserLoading());
      try {
        final users = await repository.getUsers();
        emit(UserLoaded(users));
      } catch (e) {
        emit(UserError(e.toString()));
      }
    });
  }
}

8. Navigation

Expo Router (React Native)

// Type-safe routing
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: '#2196F3' }}>
      <Tabs.Screen
        name="home"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

// Deep linking automatically supported
// myapp://users/123 -> app/users/[id].tsx

go_router (Flutter)

final router = GoRouter(
  routes: [
    ShellRoute(
      builder: (context, state, child) => ScaffoldWithNavBar(child: child),
      routes: [
        GoRoute(
          path: '/',
          builder: (context, state) => const HomeScreen(),
        ),
        GoRoute(
          path: '/users',
          builder: (context, state) => const UserListScreen(),
          routes: [
            GoRoute(
              path: ':id',
              builder: (context, state) {
                final id = state.pathParameters['id']!;
                return UserDetailScreen(userId: id);
              },
            ),
          ],
        ),
        GoRoute(
          path: '/profile',
          builder: (context, state) => const ProfileScreen(),
        ),
      ],
    ),
  ],
);

// Usage
context.go('/users/123');
context.push('/users/123');
context.pop();

9. Testing

React Native Testing

Detox (E2E)

// e2e/users.test.ts
describe('User Flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('should show user list', async () => {
    await expect(element(by.id('user-list'))).toBeVisible();
  });

  it('should navigate to user detail', async () => {
    await element(by.id('user-card-1')).tap();
    await expect(element(by.text('User Detail'))).toBeVisible();
  });

  it('should pull to refresh', async () => {
    await element(by.id('user-list')).swipe('down');
    await waitFor(element(by.id('loading-indicator')))
      .not.toBeVisible()
      .withTimeout(5000);
  });
});

Maestro (simple E2E)

# .maestro/user-flow.yaml
appId: com.example.myapp
---
- launchApp
- assertVisible: "Users"
- tapOn: "Kim"
- assertVisible: "User Detail"
- back
- assertVisible: "Users"

Flutter Testing

Widget Test

void main() {
  testWidgets('UserCard displays user info', (tester) async {
    final user = User(id: '1', name: 'Kim', email: 'kim@test.com');

    await tester.pumpWidget(
      MaterialApp(home: UserCard(user: user)),
    );

    expect(find.text('Kim'), findsOneWidget);
    expect(find.text('kim@test.com'), findsOneWidget);
  });

  testWidgets('UserListScreen shows loading indicator', (tester) async {
    await tester.pumpWidget(
      const MaterialApp(home: UserListScreen()),
    );

    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });
}

Integration Test

// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('full user flow test', (tester) async {
    await tester.pumpWidget(const MyApp());
    await tester.pumpAndSettle();

    // Verify user list
    expect(find.text('Users'), findsOneWidget);

    // Tap user card
    await tester.tap(find.text('Kim'));
    await tester.pumpAndSettle();

    // Verify detail screen
    expect(find.text('User Detail'), findsOneWidget);
  });
}

Patrol (advanced E2E)

void main() {
  patrolTest('User flow with native interactions', ($) async {
    await $.pumpWidgetAndSettle(const MyApp());

    // Handle permission dialog
    await $.native.grantPermissionWhenInUse();

    // Test both Flutter widgets and native UI
    await $(#userList).scrollTo(find.text('Kim'));
    await $(find.text('Kim')).tap();
  });
}

10. Job Market and Hiring

2025 Market Overview

MetricReact NativeFlutter
LinkedIn jobs (global)~35,000+~25,000+
Average salary (US)~130K130K-170K~120K120K-160K
Freelance rate (US)~8080-150/hr~7070-130/hr

Which to Learn First

Learn React Native first if:

  • You already know JavaScript/TypeScript
  • You have web development experience
  • Fast employment is your goal
  • You want to share code with existing React web projects

Learn Flutter first if:

  • You're starting programming fresh
  • Custom UI/animations are critical for your projects
  • You want a single codebase for desktop/web too
  • You're interested in the Google ecosystem

11. Decision Framework

Decision Flowchart

1. Is your team experienced with JavaScript/TypeScript?

  • Yes — Favor React Native
  • No — Continue evaluating

2. Are complex custom UI/animations central to the product?

  • Yes — Favor Flutter
  • No — Continue evaluating

3. Do you need to share code with a web app?

  • Yes — React Native (share React code)
  • No — Continue evaluating

4. Do you need desktop support?

  • Yes — Flutter (more mature desktop support)
  • No — Choose based on project requirements

Recommendations by Project Type

Project TypeRecommendationReason
E-commerce appReact NativeWeb code sharing, Shopify integration
Social mediaFlutterCustom UI, animations
Fintech appReact NativeLarge JavaScript ecosystem
GamesFlutterCustom rendering strengths
Enterprise appReact NativeEasier hiring
IoT dashboardFlutterDesktop + mobile unified
MVP/StartupExpo (RN)Fastest development speed
Media/StreamingFlutterPerformance optimization

12. Interview Questions and Quizzes

Top 10 Interview Questions

Q1: Explain the core components of React Native's New Architecture.

The New Architecture consists of Fabric (new rendering system), TurboModules (lazy loading of native modules), and JSI (JavaScript Interface). JSI replaces the old bridge, enabling direct synchronous communication between JavaScript and native code. Codegen auto-generates type-safe native interfaces.

Q2: Why did Flutter replace Skia with Impeller?

Skia-based rendering performed shader compilation at runtime, causing jank on first frames. Impeller pre-compiles shaders at build time, solving this problem. It directly uses Metal (iOS) and Vulkan (Android) for predictable performance.

Q3: What's the rendering difference between React Native and Flutter?

React Native uses platform native UI components (UIKit/Android Views), naturally following platform look and feel. Flutter renders every pixel with its own engine (Impeller), enabling full UI customization but requiring extra work for native appearance.

Q4: Explain Expo's advantages and limitations.

Advantages: EAS Build for simplified CI/CD, OTA updates (EAS Update), file-based routing (Expo Router), rich SDK modules, Prebuild for automated native code management. Limitations: some native modules may require config plugins or ejecting, build times can be longer than local builds.

Q5: Why is Riverpod preferred over Provider in Flutter?

Riverpod offers compile-time safety (fewer runtime errors), no BuildContext dependency, explicit provider dependency management, testability (override support), and automatic memory management via autodispose. It's an evolved version that addresses Provider's limitations.

Q6: Explain performance optimization methods in React Native.

Use FlatList (virtualized scrolling), React.memo/useMemo/useCallback to prevent unnecessary re-renders, Reanimated for UI thread animations, enable Hermes engine, image optimization (caching, resizing), bundle analysis to remove unnecessary dependencies, and enable New Architecture (JSI).

Q7: How did Dart 3's pattern matching and Records impact Flutter?

Pattern matching combined with sealed classes and switch expressions enables type-safe state handling. Records make functions returning multiple values concise. This combination makes Flutter state management code safer and more readable.

Q8: If you had to choose between React Native and Flutter, what criteria would you use?

Team tech stack (JS experience favors RN), UI requirements (custom UI favors Flutter), code sharing strategy (web sharing favors RN), hiring market (larger JS developer pool), performance requirements (graphics-intensive favors Flutter), existing infrastructure (React web presence favors RN). For most general apps, the difference is minimal, so team capability is most important.

Q9: Compare native feature access approaches in cross-platform apps.

React Native: TurboModules + JSI for direct communication with native code (Java/Kotlin, ObjC/Swift). Expo manages native configuration via config plugins. Flutter: Platform Channels (method/event channels) for async communication, or FFI for direct C library calls. Both frameworks cover most native APIs.

Q10: Explain a cross-platform testing strategy.

Follow the test pyramid: unit tests (business logic), widget/component tests (UI), integration tests (user scenarios), and E2E tests (real devices). RN uses Jest + Testing Library + Detox/Maestro; Flutter uses test + widget test + integration_test + Patrol.

5 Quizzes

Q1: What is JSI's role in React Native's New Architecture?

Answer:

JSI (JavaScript Interface) is an interface that enables direct synchronous communication between JavaScript and native code. It replaces the old asynchronous JSON serialization bridge. Written in C++, it's independent of the JavaScript engine and allows JavaScript to directly reference C++ objects. This dramatically improves performance and serves as the foundation for TurboModules and Fabric.

Q2: What's the selection criteria for StatelessWidget vs StatefulWidget in Flutter?

Answer:

StatelessWidget is used for widgets without internal state. It only displays data received from parents and always returns the same result on rebuild. StatefulWidget is used when mutable internal state is needed. However, in 2025, state management libraries like Riverpod or Bloc are the norm, so most widgets are written as StatelessWidget + state management library combinations.

Q3: What are the differences between Expo Router's file-based routing and traditional React Navigation?

Answer:

React Navigation explicitly defines navigators and screens in code. Type definitions are needed separately and nesting can become complex. Expo Router uses the file system structure as routes, like Next.js. It provides automatic deep linking, type inference, a layout system, and allows using the same URL structure on both web and mobile.

Q4: Why is Hermes better than V8/JavaScriptCore for React Native?

Answer:

Hermes is a JavaScript engine optimized for mobile environments. It pre-compiles (AOT) JavaScript to bytecode, eliminating parsing time at app startup. Its garbage collection is optimized for mobile, resulting in lower memory usage. App bundle size is also smaller. While V8 and JavaScriptCore are general-purpose engines, Hermes is specifically designed for mobile React Native.

Q5: How does Hot Reload work differently in Flutter vs React Native?

Answer:

Flutter's Hot Reload injects changed source code into the Dart VM and rebuilds the widget tree. State is preserved, allowing immediate UI change verification. React Native's Fast Refresh hot-swaps React components, maintaining state while updating the UI. Both reflect changes without app restart, but Flutter operates at the VM level while RN operates at the bundler (Metro) level.


References

  1. React Native Official Docs - Framework guide
  2. Flutter Official Docs - Framework guide
  3. Expo Documentation - Expo platform guide
  4. React Native New Architecture - New Architecture guide
  5. Flutter Impeller - Impeller rendering engine
  6. Dart Official Docs - Dart language guide
  7. Expo Router - File-based routing
  8. Riverpod - Flutter state management
  9. Zustand - React state management
  10. TanStack Query - Server state management
  11. Detox - React Native E2E testing
  12. Patrol - Flutter advanced testing
  13. go_router - Flutter navigation
  14. Reanimated - RN animation library