Flutter & React Native: The Cross-Platform Sprint — How to Ship a Two-Platform MVP in a Weekend Without Drowning in Native Bugs

When your weekend turns into a two-week native bug safari

It's Friday night. You have a killer app idea, a fresh Flutter or React Native project, and 48 hours before Monday. By Saturday afternoon you're already fighting platform-specific quirks: iOS safe areas, Android back button, keyboard avoidance, permissions, and that one native module that works on one platform but crashes on the other. The dream of a cross-platform MVP turns into a debugging marathon.

The problem isn't the framework — both Flutter and React Native are mature, production-ready tools. The problem is the gap between "write once, run anywhere" and the messy reality of two operating systems with different design languages, lifecycles, and hardware APIs. According to the official Flutter documentation (docs.flutter.dev), widgets are the building blocks, but platform channels and adaptive layouts are where most teams lose time. React Native's docs (reactnative.dev) similarly warn that native modules and platform-specific code are often necessary for full functionality.

The solution? Stop writing boilerplate from scratch. Use well-crafted prompts to generate adaptive UI, navigation, state logic, and native bridges — then spend your weekend on your unique value, not on re-solving the same cross-platform puzzles. This collection gives you 10 battle-tested prompts, from basic screen scaffolding to expert-level native module integration. Each comes with a real example and working code. Let's ship.

1. Basic: Adaptive Screen Scaffold (Flutter)

Task: Generate a screen that respects iOS and Android layout conventions (safe areas, status bar, scroll behavior).

Prompt:

Act as a senior Flutter developer. Create a HomeScreen widget that uses SafeArea, Scaffold, and a scrollable ListView. Add adaptive padding: 16 on iOS, 12 on Android. Use MediaQuery to detect platform and adjust the AppBar elevation (0 on iOS, 4 on Android). Include a floating action button only on Android. Return complete Dart code with comments.

Example result:

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final isIOS = Theme.of(context).platform == TargetPlatform.iOS;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        elevation: isIOS ? 0 : 4,
      ),
      body: SafeArea(
        child: ListView.builder(
          padding: EdgeInsets.all(isIOS ? 16 : 12),
          itemCount: 20,
          itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
        ),
      ),
      floatingActionButton: isIOS
          ? null
          : FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
    );
  }
}

This prompt saves you from the classic "why does my UI look off on one platform?" trap. Reference: Flutter's official guide on adaptive design (docs.flutter.dev/ui/adaptive).

2. Basic: Navigation with Bottom Tabs (React Native)

Task: Set up a bottom tab navigator with platform-specific icons and a stack for each tab.

Prompt:

You are a React Native expert using React Navigation v6. Generate a Tab.Navigator with three tabs: Home, Search, Profile. Use @react-navigation/bottom-tabs and @react-navigation/native-stack. On iOS, use Ionicons with filled icons; on Android, use MaterialIcons. Provide a NavigationContainer and stack navigators for each tab. Include TypeScript types for the param list.

Example result:

import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Ionicons from 'react-native-vector-icons/Ionicons';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { Platform } from 'react-native';

type RootTabParamList = { Home: undefined; Search: undefined; Profile: undefined };
const Tab = createBottomTabNavigator<RootTabParamList>();
const Stack = createNativeStackNavigator();

function HomeStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="HomeMain" component={HomeScreen} />
    </Stack.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ color, size }) => {
            const iconName = route.name === 'Home' ? 'home' : route.name === 'Search' ? 'search' : 'person';
            return Platform.OS === 'ios' ? (
              <Ionicons name={iconName} size={size} color={color} />
            ) : (
              <MaterialIcons name={iconName} size={size} color={color} />
            );
          },
        })}
      >
        <Tab.Screen name="Home" component={HomeStack} />
        <Tab.Screen name="Search" component={SearchScreen} />
        <Tab.Screen name="Profile" component={ProfileScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

React Navigation is the de-facto standard (reactnavigation.org). This prompt ensures your navigation feels native on both platforms.

3. Intermediate: State Management with Riverpod (Flutter)

Task: Implement a counter with Riverpod, including async state and error handling.

Prompt:

Write a Flutter app using Riverpod 2.x. Create a counterProvider using StateNotifierProvider. Add async loading and error states. Show a CircularProgressIndicator while loading, an error message with retry button on failure, and the counter value with increment/decrement buttons on success. Use ConsumerWidget and ref.watch. Provide full code.

Example result:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterState {
  final int value;
  final bool isLoading;
  final String? error;
  CounterState({this.value = 0, this.isLoading = false, this.error});
}

class CounterNotifier extends StateNotifier<CounterState> {
  CounterNotifier() : super(CounterState());

  Future<void> increment() async {
    state = CounterState(value: state.value, isLoading: true);
    try {
      await Future.delayed(const Duration(seconds: 1));
      state = CounterState(value: state.value + 1);
    } catch (e) {
      state = CounterState(value: state.value, error: e.toString());
    }
  }
}

final counterProvider = StateNotifierProvider<CounterNotifier, CounterState>((ref) => CounterNotifier());

class CounterScreen extends ConsumerWidget {
  const CounterScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(counterProvider);
    if (state.isLoading) return const Center(child: CircularProgressIndicator());
    if (state.error != null) {
      return Center(
        child: Column(
          children: [
            Text('Error: ${state.error}'),
            ElevatedButton(onPressed: () => ref.read(counterProvider.notifier).increment(), child: const Text('Retry')),
          ],
        ),
      );
    }
    return Center(
      child: Column(
        children: [
          Text('Count: ${state.value}'),
          ElevatedButton(onPressed: () => ref.read(counterProvider.notifier).increment(), child: const Text('Increment')),
        ],
      ),
    );
  }
}

Riverpod's official docs (riverpod.dev) recommend this pattern for scalable state management.

4. Intermediate: Redux Toolkit with Async Thunks (React Native)

Task: Set up Redux Toolkit with an async API call and loading/error states.

Prompt:

Generate a React Native Redux Toolkit slice for fetching user data from https://jsonplaceholder.typicode.com/users. Use createAsyncThunk and createSlice. Handle pending, fulfilled, and rejected states. Provide the store configuration and a component that dispatches the fetch and displays loading/error/data. Use TypeScript.

Example result:

import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';

export const fetchUsers = createAsyncThunk('users/fetch', async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  return await res.json();
});

const usersSlice = createSlice({
  name: 'users',
  initialState: { data: [], loading: false, error: null as string | null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => { state.loading = true; state.error = null; })
      .addCase(fetchUsers.fulfilled, (state, action) => { state.loading = false; state.data = action.payload; })
      .addCase(fetchUsers.rejected, (state, action) => { state.loading = false; state.error = action.error.message || 'Failed'; });
  },
});

const store = configureStore({ reducer: { users: usersSlice.reducer } });

export default function App() {
  return (
    <Provider store={store}>
      <UserList />
    </Provider>
  );
}

function UserList() {
  const dispatch = useDispatch();
  const { data, loading, error } = useSelector((state: any) => state.users);
  if (loading) return <Text>Loading...</Text>;
  if (error) return <Text>Error: {error}</Text>;
  return <FlatList data={data} keyExtractor={(item) => item.id.toString()} renderItem={({ item }) => <Text>{item.name}</Text>} />;
}

Redux Toolkit is the recommended approach by the Redux team (redux-toolkit.js.org).

5. Advanced: Native Module for Battery Status (Flutter)

Task: Create a Flutter plugin that reads battery level on iOS and Android using platform channels.

Prompt:

Write a Flutter plugin battery_reader that exposes a method getBatteryLevel(). On Android, use BatteryManager; on iOS, use UIDevice.current.batteryLevel. Provide Dart, Kotlin, and Swift code. Include method channel setup and error handling.

Example result:

Dart:

import 'package:flutter/services.dart';

class BatteryReader {
  static const _channel = MethodChannel('battery_reader');
  static Future<int> getBatteryLevel() async {
    final level = await _channel.invokeMethod<int>('getBatteryLevel');
    return level ?? -1;
  }
}

Kotlin (Android):

class BatteryReaderPlugin: FlutterPlugin, MethodCallHandler {
  private lateinit var channel: MethodChannel
  private lateinit var context: Context

  override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    context = binding.applicationContext
    channel = MethodChannel(binding.binaryMessenger, "battery_reader")
    channel.setMethodCallHandler(this)
  }

  override fun onMethodCall(call: MethodCall, result: Result) {
    if (call.method == "getBatteryLevel") {
      val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
      result.success(bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY))
    } else result.notImplemented()
  }
}

Swift (iOS):

import Flutter
import UIKit

public class BatteryReaderPlugin: NSObject, FlutterPlugin {
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "battery_reader", binaryMessenger: registrar.messenger())
    let instance = BatteryReaderPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    if call.method == "getBatteryLevel" {
      UIDevice.current.isBatteryMonitoringEnabled = true
      result(Int(UIDevice.current.batteryLevel * 100))
    } else { result(FlutterMethodNotImplemented) }
  }
}

This follows Flutter's official platform channels guide (docs.flutter.dev/platform-integration/platform-channels).

6. Advanced: Native Module for Device Info (React Native)

Task: Create a native module that returns device model and OS version.

Prompt:

Write a React Native native module DeviceInfo for Android (Kotlin) and iOS (Swift). Expose getDeviceInfo() returning { model, osVersion }. Provide the JavaScript interface and registration in MainApplication.kt and AppDelegate.swift. Use TurboModules if possible.

Example result:

JavaScript:

import { NativeModules } from 'react-native';
const { DeviceInfo } = NativeModules;
export default DeviceInfo;

Android (Kotlin):

class DeviceInfoModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
  override fun getName() = "DeviceInfo"
  @ReactMethod
  fun getDeviceInfo(promise: Promise) {
    val map = Arguments.createMap()
    map.putString("model", Build.MODEL)
    map.putString("osVersion", Build.VERSION.RELEASE)
    promise.resolve(map)
  }
}

iOS (Swift):

@objc(DeviceInfo)
class DeviceInfo: NSObject {
  @objc func getDeviceInfo(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
    resolve(["model": UIDevice.current.model, "osVersion": UIDevice.current.systemVersion])
  }
}

React Native's native modules documentation (reactnative.dev/docs/native-modules-android) is your reference.

7. Expert: CI/CD for Two Platforms (GitHub Actions)

Task: Set up a CI pipeline that builds both Android APK and iOS IPA on every push.

Prompt:

Create a GitHub Actions workflow for a Flutter project. It should run on ubuntu-latest for Android and macos-latest for iOS. Cache Flutter SDK, run tests, build APK (debug) and iOS (no codesign). Upload artifacts. Provide complete YAML.

Example result:

name: Flutter CI
on: push
jobs:
  android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { flutter-version: '3.24.0' }
      - run: flutter pub get
      - run: flutter test
      - run: flutter build apk --debug
      - uses: actions/upload-artifact@v4
        with: { name: android-apk, path: build/app/outputs/flutter-apk/app-debug.apk }
  ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { flutter-version: '3.24.0' }
      - run: flutter pub get
      - run: flutter build ios --no-codesign

This pattern is widely used; see Flutter's official CI docs (docs.flutter.dev/deployment/cd).

8. Expert: E2E Testing with Detox (React Native)

Task: Write a Detox test that checks login flow and navigation.

Prompt:

Generate a Detox test for a React Native app. Test: launch app, tap on email field, type 'test@example.com', tap password field, type 'password123', tap login button, expect to see 'Welcome' text. Provide detox.config.js and the test file. Use Jest.

Example result:

// e2e/firstTest.e2e.js
describe('Login flow', () => {
  beforeAll(async () => { await device.launchApp(); });
  it('should login successfully', async () => {
    await element(by.id('email')).typeText('test@example.com');
    await element(by.id('password')).typeText('password123');
    await element(by.id('loginButton')).tap();
    await expect(element(by.text('Welcome'))).toBeVisible();
  });
});

Detox is the recommended E2E framework for React Native (wix.github.io/Detox).

9. Expert: Performance Profiling Prompt

Task: Identify and fix jank in a Flutter list.

Prompt:

Act as a Flutter performance expert. Given a ListView with complex items, suggest optimizations: use ListView.builder, const constructors, RepaintBoundary, and avoid Opacity widgets. Provide before/after code and explain how to use DevTools to measure frame times.

Example result:

Before:

ListView(children: items.map((e) => Opacity(opacity: 0.9, child: ComplexItem(e))).toList())

After:

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, i) => RepaintBoundary(child: const ComplexItem()),
)

Use flutter run --profile and DevTools timeline. Reference: Flutter performance best practices (docs.flutter.dev/perf/best-practices).

10. Expert: Cross-Platform Design System

Task: Create a theme that adapts to iOS and Android.

Prompt:

Generate a Flutter ThemeData that uses Cupertino colors on iOS and Material 3 on Android. Define text styles, button styles, and input decorations per platform. Provide a ThemeProvider that switches based on defaultTargetPlatform. Include code.

Example result:

ThemeData getTheme(BuildContext context) {
  final isIOS = defaultTargetPlatform == TargetPlatform.iOS;
  return isIOS
      ? ThemeData(
          primaryColor: CupertinoColors.activeBlue,
          textTheme: CupertinoTextThemeData(),
          pageTransitionsTheme: PageTransitionsTheme(builders: { TargetPlatform.iOS: CupertinoPageTransitionsBuilder() }),
        )
      : ThemeData(
          useMaterial3: true,
          colorSchemeSeed: Colors.blue,
          pageTransitionsTheme: PageTransitionsTheme(builders: { TargetPlatform.android: ZoomPageTransitionsBuilder() }),
        );
}

This ensures a native feel on both platforms. See Material 3 and Cupertino docs.

Ship your MVP this weekend

Cross-platform development doesn't have to be a bug-ridden nightmare. With these prompts, you can generate adaptive UI, robust state management, native bridges, and CI pipelines in minutes — leaving you time to focus on your app's unique features. Remember: the goal isn't to avoid native code entirely, but to write it efficiently when needed.

Ready to put these prompts to work? Try them in your next Flutter or React Native project, and share your results. For more AI-powered development guides, explore asibiont.com/blog.

← All posts

Comments