This guide provides comprehensive, up-to-date best practices for Flutter and Dart development with direct links to official documentation, ensuring you're always following the latest standards and recommendations from Google and the Flutter team.
**Starting with Dart 3.0, null safety is mandatory and sound. All code must be fully null-safe. **
- Sound Null Safety: Dart 3 and later requires sound null safety - all libraries must be fully migrated
- Migration Path: Use
dart pub outdated --mode=null-safetyto check dependency status - Min SDK: Set
sdk: '>=3.0.0 <4.0.0'in pubspec.yaml
// ✅ DO: Use proper nullable/non-nullable types
class User {
final String id; // Non-nullable
final String? email; // Nullable
final List<String> tags; // Non-nullable collection
const User({
required this.id, // Required for non-nullable
this.email,
this.tags = const [], // Default value
});
}
// ❌ DON'T: Use the ! operator unless absolutely certain
String value = nullableValue!; // Avoid when possible
// ✅ DO: Use null-aware operators
String displayName = user?.name ?? 'Anonymous';Official Documentation: Effective Dart
// ✅ DO: Use PascalCase for classes
class MyWidget extends StatelessWidget { }
// ✅ DO: Use camelCase for variables and functions
final myVariable = 42;
void myFunction() { }
// ✅ DO: Use lowerCamelCase for constants
const defaultPadding = 16.0;
// ✅ DO: Use descriptive names
void calculateTotalRevenue() { } // Good
// ❌ DON'T: Use abbreviations unless common
void calcRev() { } // AvoidLatest stable release as of December 2024: Flutter 3.27
- Impeller as Default Renderer: Performance improvements with reduced jank on iOS and Android
- Swift Package Manager: Replaces CocoaPods for iOS development
- Enhanced Cupertino Widgets: Better iOS fidelity with updated CupertinoCheckbox, CupertinoRadio, CupertinoButton
- New Layout Features:
spacingparameter in Row/Column,CarouselView.weightedfor dynamic layouts
Based on recent changes, these APIs are deprecated :
| Deprecated Feature | Replacement |
|---|---|
TextField.canRequestFocus |
Use focus management directly |
ThemeData.dialogBackgroundColor |
DialogThemeData.backgroundColor |
InputDecoration.collapsed parameters |
Use only essential parameters |
Automatic AssetManifest.json generation |
Manually specify if needed |
Always check the Flutter Deprecation Guide before upgrading.
**As of 2026, teams optimize for clarity and predictability. The "state management wars" have matured. **
| Solution | Best For | Documentation |
|---|---|---|
| Riverpod 2.0+ | Most new projects, compile-time safety | Riverpod Docs |
| BLoC | Enterprise apps, complex business logic | BLoC Library |
| Signals | Ultra-hot paths, performance-critical UIs | Signals for Flutter |
Latest version: 2.6.0 (October 2024)
// ✅ DO: Use Code Generation (Riverpod 2.0+)
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_provider.g.dart';
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
}
// ✅ DO: Use ConsumerWidget for UI
class CounterDisplay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
// ⚠️ NOTE: Ref's type argument is deprecated in 2.6.0
// Old: Ref<MyState>
// New: Ref**With 120Hz displays becoming mainstream, optimize for 8-11ms frame budgets. **
// ✅ DO: Use selectors to rebuild only what changed
final userName = ref.watch(userProvider.select((user) => user.name));
// ✅ DO: Minimize rebuild scope
@riverpod
class OptimizedNotifier extends _$OptimizedNotifier {
@override
int build() => 0;
// Batch state updates
void batchUpdate() {
state = state + 1;
// Avoid multiple separate updates
}
}// ❌ DON'T: Rebuilding unnecessarily
Widget build(BuildContext context) {
return Container(
child: Text(calculateExpensiveValue()), // Runs on every build
);
}
// ✅ DO: Cache expensive computations
final expensiveValue = useMemoized(() => calculateExpensiveValue());
// ✅ DO: Use const constructors everywhere possible
const SizedBox(height: 16) // Good
Padding(padding: EdgeInsets.all(8)) // Not const - use const EdgeInsets.all(8)- Target: ≤8ms per frame (down from 16ms at 60fps)
- Profile with Flutter DevTools early and often
- Measure frame times, not just CPU usage
// ✅ DO: Always use builder for lists
ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
)
// ✅ DO: Use RepaintBoundary for isolated animations
RepaintBoundary(
child: ComplexAnimationWidget(),
)- Flutter Lazy Performance Examples - Off-screen widget optimization strategies
- Sloth App - Performance Anti-Patterns - Common performance issues in isolation
**Flutter 3.27 enhances Material 3 support with revamped sliders, progress indicators, and component theme normalization. **
// ✅ DO: Use Material 3 with dynamic color scheme
class AppTheme {
static ThemeData lightTheme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light,
),
);
}Flutter 3.27 improves Cupertino widgets for better iOS fidelity:
CupertinoCheckboxandCupertinoRadioupdatedCupertinoButtonaligns with iOS 15+ customizabilityCupertinoActionSheet,CupertinoContextMenuimprovements
// ✅ DO: Use Cupertino widgets for iOS-specific styling
import 'package:flutter/cupertino.dart';
class IosStyledPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('iOS Style'),
),
child: Center(
child: CupertinoButton(
onPressed: () {},
child: const Text('Press Me'),
),
),
);
}
}// ✅ DO: Proper async/await with error handling
Future<User> fetchUser(String id) async {
try {
final response = await http.get(Uri.parse('$apiUrl/users/$id'));
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
}
throw ApiException('Failed to load user: ${response.statusCode}');
} on SocketException {
throw NetworkException('No internet connection');
} catch (e) {
throw UnknownException('Unexpected error: $e');
}
}
// ✅ DO: Use Stream for sequences of events
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}As of Flutter 3.27, Impeller is the default rendering engine for iOS and Android.
Performance Gains (2026):
- Jank reduction: 30-50% for complex animations
- Text rendering speed: 20-40% improvement
- Drop frame rate: from 12% (Skia) to 1.5% (Impeller)
- Supports 120Hz displays seamlessly
# No configuration needed - Impeller is default in Flutter 3.27+
# For older versions, enable in android/app/build.gradle:
android {
defaultConfig {
// ...
}
}Flutter 2026 roadmap indicates Wasm will become the standard for Flutter Web.
# To enable Wasm compilation (future versions):
flutter build web --wasmBenefits:
- Near-native performance for Flutter Web
- Smaller bundle sizes
- Better memory management
include: package:flutter_lints/flutter.yaml
linter:
rules:
# Core quality rules
- always_declare_return_types
- avoid_print
- avoid_unnecessary_containers
- avoid_web_libraries_in_flutter
- camel_case_types
- constant_identifier_names
- curly_braces_in_flow_control_structures
- depend_on_referenced_packages
- no_adjacent_strings_in_list
- no_duplicate_case_values
- prefer_const_constructors
- prefer_const_declarations
- prefer_final_fields
- prefer_final_locals
- prefer_single_quotes
- sort_child_properties_last
- sort_pub_dependencies
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_final
- unnecessary_lambdas
- unnecessary_null_aware_assignments
- unnecessary_null_checks
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_string_interpolations
analyzer:
errors:
invalid_annotation_target: ignore
missing_required_param: error
missing_return: error
must_be_immutable: error
unrelated_type_equality_checks: error// 1. Unit Tests (Business Logic)
test('Counter increments correctly', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
// 2. Widget Tests (UI Components)
testWidgets('Button shows correct text', (tester) async {
await tester.pumpWidget(MyButton(text: 'Click'));
expect(find.text('Click'), findsOneWidget);
});
// 3. Integration Tests (Full User Flows)
test('Complete user flow', () async {
await driver.tap(find.byValueKey('login_button'));
await driver.enterText(find.byValueKey('email'), 'user@example.com');
await driver.tap(find.byValueKey('submit'));
expect(await driver.getText(find.byValueKey('welcome')), 'Welcome!');
});# Run all tests
flutter test
# Run with coverage
flutter test --coverage
# Generate coverage report
genhtml coverage/lcov.info -o coverage/html
# Run integration tests
flutter test integration_test/| Library/Feature | Deprecation | Replacement | Version |
|---|---|---|---|
| Riverpod Ref Type Argument | Ref<MyState> |
Ref only |
Riverpod 2.6.0 |
| CocoaPods for iOS | iOS dependency management | Swift Package Manager | Flutter 3.27 |
| Skia Renderer on Android | Skia default | Impeller default | Flutter 3.27 |
| TextField.canRequestFocus | Property | Direct focus management | Flutter 3.27 |
# Run with deprecation warnings
flutter analyze
# Check specific package
dart pub outdated
# View migration guide
dart fix --dry-run
dart fix --apply| Category | Recommended Package | Documentation |
|---|---|---|
| State Management | Riverpod ^2.6.0 | riverpod.dev |
| Navigation | go_router ^14.0+ | pub.dev/go_router |
| Networking | Dio ^5.4+ | pub.dev/dio |
| Local Storage | Hive ^4.0+ | pub.dev/hive |
| JSON Serialization | json_serializable ^6.8+ | pub.dev/json_serializable |
| Immutable Models | Freezed ^2.5+ | pub.dev/freezed |
| Dependency Injection | (Built into Riverpod) | N/A |
| Logging | Logger ^2.0+ | pub.dev/logger |
| Analytics | Firebase Analytics ^11.0+ | firebase.flutter.dev |
| Crash Reporting | Sentry ^8.0+ | pub.dev/sentry |
# Create new project
flutter create my_app --org com.example --platforms=ios,android,web
# Run with specific flavor
flutter run --flavor prod -t lib/main_prod.dart
# Build for production
flutter build apk --release --split-per-abi
flutter build ios --release
flutter build web --wasm # Future
# Analyze and fix issues
flutter analyze
dart fix --apply
# Update dependencies
flutter pub upgrade --major-versions
# Check null safety status
dart pub outdated --mode=null-safety
# Profile performance
flutter run --profile
# Generate code
dart run build_runner build --delete-conflicting-outputs
# Test with coverage
flutter test --coverage --machine > coverage.json- Flutter Official Docs - Primary Flutter documentation
- Dart Language Tour - Complete Dart language reference
- Pub.dev - Package repository with documentation
- API Reference - Complete Flutter API documentation
- Flutter Samples - Official sample applications
- Flutter Performance Best Practices
- Effective Dart
- Case Studies - Real-world Flutter success stories
- Flutter Blog - Official engineering blog
- Flutter on GitHub - Source code and issues
- Flutter Showcase - Apps built with Flutter
Before committing code, verify:
- ✅ No analyzer warnings (
flutter analyzepasses) - ✅ All tests pass (
flutter test) - ✅
constused wherever possible - ✅ No unnecessary rebuilds (check with DevTools)
- ✅ Null safety fully implemented (no
!abuses) - ✅ Proper error handling for async operations
- ✅ Semantic labels for accessibility
- ✅ Responsive layout for all screen sizes
- ✅ Deprecated APIs replaced
- ✅ Dependencies are up-to-date and null-safe
- ✅ Impeller rendering enabled (Flutter 3.27+)
- ✅ iOS and Android specific UI considerations addressed
As of 2026, both Android and iOS have introduced significant design updates—Material 3 Expressive and Apple's Liquid Glass—while the Flutter team has strategically paused direct integration of these features into core libraries. This guide provides comprehensive, actionable strategies for creating truly native-feeling Flutter apps on both platforms using current best practices and community-driven solutions.
Announced at Google I/O 2025 and rolling out with Android 16, Material 3 Expressive represents a significant evolution from Material You :
| Feature | Description | Implementation Need |
|---|---|---|
| Bouncier Animations | UI elements jiggle on swipe, snap back on dismiss | Custom animation controllers |
| Revamped Quick Settings | Smaller, categorized tiles with drag-and-drop | Platform channel integration |
| Enhanced Wallpaper Effects | Weather overlays (snow, rain, fog) with adjustable intensity | Not applicable to in-app UI |
| New Volume Panel | Sleeker layout with floating options | System UI, not app-controlled |
| Audio Sharing | LE Audio support for shared listening | Platform-specific feature |
Critical Note: The Flutter team has officially announced they are not actively developing Material 3 Expressive features in core, citing the complex migration from Material 2 to 3 and ongoing architectural reevaluation . The plan is to potentially decouple Material and Cupertino design systems into standalone packages for independent evolution .
Announced at WWDC 2025, Apple's Liquid Glass introduces a system-wide frosted-glass aesthetic :
| Feature | Description |
|---|---|
| Refractive Effects | Dynamic lighting that responds to device orientation |
| Depth & Layering | Multi-layered translucency for hierarchy communication |
| Contextual Blur | Adaptive blur strength based on content underneath |
| Glassmorphism | Subtle tinted glass effects with dynamic lighting |
Current Status: The Flutter team has stated that work is not currently planned to integrate Liquid Glass into core Cupertino widgets . This aligns with community sentiment that Flutter's strength is transcending platform limitations rather than chasing every design trend .
The Flutter team's decision to pause native design system integration is strategic :
- Decoupling Architecture: Moving Material and Cupertino to standalone packages for faster, independent evolution
- Core Focus Areas (2026 Roadmap):
- Completing Impeller migration on Android
- WebAssembly (Wasm) becoming default for web
- GenUI SDK for AI-driven adaptive interfaces
- Full-stack Dart support
- Community Leverage: Trusting the ecosystem to fill design gaps while core focuses on performance
Official Roadmap: Flutter & Dart's 2026 Roadmap
Flutter provides automatic platform adaptations for behaviors that would feel "wrong" if different . These work immediately without additional packages.
// Flutter automatically adapts navigation to platform
// Android: Zoom transition (ZoomPageTransitionsBuilder)
// iOS: Slide transition with parallax effect
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailScreen()),
);
// On iOS: slides from right (or left in RTL)
// On Android: zooms in from tapped element
// Full-screen modal (iOS-style bottom sheet)
Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true, // iOS: present-style modal
builder: (context) => ModalScreen(),
),
);| Platform | Back Behavior |
|---|---|
| Android | System back button pops top route automatically |
| iOS | Edge swipe gesture pops route, no system back button |
Both work automatically with Flutter's Navigator.
Flutter automatically applies platform-appropriate scrolling physics :
// These all adapt automatically
ListView.builder(...)
SingleChildScrollView(...)
CustomScrollView(...)
// iOS characteristics:
// - More weight and dynamic friction
// - Momentum stacking (repeated flings build speed)
// - Tapping status bar scrolls to top
// Android characteristics:
// - More static friction, less "slippery"
// - Glow overscroll indicator (not bounce)// MaterialApp automatically configures correct overscroll
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: MyHomePage(),
);
// Android: Glow indicator (colored by theme)
// iOS: Bounce with resistance// Automatic font selection
MaterialApp(
theme: ThemeData(
useMaterial3: true,
// Android: Roboto
// iOS: San Francisco (license restricts to Apple platforms)
),
);
// Platform-adaptive icons
Icon(Icons.adaptive.more_vert) // iOS: horizontal dots, Android: vertical dots
Icon(Icons.adaptive.arrow_back) // Platform-appropriate back arrow| Interaction | Android | iOS |
|---|---|---|
| Single tap | Cursor at tap location with handle | Cursor at nearest word edge |
| Long press | Selects word, shows toolbar | Places cursor, shows toolbar |
| Long press drag | Expands selection | Moves cursor |
| Keyboard gestures | Space bar swipe moves cursor | Force touch for floating cursor |
All handled automatically by TextField and CupertinoTextField.
Flutter automatically triggers platform-appropriate haptics :
| Action | Android | iOS |
|---|---|---|
| Long press text field | Buzz vibration | None |
| Picker item scroll | None | Light impact |
// Manual haptics when needed
import 'package:flutter/services.dart';
// Light impact (button press)
await HapticFeedback.lightImpact();
// Selection click (picker wheel)
await HapticFeedback.selectionClick();
// Heavy impact (error/confirmation)
await HapticFeedback.heavyImpact();Flutter provides .adaptive() constructors for components tightly integrated with OS expectations :
| Material Widget | Cupertino Widget | Adaptive Constructor |
|---|---|---|
Switch |
CupertinoSwitch |
Switch.adaptive() |
Slider |
CupertinoSlider |
Slider.adaptive() |
CircularProgressIndicator |
CupertinoActivityIndicator |
CircularProgressIndicator.adaptive() |
RefreshProgressIndicator |
CupertinoActivityIndicator |
RefreshIndicator.adaptive() |
Checkbox |
CupertinoCheckbox |
Checkbox.adaptive() |
Radio |
CupertinoRadio |
Radio.adaptive() |
AlertDialog |
CupertinoAlertDialog |
AlertDialog.adaptive() |
Important Note: AlertDialog.adaptive() requires manual button styling for complete native feel :
// Complete adaptive dialog with proper button styling
Future<void> showAdaptiveDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (BuildContext context) {
final theme = Theme.of(context);
return AlertDialog.adaptive(
title: const Text('Confirm'),
content: const Text('Are you sure?'),
actions: [
adaptiveAction(
context: context,
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
adaptiveAction(
context: context,
onPressed: () => Navigator.pop(context, true),
child: const Text('Confirm'),
),
],
);
},
);
}
Widget adaptiveAction({
required BuildContext context,
required VoidCallback onPressed,
required Widget child,
}) {
final theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return CupertinoDialogAction(
onPressed: onPressed,
child: child,
);
default:
return TextButton(
onPressed: onPressed,
child: child,
);
}
}// Complete platform-aware scaffold
import 'dart:io' show Platform;
class PlatformScaffold extends StatelessWidget {
final String title;
final Widget body;
final List<Widget>? actions;
const PlatformScaffold({
required this.title,
required this.body,
this.actions,
});
@override
Widget build(BuildContext context) {
if (Platform.isIOS) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(title),
trailing: actions != null && actions!.isNotEmpty
? Row(mainAxisSize: MainAxisSize.min, children: actions!)
: null,
),
child: body,
);
}
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: actions,
surfaceTintColor: Colors.transparent, // iOS-style minimalism
),
body: body,
);
}
}// Platform-appropriate bottom navigation
class AdaptiveBottomNavigation extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onTap;
final List<BottomNavigationItem> items;
const AdaptiveBottomNavigation({
required this.selectedIndex,
required this.onTap,
required this.items,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return CupertinoTabBar(
currentIndex: selectedIndex,
onTap: onTap,
items: items.map((item) => BottomNavigationBarItem(
icon: Icon(item.icon),
label: item.label,
)).toList(),
);
default:
return NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: onTap,
destinations: items.map((item) => NavigationDestination(
icon: Icon(item.icon),
label: item.label,
)).toList(),
);
}
}
}
class BottomNavigationItem {
final IconData icon;
final String label;
const BottomNavigationItem({required this.icon, required this.label});
}Since core Cupertino widgets won't immediately support Liquid Glass, the community has created solutions :
dependencies:
liquid_glass: ^0.1.0 # Community package for glassmorphism
# OR
liquid_glass_renderer: ^0.0.1 # Impeller-only versionNote: liquid_glass_renderer currently only supports the Impeller rendering engine .
Manual Implementation using BackdropFilter:
// Custom glassmorphism effect (approximates Liquid Glass)
class GlassmorphicCard extends StatelessWidget {
final Widget child;
final double blurStrength;
final double opacity;
const GlassmorphicCard({
required this.child,
this.blurStrength = 10,
this.opacity = 0.2,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: blurStrength, sigmaY: blurStrength),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(opacity),
Colors.white.withOpacity(opacity * 0.5),
],
),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
borderRadius: BorderRadius.circular(20),
),
child: child,
),
),
);
}
}Since core Material doesn't include the "bouncy" animations of M3 Expressive, implement custom animations :
// Spring-based bounce animation for Material 3 Expressive feel
class BouncyButton extends StatelessWidget {
final VoidCallback onPressed;
final Widget child;
const BouncyButton({
required this.onPressed,
required this.child,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => HapticFeedback.lightImpact(),
onTap: onPressed,
child: TweenAnimationBuilder(
tween: Tween<double>(begin: 1.0, end: 1.0),
duration: const Duration(milliseconds: 200),
curve: Curves.elasticOut,
builder: (context, scale, child) {
return Transform.scale(
scale: scale,
child: child,
);
},
child: Material(
elevation: 2,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: child,
),
),
),
),
);
}
}For comprehensive platform adaptation without conditional code:
dependencies:
extended_platform_widgets: ^1.0.0 # Fork supporting all platformsUsage:
// Instead of manual conditionals:
if (Platform.isAndroid) {
return ElevatedButton(...);
} else if (Platform.isIOS) {
return CupertinoButton(...);
}
// Use platform widget:
PlatformElevatedButton(
onPressed: () {},
child: Text('Button'),
)
// Renders: ElevatedButton on Android, CupertinoButton.filled on iOSThis library supports all Flutter platforms: Android, iOS, Web, macOS, Windows, Linux, Fuchsia .
// Custom page route with platform-appropriate transitions
class PlatformPageRoute<T> extends PageRoute<T> {
final WidgetBuilder builder;
PlatformPageRoute({required this.builder});
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final platform = Theme.of(context).platform;
if (platform == TargetPlatform.iOS) {
return CupertinoPageTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: false,
child: builder(context),
);
}
return FadeTransition(
opacity: animation,
child: builder(context),
);
}
// ... implement other required PageRoute methods
}Android 16 emphasizes adaptive layouts using window size classes—a concept equally applicable to Flutter :
// Implement Android 16's window size class concept
enum WindowSizeClass { compact, medium, expanded }
class AdaptiveLayout extends StatelessWidget {
final Widget compactLayout;
final Widget mediumLayout;
final Widget expandedLayout;
const AdaptiveLayout({
required this.compactLayout,
required this.mediumLayout,
required this.expandedLayout,
});
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final sizeClass = _getWindowSizeClass(width);
switch (sizeClass) {
case WindowSizeClass.expanded:
return expandedLayout;
case WindowSizeClass.medium:
return mediumLayout;
case WindowSizeClass.compact:
return compactLayout;
}
}
WindowSizeClass _getWindowSizeClass(double width) {
if (width >= 840) return WindowSizeClass.expanded;
if (width >= 600) return WindowSizeClass.medium;
return WindowSizeClass.compact;
}
}Android 16's Material 3 Adaptive library includes ListDetailPaneScaffold—implement similarly in Flutter :
// Two-pane layout (tablet/desktop) vs single-pane (phone)
class AdaptiveListDetailView extends StatelessWidget {
final List<Item> items;
final Widget Function(Item) detailBuilder;
const AdaptiveListDetailView({
required this.items,
required this.detailBuilder,
});
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final isTwoPane = width >= 600;
if (isTwoPane) {
return Row(
children: [
SizedBox(
width: width * 0.35,
child: _buildList(),
),
VerticalDivider(width: 1),
Expanded(
child: _buildDetail(selectedItem),
),
],
);
}
return _buildList();
}
Widget _buildList() => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
title: Text(items[index].title),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => detailBuilder(items[index]),
),
),
),
);
}// Test platform-specific behavior
void main() {
testWidgets('Shows Cupertino button on iOS', (tester) async {
// Set platform to iOS
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: PlatformElevatedButton(
onPressed: () {},
child: Text('Button'),
),
),
);
expect(find.byType(CupertinoButton), findsOneWidget);
// Reset
debugDefaultTargetPlatformOverride = null;
});
}| Requirement | Android | iOS | Implementation |
|---|---|---|---|
| Navigation bar | AppBar |
CupertinoNavigationBar |
Platform check or PlatformScaffold |
| Bottom tabs | NavigationBar |
CupertinoTabBar |
Platform check |
| Buttons | ElevatedButton |
CupertinoButton |
.adaptive() constructors |
| Dialogs | AlertDialog |
CupertinoAlertDialog |
AlertDialog.adaptive() with custom actions |
| Progress | CircularProgressIndicator |
CupertinoActivityIndicator |
.adaptive() constructor |
| Switches | Switch |
CupertinoSwitch |
Switch.adaptive() |
| Scroll physics | Glow overscroll | Bounce overscroll | Automatic |
| Back navigation | System back button | Edge swipe | Automatic |
| Haptics | Vibration on long press | Light impact on pickers | Automatic + manual |
| Typography | Roboto | San Francisco | Automatic in MaterialApp |
| Icons | Vertical dots | Horizontal dots | Icons.adaptive |
- Flutter Platform Adaptations - Official guide to automatic adaptations
- Flutter & Dart 2026 Roadmap - Strategic direction including design system decoupling
- Material 3 in Flutter - Official Material 3 implementation guide
- Cupertino Widgets - iOS-style widget reference
- Android 16 Adaptive Layouts - Compose Material 3 Adaptive patterns
- Material 3 Expressive Overview - Android 16 design features
- Extended Platform Widgets - Community package for platform adaptation
- Leverage built-in adaptations first – Navigation, scrolling, text editing, and haptics work automatically
- Use
.adaptive()constructors for switches, sliders, progress indicators, and alerts - Implement custom
platform_widgetsfor scaffolding and complex components - Monitor community packages for Liquid Glass and Material 3 Expressive effects
- Test on both platforms using
debugDefaultTargetPlatformOverride - Design responsively using window size classes for foldables and tablets
- Stay updated on Flutter's decoupling of Material/Cupertinto—expected to enable faster trend adoption
This guide provides a comprehensive catalog of Flutter's widget library organized by category, with the latest updates from Flutter 3.27+ and practical implementation patterns for MethodChannel.
The foundational building blocks for any Flutter application.
| Widget | Description | Key Use Case |
|---|---|---|
Container |
Combines painting, positioning, and sizing | Most common layout wrapper with padding, margin, decoration |
Row |
Horizontally arranges children | Horizontal layouts, button groups, form rows |
Column |
Vertically arranges children | Vertical stacks, forms, lists |
Stack |
Positions children relative to edges | Overlays, badges, floating elements |
IndexedStack |
Shows one child at a time by index | Tab content switching without rebuilding |
ListView |
Scrollable list of children | Long lists, chat messages, feeds |
GridView |
Scrollable 2D array of children | Photo galleries, product grids |
SingleChildScrollView |
Makes a single child scrollable | Forms that exceed screen height |
CustomScrollView |
Creates custom scroll effects | Sliver-based layouts, custom scroll behaviors |
With Flutter 3.27+, Material 3 is the default for new projects.
| Widget | Description | Latest Updates |
|---|---|---|
MaterialApp |
Top-level app container with Material theming | M3+ support for dynamic color schemes |
Scaffold |
Basic Material layout structure | Improved scroll-under behavior fixed in 3.27 |
AppBar |
Application bar with toolbar and actions | surfaceTintColor for elevation effect |
NavigationBar |
Bottom navigation (replaces BottomNavigationBar) | M3 replacement for BottomNavigationBar |
NavigationRail |
Side navigation for large screens | Ideal for tablets and desktop |
Drawer |
Slide-out navigation panel | Standard Material drawer |
TabBar + TabBarView |
Tab navigation with content | Connected tabs |
Migration Note: BottomNavigationBar is deprecated in favor of NavigationBar for Material 3.
| Widget | Description |
|---|---|
ElevatedButton |
Raised button with elevation (preferred for M3) |
FilledButton |
Solid background button (M3 primary action) |
FilledButton.icon |
Replaces ElevatedButton.icon |
TextButton |
Flat, text-only button |
OutlinedButton |
Button with border, no fill |
FloatingActionButton |
Circular action button—use .small or .large constructors |
IconButton |
Icon as interactive button |
| Widget | Description | M3 Status |
|---|---|---|
Checkbox |
Boolean selection | Use .adaptive() for cross-platform |
Radio |
Single selection from group | Standard M3 styling |
Switch |
On/off toggle | Use .adaptive() |
Slider |
Value selection sliding | M3 updated design |
SegmentedButton |
Button group for selection | Now supports vertical direction property |
TextField |
Text input with decoration | M3 styling |
DropdownMenu |
Enhanced dropdown with search | Improved focus handling in 3.27 |
| Widget | Description |
|---|---|
AlertDialog |
Interruptive confirmation dialog |
SimpleDialog |
Simple options dialog |
SnackBar |
Temporary bottom notification |
BottomSheet |
Modal or persistent bottom sheet |
Tooltip |
Help text on long press |
ProgressIndicator |
Linear and circular progress |
Flutter 3.27 brought major Cupertino improvements to match iOS 15+ design.
| Widget | Description | 3.27+ Updates |
|---|---|---|
CupertinoApp |
iOS-style app root | Standard |
CupertinoPageScaffold |
Basic iOS page structure | Standard |
CupertinoTabScaffold |
Tabbed iOS app structure | Standard |
CupertinoNavigationBar |
iOS navigation bar | Now has transparent background until scroll |
CupertinoSliverNavigationBar |
Collapsible navigation bar | Transparent background, smooth color transitions |
CupertinoTabBar |
Bottom tab bar | Standard |
| Widget | Description | 3.27+ Updates |
|---|---|---|
CupertinoButton |
iOS-style button | Now supports iOS 15+ styles: .filled, .tinted constructors |
CupertinoTextField |
iOS-style text input | Standard |
CupertinoSwitch |
iOS toggle switch | More configurable thumb images, fill colors |
CupertinoSlider |
iOS-style slider | Standard |
CupertinoCheckbox |
iOS checkbox | Refined sizes, colors, stroke widths |
CupertinoRadio |
iOS radio button | Updated press behaviors |
CupertinoSlidingSegmentedControl |
iOS segmented picker | Proportional layouts, disable individual segments |
CupertinoPicker |
iOS scroll wheel picker | Scroll directly to tapped items |
CupertinoDatePicker |
Date/time picker | Tapped item scrolling |
CupertinoAlertDialog |
iOS alert dialog | Tap-slide gesture support |
CupertinoContextMenu |
3D Touch-style menu | Improvements in 3.27 |
CupertinoActivityIndicator |
Loading spinner | Standard |
| Widget | Description |
|---|---|
Center |
Centers child within parent |
Align |
Positions child using alignment |
Padding |
Adds empty space around child |
SizedBox |
Fixed size box (also used for spacing) |
ConstrainedBox |
Applies min/max constraints |
Expanded |
Fills available space in Row/Column |
Flexible |
Can flex but not forced to expand |
AspectRatio |
Maintains aspect ratio |
FittedBox |
Scales child to fit |
Transform |
Applies matrix transformations |
Offstage |
Hides child without destroying state |
Opacity |
Makes child partially transparent |
ClipRect / ClipRRect / ClipOval / ClipPath |
Clips child with shapes |
| Widget | Description |
|---|---|
Row |
Horizontal arrangement |
Column |
Vertical arrangement |
Wrap |
Flows children to next line/column |
Flow |
Custom multi-child layouts |
Table |
Table layout algorithm |
ListBody |
Sequential layout along axis |
New in Flutter 3.27: Row and Column now have a spacing parameter for space between children.
| Widget | Description |
|---|---|
ListView |
Standard scrollable list |
GridView |
Scrollable grid |
SingleChildScrollView |
Single widget scroll |
CustomScrollView |
Custom sliver composition |
NestedScrollView |
Nested scrolling with floating app bars |
Scrollbar |
Scroll position indicator |
RefreshIndicator |
Pull-to-refresh gesture |
NotificationListener |
Listen to scroll notifications |
| Widget | Description |
|---|---|
Text |
Single-styled text |
RichText |
Multiple text styles inline |
SelectableText |
Text that can be copied |
TextField |
Text input (also in input category) |
DefaultTextStyle |
Inherited text style |
TextSpan |
Inline text fragments for RichText |
| Widget | Description |
|---|---|
Image |
Displays images (asset, network, file, memory) |
Icon |
Material Design icon |
CircleAvatar |
Circular image with fallback |
FadeInImage |
Image with fade-in placeholder |
RawImage |
Direct dart:ui image display |
| Widget | Description |
|---|---|
FutureBuilder |
Builds UI based on Future state |
StreamBuilder |
Builds UI based on Stream emissions |
| Widget | Description |
|---|---|
AnimatedContainer |
Animates container properties |
AnimatedOpacity |
Fades widget in/out |
AnimatedPadding |
Animates padding changes |
AnimatedAlign |
Animates alignment |
AnimatedPositioned |
Animates position in Stack |
AnimatedCrossFade |
Fades between two children |
Hero |
Shared element transitions |
AnimatedBuilder |
Custom animation builder |
AnimatedList |
Animated list insert/remove |
| Widget | Description |
|---|---|
GestureDetector |
Detects various gestures |
InkWell |
Material ripple effect + gesture |
Draggable / DragTarget |
Drag and drop |
Dismissible |
Swipe-to-dismiss |
LongPressDraggable |
Long press then drag |
IgnorePointer |
Ignores touch input |
AbsorbPointer |
Absorbs touch input |
| Widget | Description |
|---|---|
Theme |
Applies Material theme |
MediaQuery |
Provides screen size/orientation |
LayoutBuilder |
Builds based on parent constraints |
OrientationBuilder |
Builds based on orientation |
| Widget | Description |
|---|---|
Semantics |
Describes widget meaning for screen readers |
MergeSemantics |
Groups semantic nodes |
ExcludeSemantics |
Removes from accessibility tree |
New in Flutter 3.27: CarouselView.weighted allows dynamic layouts with flex weights:
| Constructor | Description |
|---|---|
CarouselView |
Standard carousel |
CarouselView.weighted |
Custom relative item weights (multi-browse, hero, centered-hero layouts) |
Platform channels enable communication between Flutter and native platform code for features not available in Flutter packages.
Channel Types:
MethodChannel- Invoke methods and receive return valuesEventChannel- Stream events from native to FlutterBasicMessageChannel- Send raw messages
import 'package:flutter/services.dart';
// Define channel with unique identifier
static const platform = MethodChannel('com.example.app/native');
// Call method and handle response
Future<String> getBatteryLevel() async {
try {
final String result = await platform.invokeMethod('getBatteryLevel');
return result;
} on PlatformException catch (e) {
print("Failed: '${e.message}'");
return "Unknown";
}
}
// With parameters
Future<void> saveToGallery(Uint8List imageBytes) async {
try {
await platform.invokeMethod('saveImage', {'image': imageBytes});
} on PlatformException catch (e) {
print("Save failed: ${e.message}");
}
}class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.app/native"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"getBatteryLevel" -> getBatteryLevel(result)
"saveImage" -> saveImage(call.arguments, result)
else -> result.notImplemented()
}
}
}
private fun getBatteryLevel(result: MethodChannel.Result) {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available", null)
}
}
private fun saveImage(arguments: Any?, result: MethodChannel.Result) {
val args = arguments as Map<String, Any>
val bytes = args["image"] as ByteArray
// Save implementation
result.success(true)
}
}@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(
name: "com.example.app/native",
binaryMessenger: controller.binaryMessenger
)
batteryChannel.setMethodCallHandler { (call, result) in
switch call.method {
case "getBatteryLevel":
self.getBatteryLevel(result: result)
case "saveImage":
self.saveImage(call: call, result: result)
default:
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func getBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
let level = device.batteryLevel
if level != -1.0 {
result(Int(level * 100))
} else {
result(FlutterError(code: "UNAVAILABLE", message: "Battery level unavailable", details: nil))
}
}
private func saveImage(call: FlutterMethodCall, result: FlutterResult) {
guard let args = call.arguments as? [String: Any],
let imageData = args["image"] as? FlutterStandardTypedData else {
result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
return
}
// Save implementation
result(true)
}
}Flutter Side:
import 'package:flutter/services.dart';
class NativeSensorService {
static const EventChannel _sensorChannel = EventChannel('com.example.app/sensors');
Stream<double> get accelerometerStream {
return _sensorChannel.receiveBroadcastStream()
.map((event) => event['x'] as double);
}
}Android Side:
class MainActivity: FlutterActivity() {
private var eventSink: EventChannel.EventSink? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
EventChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.app/sensors")
.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
startSensorListening()
}
override fun onCancel(arguments: Any?) {
stopSensorListening()
eventSink = null
}
})
}
private fun onSensorChanged(x: Float, y: Float, z: Float) {
eventSink?.success(mapOf("x" to x, "y" to y, "z" to z))
}
}iOS Side:
class AppDelegate: FlutterAppDelegate {
private var eventSink: FlutterEventSink?
override func application(...) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let sensorChannel = FlutterEventChannel(
name: "com.example.app/sensors",
binaryMessenger: controller.binaryMessenger
)
sensorChannel.setStreamHandler(self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
extension AppDelegate: FlutterStreamHandler {
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
startSensorUpdates()
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
stopSensorUpdates()
eventSink = nil
return nil
}
}Error Handling:
// Always handle PlatformException
try {
await platform.invokeMethod('method');
} on PlatformException catch (e) {
switch (e.code) {
case 'PERMISSION_DENIED':
// Request permission
break;
case 'UNAVAILABLE':
// Show fallback
break;
default:
// Generic error
}
}Type Safety:
// Define method names as constants
class NativeMethods {
static const String getBattery = 'getBatteryLevel';
static const String saveImage = 'saveImage';
}
// Define argument keys
class NativeArgs {
static const String image = 'image';
}| Use Case | Reason |
|---|---|
| Hardware sensors | Camera, gyroscope, proximity |
| File system | Advanced file operations |
| Biometric auth | Face ID, fingerprint |
| Local notifications | Rich notifications |
| Bluetooth | BLE communication |
| NFC | Payment, scanning |
| Platform-specific UI | Native maps, video players |
| Battery/Network info | System monitoring |
| Deprecated | Replacement |
|---|---|
ElevatedButton.icon |
FilledButton.icon |
TextButton.icon |
FilledButton.icon |
OutlinedButton.icon |
Use standard OutlinedButton |
BottomNavigationBar |
NavigationBar |
Chip (generic) |
FilterChip, AssistChip, InputChip, ElevatedChip |
FloatingActionButton (default) |
FloatingActionButton.large or .small |
PopupMenuButton |
MenuAnchor (more flexible) |
| Deprecated | Replacement |
|---|---|
ThemeData.dialogBackgroundColor |
DialogThemeData.backgroundColor |
MaterialStateProperty<T> |
WidgetStateProperty<T> |
flutter create --platforms=android,ios --template=app my_app| Need | Start Here |
|---|---|
| Simple container | Container |
| List of items | ListView.builder |
| Grid of items | GridView.builder |
| Form with inputs | Form + TextField |
| Two-pane layout | Row with Expanded or NavigationRail |
| Modal overlay | AlertDialog or showModalBottomSheet |
| Loading indicator | CircularProgressIndicator.adaptive() |
| Page navigation | Navigator + GoRouter |
| User toggle | Switch.adaptive() |
| Multiple selection | Checkbox or SegmentedButton |
Platform channels enable communication between Flutter and native platform code. Understanding the architecture is crucial for effective implementation.
Channel Types:
- MethodChannel: Invoke methods and receive return values (most common)
- EventChannel: Stream events from native to Flutter
- BasicMessageChannel: Send raw messages for custom protocols
Supported Platforms & Languages:
| Platform | Languages |
|---|---|
| Android | Kotlin, Java |
| iOS | Swift, Objective-C |
| macOS | Objective-C |
| Windows | C++ |
| Linux | C |
import 'package:flutter/services.dart';
import 'dart:developer' as developer;
class NativeBatteryService {
static const MethodChannel _channel = MethodChannel('com.example.app/battery');
// Singleton pattern for channel services
static final NativeBatteryService _instance = NativeBatteryService._internal();
factory NativeBatteryService() => _instance;
NativeBatteryService._internal();
// Public API with proper error handling
Future<int> getBatteryLevel() async {
try {
final int level = await _channel.invokeMethod('getBatteryLevel');
return level;
} on PlatformException catch (e) {
developer.log(
'Failed to get battery level',
name: 'NativeBatteryService',
level: 1000,
error: e,
);
// Re-throw typed exceptions for caller handling
switch (e.code) {
case 'UNAVAILABLE':
throw BatteryUnavailableException();
case 'PERMISSION_DENIED':
throw BatteryPermissionException();
default:
throw BatteryException(e.message ?? 'Unknown error');
}
}
}
// Method with parameters
Future<bool> setPowerSavingMode(bool enabled) async {
try {
return await _channel.invokeMethod('setPowerSavingMode', {'enabled': enabled});
} on PlatformException catch (e) {
developer.log('Power saving mode failed', error: e);
return false;
}
}
// Streaming data from native
Stream<double> getBatteryLevelStream() {
const EventChannel eventChannel = EventChannel('com.example.app/battery_stream');
return eventChannel.receiveBroadcastStream().map((event) => event as double);
}
}class MainActivity: FlutterActivity() {
private val BATTERY_CHANNEL = "com.example.app/battery"
private val BATTERY_STREAM_CHANNEL = "com.example.app/battery_stream"
private var eventSink: EventChannel.EventSink? = null
private var batteryLevelHandler: BatteryLevelHandler? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Method Channel
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BATTERY_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"getBatteryLevel" -> getBatteryLevel(result)
"setPowerSavingMode" -> setPowerSavingMode(call, result)
else -> result.notImplemented()
}
}
// Event Channel for streaming
EventChannel(flutterEngine.dartExecutor.binaryMessenger, BATTERY_STREAM_CHANNEL)
.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
startBatteryMonitoring()
}
override fun onCancel(arguments: Any?) {
stopBatteryMonitoring()
eventSink = null
}
})
}
private fun getBatteryLevel(result: MethodChannel.Result) {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available", null)
}
}
private fun setPowerSavingMode(call: MethodCall, result: MethodChannel.Result) {
val enabled = call.argument<Boolean>("enabled") ?: false
// Implementation for power saving mode
result.success(true)
}
private fun startBatteryMonitoring() {
// Register battery level listener
batteryLevelHandler = BatteryLevelHandler { level ->
eventSink?.success(level)
}
}
private fun stopBatteryMonitoring() {
batteryLevelHandler?.unregister()
batteryLevelHandler = null
}
}@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private let BATTERY_CHANNEL = "com.example.app/battery"
private let BATTERY_STREAM_CHANNEL = "com.example.app/battery_stream"
private var eventSink: FlutterEventSink?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
// Method Channel
let batteryChannel = FlutterMethodChannel(
name: BATTERY_CHANNEL,
binaryMessenger: controller.binaryMessenger
)
batteryChannel.setMethodCallHandler { [weak self] call, result in
switch call.method {
case "getBatteryLevel":
self?.getBatteryLevel(result: result)
case "setPowerSavingMode":
self?.setPowerSavingMode(call: call, result: result)
default:
result(FlutterMethodNotImplemented)
}
}
// Event Channel
let batteryEventChannel = FlutterEventChannel(
name: BATTERY_STREAM_CHANNEL,
binaryMessenger: controller.binaryMessenger
)
batteryEventChannel.setStreamHandler(self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func getBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
let level = device.batteryLevel
if level != -1.0 {
result(Int(level * 100))
} else {
result(FlutterError(
code: "UNAVAILABLE",
message: "Battery level unavailable",
details: nil
))
}
}
private func setPowerSavingMode(call: FlutterMethodCall, result: FlutterResult) {
guard let args = call.arguments as? [String: Any],
let enabled = args["enabled"] as? Bool else {
result(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
// Implementation
result(true)
}
}
extension AppDelegate: FlutterStreamHandler {
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
startBatteryMonitoring()
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
stopBatteryMonitoring()
eventSink = nil
return nil
}
}For production apps, use the Pigeon package for type-safe platform channel code generation .
# pubspec.yaml
dev_dependencies:
pigeon: ^22.0.0// pigeon.dart (input file)
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(PigeonOptions(
dartOut: 'lib/native_api.g.dart',
kotlinOut: 'android/app/src/main/kotlin/com/example/NativeApi.g.kt',
swiftOut: 'ios/Runner/NativeApi.g.swift',
))
@HostApi()
abstract class BatteryApi {
int getBatteryLevel();
@async
Future<bool> setPowerSavingMode(bool enabled);
}Run generation:
dart run pigeon --input pigeon.dart- Use unique, reverse-domain channel names:
com.yourcompany.appname/feature - Handle
notImplementedcase: Always callresult.notImplemented()for unknown methods - Use async/await with try-catch: Wrap all
invokeMethodcalls - Keep native handlers thin: Parse arguments, call business logic, return results
- Document method contracts: Specify argument types and return values
- Use constants for method names: Avoid string literals scattered in code
- Implement proper error handling: Return structured errors with codes
- Don't block the main thread: Channel calls are synchronous on native side
- Don't put business logic in channel handlers: Delegate to services
- Don't ignore exceptions: Always handle PlatformException
- Don't use channels for trivial operations: High-frequency calls can impact performance
- Don't share channel names across features: Each feature should have its own channel
| Use Case | Why Platform Channel |
|---|---|
| Biometric Authentication | Native Face ID/Touch ID APIs |
| Camera Customization | Advanced camera features beyond camera plugin |
| File System Access | Access to external storage, document pickers |
| Bluetooth/BLE | Low-level Bluetooth control |
| NFC | Payment, tag reading |
| Background Services | Push notifications, background sync |
| Hardware Sensors | Gyroscope, proximity, temperature |
| Platform-Specific UI | Native maps, video players, WebView |
| Existing Native SDKs | Payment SDKs, analytics, CRM integration |
// Unit test with mocked MethodChannel
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
const MethodChannel batteryChannel = MethodChannel('com.example.app/battery');
test('getBatteryLevel returns expected value', () async {
// Setup mock response
batteryChannel.setMockMethodCallHandler((call) async {
if (call.method == 'getBatteryLevel') {
return 75;
}
return null;
});
final service = NativeBatteryService();
final level = await service.getBatteryLevel();
expect(level, 75);
});
tearDown(() {
batteryChannel.setMockMethodCallHandler(null);
});
}Flutter 3.27 brought significant improvements to iOS-style widgets, bringing them closer to Apple's Human Interface Guidelines .
| Widget | Key Updates |
|---|---|
CupertinoCheckbox |
New sizes, colors, stroke widths, press behaviors |
CupertinoRadio |
Mouse cursors, semantic labels, fill colors |
CupertinoSwitch |
Thumb images, fill colors, track color renamed |
CupertinoSlidingSegmentedControl |
Thumb radius, separator height, proportional layout, disable individual segments |
CupertinoNavigationBar |
Transparent background until scroll |
CupertinoSliverNavigationBar |
Colors interpolate during scroll |
CupertinoButton |
iOS 15+ styles via sizeStyle, .tinted constructor, onLongPress |
CupertinoPicker/CupertinoDatePicker |
Scroll to tapped items |
CupertinoAlertDialog |
Tap-slide gesture support |
CupertinoActionSheet |
Proper padding/font sizes across text settings, haptic feedback |
CupertinoContextMenu |
Scrollable when actions overflow |
CupertinoMagnifier |
Zoom effect with magnification scale |
With Material 3 as default, several widgets have been deprecated :
| Deprecated | Replacement | Notes |
|---|---|---|
ElevatedButton.icon |
FilledButton.icon |
M3 recommends FilledButton for primary actions |
TextButton.icon |
FilledButton.icon |
Icon buttons now use FilledButton |
OutlinedButton.icon |
OutlinedButton |
Icon constructor removed |
BottomNavigationBar |
NavigationBar |
Complete redesign for M3 |
Chip (generic) |
FilterChip, AssistChip, InputChip, ElevatedChip |
More specific chip types |
FloatingActionButton (default) |
.large or .small constructors |
Explicit size required |
PopupMenuButton |
MenuAnchor |
More flexible menu control |
MaterialStateProperty<T> |
WidgetStateProperty<T> |
Renamed for clarity |
ThemeData.dialogBackgroundColor |
DialogThemeData.backgroundColor |
Organized under dialog theme |
ChipThemeData (M2) |
ChipTheme (M3) |
New theming structure |
CarouselView.weighted: Dynamic carousel layouts with flex weights
CarouselView.weighted(
flexWeights: [2, 1, 1, 2], // Custom relative sizing
children: [...],
)Row/Column Spacing: Direct spacing parameter removes need for SizedBox between children
Row(
spacing: 16, // New in 3.27
children: [...],
)SegmentedButton Vertical Direction:
SegmentedButton(
direction: Axis.vertical, // New property
segments: [...],
selected: {...},
)ModalRoute Transition Improvements: Exit transitions now sync with enter transitions for smoother navigation
| Test Type | Proportion | Description | Execution Time |
|---|---|---|---|
| Unit Tests | 70% | Business logic, models, services | Milliseconds |
| Widget Tests | 20% | Individual widget behavior | Seconds |
| Integration Tests | 10% | Full user flows | Minutes |
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
// Create mock
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late UserService service;
late MockUserRepository mockRepository;
setUp(() {
mockRepository = MockUserRepository();
service = UserService(mockRepository);
});
group('getUser', () {
test('returns user when found', () async {
// Arrange
const userId = '123';
final expectedUser = User(id: userId, name: 'Test');
when(() => mockRepository.fetchUser(userId))
.thenAnswer((_) async => expectedUser);
// Act
final result = await service.getUser(userId);
// Assert
expect(result, expectedUser);
verify(() => mockRepository.fetchUser(userId)).called(1);
});
test('throws UserNotFound when user missing', () async {
when(() => mockRepository.fetchUser(any()))
.thenThrow(UserNotFoundException());
expect(() => service.getUser('999'), throwsA(isA<UserNotFoundException>()));
});
});
}import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Button matches golden file', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.lightTheme,
home: Scaffold(
body: Center(
child: MyCustomButton(label: 'Click Me'),
),
),
),
);
await expectLater(
find.byType(MyCustomButton),
matchesGoldenFile('goldens/my_button.png'),
);
});
}// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Complete login and navigation flow', (tester) async {
app.main();
await tester.pumpAndSettle();
// Login
await tester.enterText(find.byKey(const Key('email_field')), 'user@example.com');
await tester.enterText(find.byKey(const Key('password_field')), 'password');
await tester.tap(find.byKey(const Key('login_button')));
await tester.pumpAndSettle(const Duration(seconds: 2));
// Verify navigation to home
expect(find.byType(HomeScreen), findsOneWidget);
// Navigate to details
await tester.tap(find.byKey(const Key('item_0')));
await tester.pumpAndSettle();
expect(find.byType(DetailScreen), findsOneWidget);
});
}Run integration tests:
flutter test integration_testGenerate coverage report:
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.htmlMinimum coverage targets:
- Core business logic: 100%
- Repositories/Services: 90%
- BLoC/Providers: 85%
- UI Widgets: 70%
- Overall: 80%
| Level | Requirement | Flutter Implementation |
|---|---|---|
| A (Minimum) | Non-text content has text alternative | Semantics(label: 'description') |
| A | Keyboard navigable | Focus and FocusNode widgets |
| AA | Color contrast 4.5:1 | Use ColorScheme with sufficient contrast |
| AA | Text resize 200% | Test with MediaQuery.textScaler |
| AA | Focus visible | FocusHighlight widget |
// Accessible custom card widget
class AccessibleCard extends StatelessWidget {
final String title;
final String description;
final VoidCallback onTap;
final String? imageUrl;
const AccessibleCard({
required this.title,
required this.description,
required this.onTap,
this.imageUrl,
});
@override
Widget build(BuildContext context) {
return Semantics(
label: '$title. $description',
hint: 'Double tap to view details',
button: true,
enabled: true,
onTap: onTap,
child: FocusableActionDetector(
onFocusChange: (hasFocus) {
// Announce focus change for screen readers
if (hasFocus) {
SemanticsService.announce('$title card focused', Directionality.of(context));
}
},
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
if (imageUrl != null)
ExcludeSemantics(
child: Image.asset(
imageUrl!,
width: 50,
height: 50,
excludeFromSemantics: true,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
),
),
),
);
}
}class ScalableApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
// Respect system font scale
final mediaQuery = MediaQuery.of(context);
final textScaler = mediaQuery.textScaler.clamp(
minScale: 1.0,
maxScale: 2.0,
);
return MediaQuery(
data: mediaQuery.copyWith(textScaler: textScaler),
child: child,
);
},
home: const MyHomePage(),
);
}
}testWidgets('Accessibility labels are present', (tester) async {
await tester.pumpWidget(const AccessibleCard(
title: 'Test',
description: 'Description',
onTap: () {},
));
final semantics = tester.getSemantics(find.byType(AccessibleCard));
expect(semantics.label, 'Test. Description');
expect(semantics.hasFlag(SemanticsFlag.isButton), true);
});// providers/counter_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_provider.g.dart';
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
@riverpod
class FilteredTodos extends _$FilteredTodos {
@override
List<Todo> build() {
final todos = ref.watch(todoListProvider);
final filter = ref.watch(todoFilterProvider);
return switch (filter) {
TodoFilter.completed => todos.where((t) => t.completed).toList(),
TodoFilter.active => todos.where((t) => !t.completed).toList(),
TodoFilter.all => todos,
};
}
}
// Usage in UI
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
final counterNotifier = ref.read(counterProvider.notifier);
return Row(
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: counterNotifier.decrement,
),
Text('$count'),
IconButton(
icon: const Icon(Icons.add),
onPressed: counterNotifier.increment,
),
],
);
}
}// Event
abstract class LoginEvent {}
class LoginEmailChanged extends LoginEvent {
final String email;
LoginEmailChanged(this.email);
}
class LoginPasswordChanged extends LoginEvent {
final String password;
LoginPasswordChanged(this.password);
}
class LoginSubmitted extends LoginEvent {}
// State
abstract class LoginState {}
class LoginInitial extends LoginState {}
class LoginLoading extends LoginState {}
class LoginSuccess extends LoginState {
final User user;
LoginSuccess(this.user);
}
class LoginFailure extends LoginState {
final String error;
LoginFailure(this.error);
}
// BLoC
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthRepository authRepository;
LoginBloc(this.authRepository) : super(LoginInitial()) {
on<LoginEmailChanged>((event, emit) {
// Handle email change
});
on<LoginSubmitted>((event, emit) async {
emit(LoginLoading());
try {
final user = await authRepository.login();
emit(LoginSuccess(user));
} catch (e) {
emit(LoginFailure(e.toString()));
}
});
}
}# pubspec.yaml
flutter:
generate: true # Enable code generation
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart// lib/l10n/app_en.arb
{
"appTitle": "My App",
"welcomeMessage": "Welcome, {name}!",
"@welcomeMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"itemsCount": "{count, plural, =0{No items} one{1 item} other{{count} items}}"
}import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context)!;
return Column(
children: [
Text(t.appTitle),
Text(t.welcomeMessage('John')),
Text(t.itemsCount(5)),
],
);
}
}MaterialApp(
locale: Locale('ar'), // Arabic
supportedLocales: const [
Locale('en'),
Locale('ar'),
],
localizationsDelegates: AppLocalizations.localizationsDelegates,
// Automatically handles RTL layout
);- No unnecessary rebuilds (check with
PerformanceOverlay) -
ListView.builderused for long lists - Images with proper caching (
cached_network_image) - Animations use
RepaintBoundary - No expensive operations in
build()method -
constconstructors used where possible - App size optimized (remove unused assets)
- No hardcoded API keys or secrets
- Sensitive data stored in secure storage
- Network traffic encrypted (HTTPS)
- Certificate pinning for critical APIs
- Proguard/R8 enabled for Android (minifyEnabled)
- iOS code obfuscation enabled
- Input validation for all user inputs
- Proper session management with refresh tokens
- All interactive elements have semantic labels
- Color contrast ratio ≥ 4.5:1
- Dynamic type scaling supported
- Focus navigation works on all platforms
- Tested with TalkBack (Android) and VoiceOver (iOS)
- iOS:
- App icons (all sizes)
- Launch screen storyboard
- Info.plist properly configured
- Swift Package Manager dependencies set
- Android:
- Adaptive icons
- Edge-to-edge display handling
- Predictive back gesture (Android 14+)
- Material You dynamic colors
-
flutter analyzepasses with 0 warnings -
dart format --output=none --set-exit-if-changed .passes - Test coverage ≥ 80%
- No
print()statements in production - Proper logging with
developer.logorloggerpackage - All async operations have error handling
- Android:
- Signing configuration for release builds
- Proguard rules configured
- Split APK by ABI
- iOS:
- Distribution certificate
- Provisioning profile
- App Store Connect metadata complete
- Web:
- PWA manifest configured
- Service worker registered
- SEO meta tags
- Flutter Official Documentation - Complete Flutter documentation
- Dart Language Tour - Dart language reference
- Widget Catalog - Official widget index
- Flutter API Reference - Complete API docs
- Material 3 in Flutter - Material Design guidelines
- Cupertino Widgets - iOS-style widgets
- Platform Channels Guide
- DevTools Documentation - Performance profiling
- Integration Testing - End-to-end testing
- Pigeon Package - Type-safe channels
- Flutter Blog - Official engineering blog
- Flutter on GitHub - Source code
- What's New in Flutter - Release notes