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 callWidgetsFlutterBinding.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:
- Default entrypoint:
lib/main.dartis the default, but you can specify others with-t - Multiple entrypoints: Create separate files for dev, staging, prod environments
- Platform-specific: Different entrypoints for web, mobile, desktop
- Combine with flavors: Use both entrypoints and build flavors for maximum flexibility
- 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.