Back to Blog
Mobile10 min readJuly 20, 2024

Flutter in Production: Building Cross-Platform Apps That Feel Native

A practical guide to Flutter for production mobile apps. Covers state management with Riverpod, platform channels for native features, performance profiling, and CI/CD with Fastlane.

FlutterDartMobileiOSAndroid
A

Azam

DevOps & AI Consultant

Why Flutter for Production Apps

Flutter renders its own widgets using the Skia (now Impeller) graphics engine rather than wrapping native platform components. This means pixel-perfect consistency across iOS and Android without the gaps that React Native's bridge architecture can introduce. The trade-off is a larger app binary and an opinionated UI toolkit that requires learning Flutter's widget system rather than adapting existing web knowledge.

Flutter is particularly strong for: teams that need true cross-platform consistency, apps with custom UI that doesn't map well to platform-standard components, and projects that also target web or desktop from the same codebase.

State Management with Riverpod

Riverpod is the most maintainable state management solution for Flutter production apps. It is compile-time safe (no runtime exceptions from missing providers), supports async state natively, and makes dependencies between providers explicit and testable.

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'user_provider.g.dart';

// Async provider that fetches user data
@riverpod
Future currentUser(CurrentUserRef ref) async {
  final authToken = ref.watch(authTokenProvider);
  if (authToken == null) throw Exception('Not authenticated');
  return ref.watch(apiClientProvider).getUser(authToken);
}

// Notifier for complex state with mutations
@riverpod
class CartNotifier extends _$CartNotifier {
  @override
  List build() => [];

  void addItem(Product product) {
    state = [...state, CartItem(product: product, quantity: 1)];
  }

  void removeItem(String productId) {
    state = state.where((i) => i.product.id != productId).toList();
  }
}
// In your widget
class CartPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(cartNotifierProvider);
    final userAsync = ref.watch(currentUserProvider);

    return userAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => ErrorWidget(error: e),
      data: (user) => CartView(user: user, items: items),
    );
  }
}

Platform Channels for Native Features

Flutter's Dart code runs on the Dart VM and communicates with native iOS/Android code through platform channels. Use this for features with no Flutter plugin: biometric auth specifics, background location, NFC, Bluetooth, or proprietary SDKs.

// Dart side
const _channel = MethodChannel('com.company.app/native');

Future getNativeDeviceId() async {
  try {
    return await _channel.invokeMethod('getDeviceId') ?? '';
  } on PlatformException catch (e) {
    throw Exception('Failed to get device ID: ${e.message}');
  }
}

// iOS side (Swift)
// AppDelegate.swift
FlutterMethodChannel(name: "com.company.app/native",
                     binaryMessenger: controller.binaryMessenger)
  .setMethodCallHandler { call, result in
    if call.method == "getDeviceId" {
      result(UIDevice.current.identifierForVendor?.uuidString)
    }
  }

Performance Profiling

Flutter's DevTools suite is the most important tool for identifying performance regressions. Run apps in profile mode (not debug) for accurate performance data.

# Run in profile mode
flutter run --profile

# Key metrics to watch in DevTools:
# - Frame build time: target < 16ms (60fps) or < 8ms (120fps)
# - Jank: frames that miss the deadline
# - Memory: watch for leaks with the Memory tab

Common Flutter performance issues:

  • Rebuilding too many widgets: Use const constructors on widgets that don't change. const widgets are not rebuilt when their parent rebuilds.
  • Heavy computation on the UI thread: Move expensive work to a separate Isolate — Flutter's equivalent of a background thread.
  • Unoptimised images: Always use compressed images and cache network images with cached_network_image.
  • ListView without item keys: Add key: ValueKey(item.id) to list items for efficient diffing.

CI/CD with Fastlane and GitHub Actions

# .github/workflows/flutter.yml
jobs:
  build-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 appbundle --release
      - name: Sign and upload to Play Store
        run: bundle exec fastlane android deploy
        env:
          SUPPLY_JSON_KEY_DATA: ${{ secrets.PLAY_STORE_KEY }}

  build-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
      - run: flutter pub get && flutter build ios --release --no-codesign
      - name: Sign and upload to App Store
        run: bundle exec fastlane ios deploy
        env:
          APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_KEY }}

Want to Build This for Your Team?

I help teams implement the patterns and architectures described in these articles. Let's talk about your project.

Book a Free Call