Back to all articles
4 min read

Understanding Flutter's Main Method and Custom Entrypoints

Every Flutter app starts with a main() function. But did you know you can have multiple entrypoints for different platforms, environments, or build flavors? Let's explore how this works.

The Default Main Method

A standard Flutter app has a single entrypoint in lib/main.dart:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      home: const HomePage(),
    );
  }
}

The main() function is the entry point that Dart looks for when starting your application. The runApp() function takes your root widget and attaches it to the screen.

What Happens Before runApp()?

You can execute code before runApp() to initialize services:

void main() async {
  // Ensure Flutter bindings are initialized
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize services
  await Firebase.initializeApp();
  await Hive.initFlutter();

  // Set up error handling
  FlutterError.onError = (details) {
    FirebaseCrashlytics.instance.recordFlutterError(details);
  };

  // Now run the app
  runApp(const MyApp());
}

Important: If you need to call any async methods before runApp(), you must call WidgetsFlutterBinding.ensureInitialized() first.

Creating Multiple Entrypoints

You can create different entrypoints for different purposes. Here's a common structure:

lib/
├── main.dart              # Default/production
├── main_dev.dart          # Development
├── main_staging.dart      # Staging
└── main_mock.dart         # With mock data

Example: Development Entrypoint

// lib/main_dev.dart
import 'package:flutter/material.dart';
import 'package:my_app/app.dart';
import 'package:my_app/config/environment.dart';

void main() {
  Environment.init(
    apiBaseUrl: 'https://dev-api.example.com',
    enableLogging: true,
    enableDevTools: true,
  );

  runApp(const MyApp());
}

Example: Production Entrypoint

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:my_app/app.dart';
import 'package:my_app/config/environment.dart';

void main() {
  Environment.init(
    apiBaseUrl: 'https://api.example.com',
    enableLogging: false,
    enableDevTools: false,
  );

  runApp(const MyApp());
}

Running Different Entrypoints

Use the -t flag to specify a custom entrypoint:

# Run development version
flutter run -t lib/main_dev.dart

# Run staging version
flutter run -t lib/main_staging.dart

# Build with specific entrypoint
flutter build apk -t lib/main_dev.dart

Platform-Specific Entrypoints

You can also create entrypoints for specific platforms:

lib/
├── main.dart
├── main_mobile.dart       # iOS and Android
├── main_desktop.dart      # macOS, Windows, Linux
└── main_web.dart          # Web specific

Web-Specific Entrypoint

// lib/main_web.dart
import 'package:flutter/material.dart';
import 'package:my_app/app.dart';
import 'package:url_strategy/url_strategy.dart';

void main() {
  // Remove # from URLs on web
  setPathUrlStrategy();

  // Web-specific initialization
  runApp(const MyApp());
}

Run it with:

flutter run -d chrome -t lib/main_web.dart

Using Flavors with Entrypoints

Combine entrypoints with build flavors for a powerful configuration system:

Android Configuration

In android/app/build.gradle:

android {
    flavorDimensions "environment"

    productFlavors {
        dev {
            dimension "environment"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
        }
        staging {
            dimension "environment"
            applicationIdSuffix ".staging"
            versionNameSuffix "-staging"
        }
        prod {
            dimension "environment"
        }
    }
}

iOS Configuration

Create schemes in Xcode for each flavor (dev, staging, prod).

Running Flavored Builds

# Development flavor with dev entrypoint
flutter run --flavor dev -t lib/main_dev.dart

# Production flavor with production entrypoint
flutter run --flavor prod -t lib/main.dart

# Build release APK for staging
flutter build apk --flavor staging -t lib/main_staging.dart

Environment Configuration Pattern

Here's a clean pattern for managing environments:

// lib/config/environment.dart
enum EnvironmentType { dev, staging, prod }

class Environment {
  static late EnvironmentType type;
  static late String apiBaseUrl;
  static late bool enableLogging;

  static void init({
    required EnvironmentType env,
    required String apiUrl,
    bool logging = false,
  }) {
    type = env;
    apiBaseUrl = apiUrl;
    enableLogging = logging;
  }

  static bool get isDev => type == EnvironmentType.dev;
  static bool get isProd => type == EnvironmentType.prod;
}
// lib/main_dev.dart
void main() {
  Environment.init(
    env: EnvironmentType.dev,
    apiUrl: 'https://dev-api.example.com',
    logging: true,
  );
  runApp(const MyApp());
}

Native Platform Entrypoints

Android: Custom Application Class

You can also customize the Android native entrypoint:

// android/app/src/main/kotlin/.../MainApplication.kt
class MainApplication : FlutterApplication() {
    override fun onCreate() {
        super.onCreate()
        // Native Android initialization
    }
}

iOS: Custom AppDelegate

// ios/Runner/AppDelegate.swift
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Native iOS initialization
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

Testing with Custom Entrypoints

Create a test-specific entrypoint with mocked dependencies:

// lib/main_test.dart
import 'package:my_app/app.dart';
import 'package:my_app/services/api_service.dart';
import 'package:get_it/get_it.dart';

void main() {
  // Register mock services
  GetIt.I.registerSingleton<ApiService>(MockApiService());

  runApp(const MyApp());
}

Summary

Key takeaways:

  1. Default entrypoint: lib/main.dart is the default, but you can specify others with -t
  2. Multiple entrypoints: Create separate files for dev, staging, prod environments
  3. Platform-specific: Different entrypoints for web, mobile, desktop
  4. Combine with flavors: Use both entrypoints and build flavors for maximum flexibility
  5. Initialize early: Use WidgetsFlutterBinding.ensureInitialized() for async initialization

This pattern keeps your configuration clean and makes it easy to switch between different app configurations during development and deployment.