Advanced
Group 20: BLoC/Cubit State Management
Example 56: Cubit
Cubit is a simplified BLoC that emits new states via methods instead of events. It is part of the flutter_bloc package and provides predictable, testable state management. Cubit is the recommended starting point - upgrade to full BLoC only when you need event-driven traceability.
graph LR
A["UI Widget<br/>BlocBuilder"] -->|"cubit.method#40;#41;"| B["Cubit<br/>emits state"]
B -->|"new state"| A
style A fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
style B fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; // => flutter_bloc: ^8.1.0
// State class: immutable snapshot of what the UI should show
class CounterState {
final int count;
final String label;
const CounterState({required this.count, required this.label});
}
// Cubit: encapsulates state + business logic, emits new states on method calls
class CounterCubit extends Cubit<CounterState> {
// super() sets the initial state
CounterCubit() : super(const CounterState(count: 0, label: 'Start counting'));
void increment() {
// emit() replaces current state and notifies all BlocBuilder listeners
emit(CounterState(
count: state.count + 1, // => Increment count
label: state.count + 1 >= 10 // => Update label at threshold
? 'Keep going!'
: 'Count: ${state.count + 1}',
));
}
void decrement() {
if (state.count <= 0) return; // => Guard: don't go below zero
emit(CounterState(
count: state.count - 1,
label: state.count - 1 == 0 ? 'Start counting' : 'Count: ${state.count - 1}',
));
}
void reset() => emit(const CounterState(count: 0, label: 'Reset!'));
}
void main() {
runApp(
// BlocProvider creates the Cubit and makes it available to the subtree
BlocProvider(
create: (context) => CounterCubit(), // => Factory creates cubit instance
child: const MaterialApp(home: CounterPage()),
),
);
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Cubit Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// BlocBuilder rebuilds only when state changes
BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) { // => Receives current state
return Column(
children: [
Text(state.label, // => Reactive label from state
style: const TextStyle(fontSize: 20)),
const SizedBox(height: 8),
Text('${state.count}',
style: const TextStyle(fontSize: 72,
fontWeight: FontWeight.bold)),
],
);
},
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
// context.read accesses cubit without subscribing to rebuilds
onPressed: () => context.read<CounterCubit>().decrement(),
child: const Text('-'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: () => context.read<CounterCubit>().increment(),
child: const Text('+'),
),
const SizedBox(width: 16),
TextButton(
onPressed: () => context.read<CounterCubit>().reset(),
child: const Text('Reset'),
),
],
),
],
),
),
);
}
}
// => Counter state managed through Cubit with BlocBuilder reactive UIKey Takeaway: Cubit emits states via emit(); BlocProvider makes the cubit available; BlocBuilder rebuilds when state changes; context.read calls cubit methods from callbacks without subscribing.
Why It Matters: Cubit provides three critical production benefits over raw setState: testability (you can unit test cubit logic without Flutter), scalability (business logic is separated from UI), and debuggability (every state transition is logged by BlocObserver). Teams using BLoC/Cubit can write bloc tests that verify every state transition independently of the widget tree, reducing integration test burden significantly.
Example 57: BLoC with Events
Full BLoC pattern separates intent (events) from response (states). Events describe what happened; states describe what the UI should show. Use BLoC instead of Cubit when you need event history for debugging, multiple handlers for the same event, or event transformations (debounce, throttle).
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Events: describe user actions or system triggers
abstract class SearchEvent {}
class SearchQueryChanged extends SearchEvent {
final String query;
SearchQueryChanged(this.query);
}
class SearchCleared extends SearchEvent {}
// States: describe what the UI should render
abstract class SearchState {}
class SearchInitial extends SearchState {}
class SearchLoading extends SearchState {}
class SearchResults extends SearchState {
final List<String> results;
final String query;
SearchResults({required this.results, required this.query});
}
class SearchError extends SearchState {
final String message;
SearchError(this.message);
}
// BLoC: maps events to states via EventHandlers
class SearchBloc extends Bloc<SearchEvent, SearchState> {
SearchBloc() : super(SearchInitial()) {
// on<EventType> registers a handler for events of that type
on<SearchQueryChanged>(_onQueryChanged);
on<SearchCleared>(_onCleared);
}
// Handler runs when SearchQueryChanged event is added
Future<void> _onQueryChanged(
SearchQueryChanged event,
Emitter<SearchState> emit,
) async {
if (event.query.isEmpty) {
emit(SearchInitial()); // => Clear results for empty query
return;
}
emit(SearchLoading()); // => Show loading indicator
try {
await Future.delayed(const Duration(milliseconds: 300)); // => Simulate search API
// Simulate filtering items that match the query
final allItems = List.generate(20, (i) => 'Flutter item ${i + 1}');
final results = allItems
.where((item) => item.toLowerCase().contains(event.query.toLowerCase()))
.toList();
emit(SearchResults(results: results, query: event.query));
// => Emit results state with matching items
} catch (e) {
emit(SearchError('Search failed: $e')); // => Emit error state on exception
}
}
void _onCleared(SearchCleared event, Emitter<SearchState> emit) {
emit(SearchInitial()); // => Reset to initial state
}
}
void main() => runApp(BlocProvider(
create: (_) => SearchBloc(),
child: const MaterialApp(home: SearchPage()),
));
class SearchPage extends StatelessWidget {
const SearchPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BLoC Search')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
decoration: const InputDecoration(
hintText: 'Search...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.search),
),
onChanged: (query) {
// add() dispatches an event to the BLoC
context.read<SearchBloc>().add(SearchQueryChanged(query));
// => Event enters the BLoC stream, triggers _onQueryChanged
},
),
),
Expanded(
child: BlocBuilder<SearchBloc, SearchState>(
builder: (context, state) {
if (state is SearchInitial) {
return const Center(child: Text('Type to search...'));
}
if (state is SearchLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is SearchError) {
return Center(child: Text(state.message,
style: const TextStyle(color: Colors.red)));
}
final results = state as SearchResults;
if (results.results.isEmpty) {
return Center(child: Text('No results for "${results.query}"'));
}
return ListView.builder(
itemCount: results.results.length,
itemBuilder: (context, i) => ListTile(
title: Text(results.results[i]),
),
);
},
),
),
],
),
);
}
}
// => Search dispatches events; BLoC maps them to Loading/Results/Error statesKey Takeaway: BLoC maps events to states via on<EventType> handlers; add() dispatches events; emit() pushes new states to all BlocBuilder listeners; sealed event/state class hierarchies make all cases explicit and exhaustive.
Why It Matters: BLoC is the architecture of choice for large Flutter teams because every state transition is explicit, logged, and testable. BlocObserver can log every event and state change globally - invaluable for debugging complex user flows in production. The event/state separation also enables features like undo (replay previous events) and analytics (track every user action as a stream of events).
Group 21: Platform Interop
Example 58: dart:js_interop
dart:js_interop (Dart 3.3+) provides static, type-safe JavaScript interop. Use @JS() annotations to call browser APIs and JavaScript libraries from Dart. This replaces the older dart:html and package:js approaches with better null safety and wasm compatibility.
import 'package:flutter/material.dart';
import 'dart:js_interop'; // => Dart 3.3+ static JS interop
// Declare JS bindings using extension types
@JS('window') // => Refers to the JS window object
external JSObject get window; // => external = implemented in JS
// Declare individual window methods as external functions
@JS('window.alert')
external void jsAlert(String message); // => Calls browser alert()
@JS('window.confirm')
external bool jsConfirm(String message); // => Calls browser confirm()
// Declare localStorage using extension type
extension type JSLocalStorage._(JSObject _) implements JSObject {
external void setItem(String key, String value); // => localStorage.setItem
external String? getItem(String key); // => localStorage.getItem
external void removeItem(String key); // => localStorage.removeItem
}
@JS('window.localStorage')
external JSLocalStorage get localStorage; // => Access window.localStorage
void main() => runApp(const MaterialApp(home: JsInteropDemo()));
class JsInteropDemo extends StatefulWidget {
const JsInteropDemo({super.key});
@override
State<JsInteropDemo> createState() => _JsInteropDemoState();
}
class _JsInteropDemoState extends State<JsInteropDemo> {
String _storedValue = '';
@override
void initState() {
super.initState();
// Read from localStorage on startup
_storedValue = localStorage.getItem('flutter_demo') ?? 'Nothing stored yet';
}
void _saveToLocalStorage() {
const value = 'Flutter Web saved this at runtime';
localStorage.setItem('flutter_demo', value); // => Persists across page refreshes
setState(() => _storedValue = value);
}
void _showBrowserAlert() {
jsAlert('Hello from Flutter Web via dart:js_interop!');
// => Calls native browser alert() dialog
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('JS Interop')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('localStorage value:',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(_storedValue), // => Shows current localStorage value
],
),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _saveToLocalStorage,
child: const Text('Save to localStorage'),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: _showBrowserAlert,
child: const Text('Call browser alert()'),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () {
localStorage.removeItem('flutter_demo'); // => Clear stored value
setState(() => _storedValue = 'Cleared');
},
child: const Text('Clear localStorage'),
),
],
),
),
);
}
}
// => Flutter calls browser APIs directly via dart:js_interopKey Takeaway: Use @JS() annotations with external declarations to bind Dart code to JavaScript APIs; dart:js_interop provides compile-time type safety and works with both dart2js and dart2wasm compilation.
Why It Matters: Production Flutter Web apps frequently need to call browser APIs not exposed by Flutter - analytics scripts, third-party SDKs, native browser storage, clipboard API, and Web Audio. dart:js_interop is the Dart 3.3+ standard for this, replacing the older dart:html approach which is incompatible with WebAssembly compilation. Migrating to dart:js_interop is required for apps targeting Flutter’s WASM output.
Example 59: IndexedDB via dart:js_interop
Browser IndexedDB provides a persistent, transactional key-value store with index support. It handles larger data volumes than localStorage (which is limited to ~5MB) and supports complex queries. Use it for offline-capable Flutter Web apps.
import 'dart:js_interop';
import 'package:flutter/material.dart';
import 'package:web/web.dart' as web; // => package:web ^0.5.0 for typed web APIs
void main() => runApp(const MaterialApp(home: IndexedDBDemo()));
// High-level wrapper around IndexedDB operations
class LocalDatabase {
static const _dbName = 'FlutterWebDB';
static const _storeName = 'notes';
web.IDBDatabase? _db;
Future<void> open() async {
// Open database with version 1, create store if needed
final request = web.window.indexedDB.open(_dbName, 1);
// onupgradeneeded fires when DB is created or version increases
request.onupgradeneeded = (web.IDBVersionChangeEvent event) {
final db = request.result as web.IDBDatabase;
if (!db.objectStoreNames.contains(_storeName)) {
db.createObjectStore(_storeName,
web.IDBObjectStoreParameters(keyPath: 'id'.toJS)); // => 'id' field is the key
}
}.toJS;
// Wait for success
final completer = Completer<void>();
request.onsuccess = (web.Event _) {
_db = request.result as web.IDBDatabase;
completer.complete();
}.toJS;
request.onerror = (web.Event _) =>
completer.completeError('Failed to open DB').toJS;
return completer.future;
}
Future<void> put(Map<String, dynamic> note) async {
final tx = _db!.transaction(_storeName.toJS, 'readwrite');
tx.objectStore(_storeName).put(note.jsify()); // => Store JS-ified Dart map
}
Future<List<Map<String, dynamic>>> getAll() async {
// ... implementation queries all records
return [];
}
}
class IndexedDBDemo extends StatelessWidget {
const IndexedDBDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('IndexedDB')),
body: const Padding(
padding: EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('IndexedDB Demo', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 12),
Text(
'IndexedDB provides persistent browser storage beyond localStorage.\n'
'- Stores structured data (objects, arrays)\n'
'- Supports transactions for data integrity\n'
'- No size limit (depends on disk space)\n'
'- Indexed queries for fast lookups\n'
'- Survives page refreshes and browser close',
),
],
),
),
);
}
}
// => IndexedDB stores structured data with transactions and indexesKey Takeaway: Use package:web for typed browser API access; IndexedDB requires a database open + version upgrade pattern; all operations are async request-based with onsuccess/onerror handlers.
Why It Matters: Offline-capable progressive web apps need robust client-side storage. localStorage is synchronous and limited to 5MB of strings. IndexedDB stores gigabytes of structured data, supports indexing for fast queries, and operates asynchronously to avoid blocking the main thread. Production PWAs use IndexedDB for offline caching of user data, queued writes that sync when connectivity returns, and large media metadata stores.
Example 60: WebSocket Real-time Communication
WebSocket enables full-duplex communication between the browser and server. In Flutter Web, use dart:html’s WebSocket or package:web’s WebSocket for direct browser API access, or wrap it in a Dart Stream for reactive integration.
import 'package:flutter/material.dart';
import 'dart:async';
// WebSocket client wrapper using Streams
class WebSocketService {
// Use package:web or dart:html for browser WebSocket
// This example shows the conceptual Stream-based wrapper pattern
final StreamController<String> _incomingMessages =
StreamController<String>.broadcast(); // => broadcast: multiple listeners allowed
Stream<String> get messages => _incomingMessages.stream; // => Expose read-only stream
// In real implementation: use web.WebSocket(url) from package:web
void simulateConnection() {
// Simulate receiving messages from server
Timer.periodic(const Duration(seconds: 2), (timer) {
if (_incomingMessages.isClosed) {
timer.cancel(); // => Stop if stream closed
return;
}
_incomingMessages.add(
'Server message at ${DateTime.now().toIso8601String()}');
// => Each message is pushed to all stream subscribers
});
}
void send(String message) {
// In real implementation: webSocket.send(message)
_incomingMessages.add('Echo: $message'); // => Simulate echo from server
}
void dispose() {
_incomingMessages.close(); // => Close stream to release resources
}
}
void main() => runApp(const MaterialApp(home: WebSocketDemo()));
class WebSocketDemo extends StatefulWidget {
const WebSocketDemo({super.key});
@override
State<WebSocketDemo> createState() => _WebSocketDemoState();
}
class _WebSocketDemoState extends State<WebSocketDemo> {
final _service = WebSocketService();
final _controller = TextEditingController();
final List<String> _messages = []; // => Accumulated message history
@override
void initState() {
super.initState();
_service.simulateConnection(); // => Start receiving messages
// Subscribe to the message stream
_service.messages.listen((message) {
if (mounted) {
setState(() => _messages.insert(0, message)); // => Prepend newest at top
if (_messages.length > 50) _messages.removeLast(); // => Keep last 50
}
});
}
@override
void dispose() {
_service.dispose(); // => Close WebSocket connection
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('WebSocket'),
actions: [
// Connection status indicator
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
width: 12,
height: 12,
decoration: const BoxDecoration(
color: Colors.green, // => Green = connected
shape: BoxShape.circle,
),
),
],
),
body: Column(
children: [
// Message input row
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Send a message...',
border: OutlineInputBorder(),
),
onSubmitted: (text) {
if (text.isNotEmpty) {
_service.send(text); // => Send message to server
_controller.clear();
}
},
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.send),
onPressed: () {
final text = _controller.text;
if (text.isNotEmpty) {
_service.send(text);
_controller.clear();
}
},
),
],
),
),
// Messages list
Expanded(
child: ListView.builder(
reverse: true, // => Newest at bottom
itemCount: _messages.length,
itemBuilder: (context, index) => ListTile(
dense: true,
title: Text(_messages[index]),
),
),
),
],
),
);
}
}
// => Real-time message stream displayed as they arrive from serverKey Takeaway: Wrap browser WebSocket in a Dart StreamController.broadcast() for reactive integration; subscribe in initState, unsubscribe in dispose; check mounted before calling setState in stream callbacks.
Why It Matters: Real-time features - live dashboards, chat, collaborative editing, live notifications - require WebSocket. The StreamController.broadcast() wrapper transforms the callback-based WebSocket API into a Dart Stream that integrates naturally with StreamBuilder and Riverpod StreamProvider. The mounted check in the stream listener is critical because the async listener can fire after the widget is disposed.
Group 22: PWA and Production Web
Example 61: PWA Configuration
Flutter Web generates a Progressive Web App by default. The manifest.json and service worker in the web/ folder control installation prompts, offline behavior, and app appearance. Customizing these makes your Flutter Web app installable as a native-like desktop or mobile app.
// This example shows the web/ folder files that configure PWA behavior
// web/manifest.json - Controls how the app appears when installed
/*
{
"name": "My Flutter App",
"short_name": "FlutterApp",
"start_url": ".",
"display": "standalone",
"background_color": "#0173B2",
"theme_color": "#0173B2",
"description": "A Flutter Web PWA example",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{ "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
{ "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
*/
// web/index.html head section - SEO and PWA meta tags
/*
<head>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="Your app description for SEO">
<!-- iOS PWA support -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="My Flutter App">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- OpenGraph for social sharing -->
<meta property="og:title" content="My Flutter App">
<meta property="og:description" content="Built with Flutter Web">
<meta property="og:image" content="https://myapp.com/icons/og-image.png">
<!-- Theme color for browser chrome -->
<meta name="theme-color" content="#0173B2">
<title>My Flutter App</title>
<link rel="manifest" href="manifest.json">
</head>
*/
import 'package:flutter/material.dart';
// Dart code: detect if running as installed PWA
bool get isInstalledPWA {
// When installed as PWA, display-mode is 'standalone'
// This is checked via JS media query
// In production, use dart:js_interop to check:
// window.matchMedia('(display-mode: standalone)').matches
return false; // => Placeholder - implement with js_interop in production
}
void main() => runApp(const MaterialApp(home: PWAInfoPage()));
class PWAInfoPage extends StatelessWidget {
const PWAInfoPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PWA Configuration')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('PWA Status',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text('Running as PWA: ${isInstalledPWA ? "Yes" : "No"}'),
// => Shows if app is running in standalone (installed) mode
const SizedBox(height: 4),
const Text('PWA features:\n'
'• Installable from browser\n'
'• Offline support via service worker\n'
'• Custom splash screen\n'
'• Standalone window (no browser chrome)\n'
'• Push notifications (with permission)'),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Renderer Selection',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
const Text(
'Flutter Web renderer options:\n\n'
'• auto (default): html for mobile, canvaskit for desktop\n'
'• canvaskit: pixel-perfect, consistent, larger bundle (~2MB)\n'
'• html: lighter bundle, uses DOM elements\n'
'• wasm (experimental): dart2wasm + skwasm renderer\n\n'
'Build: flutter build web --web-renderer canvaskit',
),
],
),
),
),
],
),
),
);
}
}Key Takeaway: Configure web/manifest.json for installation metadata; add meta tags in web/index.html for SEO and iOS PWA support; choose renderer with --web-renderer canvaskit for pixel-perfect rendering or html for smaller bundles.
Why It Matters: PWA installability and proper meta tags are the difference between a Flutter Web experiment and a production-quality web application. Without manifest.json, browsers will not offer the “Add to Home Screen” prompt. Without OpenGraph tags, link sharing on social media shows blank previews. Without proper description meta tags, search engine indexing is degraded. These are the polish details that distinguish professional Flutter Web deployments.
Group 23: Performance
Example 62: const Constructors and Widget Caching
const constructors create compile-time constant widgets that Flutter can skip rebuilding entirely. Using const throughout your widget tree is the simplest and most impactful performance optimization available in Flutter.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ConstDemo()));
// This widget's entire subtree is compile-time constant
// Flutter will never rebuild it regardless of parent rebuilds
class StaticHeader extends StatelessWidget {
const StaticHeader({super.key}); // => const constructor required
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(16), // => const EdgeInsets
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Every child is const - entire subtree is cached
Text('Welcome to Flutter Web', // => const Text literal
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold, // => const TextStyle
),
),
SizedBox(height: 8),
Text('Build fast, beautiful web apps'), // => const Text
SizedBox(height: 4),
Row(
children: [
Icon(Icons.check, color: Colors.teal, size: 16),
SizedBox(width: 4),
Text('Pixel-perfect rendering'),
],
),
],
),
);
}
}
class ConstDemo extends StatefulWidget {
const ConstDemo({super.key});
@override
State<ConstDemo> createState() => _ConstDemoState();
}
class _ConstDemoState extends State<ConstDemo> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('const Performance')), // => const AppBar title
body: Column(
children: [
// StaticHeader is const - even though _counter changes, this never rebuilds
const StaticHeader(), // => const prevents rebuild on setState
// This Text DOES rebuild on setState because it uses _counter
Text('Counter: $_counter', // => Cannot be const (uses variable)
style: const TextStyle(fontSize: 24)), // => TextStyle IS const
const SizedBox(height: 16), // => const spacer never rebuilds
ElevatedButton(
onPressed: () => setState(() => _counter++), // => Only Text above rebuilds
child: const Text('Increment'), // => const button label
),
],
),
);
}
}
// => StaticHeader never rebuilds despite counter state changesKey Takeaway: const widgets are compiled to single instances that Flutter skips in rebuild cycles; const is the highest-impact, lowest-effort Flutter performance optimization; apply it to every widget that does not depend on runtime state.
Why It Matters: Flutter’s rendering engine checks each widget to decide if it needs rebuilding. const widgets are identical object instances - Flutter skips them entirely with a pointer comparison. In a complex list with 50 items each containing 10 sub-widgets, const can prevent 500 widget comparisons on every frame. This is not premature optimization - it is fundamental Flutter best practice recommended by the Flutter team as a first-pass optimization.
Example 63: RepaintBoundary and Performance Profiling
RepaintBoundary isolates a subtree into its own render layer. Animations or frequent updates inside the boundary do not trigger repaints outside it. This is essential for animated widgets inside large, complex screens.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: RepaintDemo()));
// This widget updates frequently (animation)
class AnimatedCounter extends StatefulWidget {
const AnimatedCounter({super.key});
@override
State<AnimatedCounter> createState() => _AnimatedCounterState();
}
class _AnimatedCounterState extends State<AnimatedCounter>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true); // => Continuous loop
_animation = Tween<double>(begin: 0, end: 1).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, _) => Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color.lerp(Colors.blue, Colors.orange, _animation.value), // => Animated color
),
child: Center(
child: Text(
'${(_animation.value * 100).toInt()}%',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
);
}
}
class ExpensiveStaticWidget extends StatelessWidget {
const ExpensiveStaticWidget({super.key});
@override
Widget build(BuildContext context) {
// Simulates expensive layout computation (many widgets)
return Column(
children: List.generate(
20,
(i) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Icon(Icons.circle, size: 12, color: Colors.grey.shade400),
const SizedBox(width: 8),
Text('Static content item ${i + 1}'),
],
),
),
),
);
}
}
class RepaintDemo extends StatelessWidget {
const RepaintDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('RepaintBoundary')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// WITHOUT RepaintBoundary: animated widget repaints entire Column on every frame
// WITH RepaintBoundary: only the boundary layer repaints, Column is unaffected
RepaintBoundary(
// => Everything inside this boundary is isolated in its own compositing layer
// => Animation repaints ONLY this layer, not the Column or ExpensiveStaticWidget
child: const AnimatedCounter(),
),
const SizedBox(height: 16),
// This expensive static widget is NOT repainted during animation
const RepaintBoundary(
// => Promotes ExpensiveStaticWidget to its own layer
// => Flutter compositor can skip it during animation frames
child: ExpensiveStaticWidget(),
),
// Flutter DevTools tips card
const SizedBox(height: 16),
const Card(
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
'Profile with Flutter DevTools:\n'
'• flutter run --profile -d chrome\n'
'• Open DevTools: flutter pub global run devtools\n'
'• Enable "Highlight Repaints" to visualize boundaries\n'
'• Use Timeline to identify jank frames (>16ms)',
),
),
),
],
),
),
);
}
}
// => Animation inside RepaintBoundary does not repaint the static list below itKey Takeaway: Wrap frequently-updating widgets (animations, live data) in RepaintBoundary to isolate their repaints; use Flutter DevTools “Highlight Repaints” mode to identify unnecessary repaints; run --profile mode for accurate performance measurements.
Why It Matters: A 60fps animation that causes the entire screen to repaint on every frame is a performance disaster on complex screens. RepaintBoundary tells the Flutter compositor to manage the animated widget on its own GPU layer, leaving static content undisturbed. This is how production Flutter Web apps with live dashboards, animated charts, and scrolling lists maintain 60fps without GPU overload.
Group 24: Accessibility and Internationalization
Example 64: Semantics Widget
Semantics provides accessibility metadata to screen readers. Every interactive widget that lacks a text label needs a Semantics wrapper with a descriptive label. The excludeSemantics property removes a widget from the accessibility tree when it is purely decorative.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: SemanticsDemo()));
class SemanticsDemo extends StatefulWidget {
const SemanticsDemo({super.key});
@override
State<SemanticsDemo> createState() => _SemanticsDemoState();
}
class _SemanticsDemoState extends State<SemanticsDemo> {
bool _isLiked = false;
int _likeCount = 42;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Accessibility / Semantics')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icon-only button REQUIRES Semantics label
Semantics(
label: _isLiked ? 'Unlike post' : 'Like post', // => Screen reader reads this
hint: 'Double-tap to toggle like', // => Usage hint
button: true, // => Identifies as interactive button
child: GestureDetector(
onTap: () => setState(() {
_isLiked = !_isLiked;
_likeCount += _isLiked ? 1 : -1;
}),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_isLiked ? Icons.favorite : Icons.favorite_border,
color: _isLiked ? Colors.red : Colors.grey,
),
const SizedBox(width: 4),
Text('$_likeCount'),
],
),
),
),
// => Screen reader says "Like post, button. Double-tap to toggle like."
const SizedBox(height: 16),
// Decorative image excluded from semantics tree
Semantics(
excludeSemantics: true, // => Hidden from screen readers
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0173B2), Color(0xFF029E73)],
),
borderRadius: BorderRadius.circular(8),
),
),
),
// => Decorative gradient is excluded from accessibility tree
const SizedBox(height: 8),
const Text('Decorative gradient above is hidden from screen readers'),
const SizedBox(height: 16),
// MergeSemantics combines children into one semantics node
MergeSemantics(
child: Row(
children: [
const Icon(Icons.schedule, size: 16),
const SizedBox(width: 4),
const Text('Published'),
const SizedBox(width: 4),
const Text('March 19, 2026'),
],
),
),
// => Screen reader reads all as one: "Published March 19, 2026"
// => Without MergeSemantics: reads icon + "Published" + date separately
const SizedBox(height: 16),
// Focus traversal order for keyboard accessibility
FocusTraversalGroup(
policy: OrderedTraversalPolicy(), // => Traverse in widget order
child: Column(
children: [
ElevatedButton(
autofocus: true, // => This button gets focus on page load
onPressed: () {},
child: const Text('First Action (auto-focused)'),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () {},
child: const Text('Second Action'),
),
],
),
),
],
),
),
);
}
}
// => Screen readers can navigate all interactive elements with proper labelsKey Takeaway: Wrap icon-only interactions in Semantics with label and button: true; use excludeSemantics: true for decorative elements; MergeSemantics combines multiple nodes into one readable announcement.
Why It Matters: WCAG 2.1 Level AA compliance is legally required for public-facing web applications in many jurisdictions. Semantics is how Flutter maps its widget tree to the browser’s accessibility tree (ARIA). Icon buttons without labels are one of the most common WCAG violations in Flutter Web apps. The autofocus property ensures keyboard users land on the primary action when a page loads, satisfying WCAG 2.4.3 (Focus Order).
Example 65: Internationalization with flutter_localizations
flutter_localizations + intl package provide multi-language support. ARB (Application Resource Bundle) files store translated strings. AppLocalizations.of(context) retrieves the current locale’s translations. The MaterialApp.localizationsDelegates and supportedLocales configure available languages.
import 'package:flutter/material.dart';
// In production: import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// Simplified mock for demonstration - in production, generated from ARB files
class AppStrings {
final String welcome;
final String signIn;
final String language;
const AppStrings({
required this.welcome,
required this.signIn,
required this.language,
});
static const en = AppStrings(
welcome: 'Welcome to Flutter Web',
signIn: 'Sign In',
language: 'Language',
);
static const id = AppStrings(
welcome: 'Selamat datang di Flutter Web',
signIn: 'Masuk',
language: 'Bahasa',
);
}
// In production, use proper ARB files:
// lib/l10n/app_en.arb
// {
// "@@locale": "en",
// "welcome": "Welcome to Flutter Web",
// "@welcome": { "description": "Welcome message on home screen" },
// "signIn": "Sign In"
// }
//
// lib/l10n/app_id.arb
// {
// "@@locale": "id",
// "welcome": "Selamat datang di Flutter Web",
// "signIn": "Masuk"
// }
void main() => runApp(const I18nDemo());
class I18nDemo extends StatefulWidget {
const I18nDemo({super.key});
@override
State<I18nDemo> createState() => _I18nDemoState();
}
class _I18nDemoState extends State<I18nDemo> {
Locale _locale = const Locale('en'); // => Start with English
AppStrings get _strings => _locale.languageCode == 'id'
? AppStrings.id : AppStrings.en; // => Select strings for current locale
@override
Widget build(BuildContext context) {
return MaterialApp(
locale: _locale, // => Override the app locale
// In production: include flutter_localizations delegates
// localizationsDelegates: AppLocalizations.localizationsDelegates,
// supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
appBar: AppBar(title: Text(_strings.language)),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_strings.welcome,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
// => Shows "Welcome to Flutter Web" or "Selamat datang..." based on locale
const SizedBox(height: 24),
// Language selector
Row(
children: [
ChoiceChip(
label: const Text('English'),
selected: _locale.languageCode == 'en',
onSelected: (_) => setState(() => _locale = const Locale('en')),
),
const SizedBox(width: 8),
ChoiceChip(
label: const Text('Indonesian'),
selected: _locale.languageCode == 'id',
onSelected: (_) => setState(() => _locale = const Locale('id')),
),
],
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {},
child: Text(_strings.signIn), // => Localized button label
),
],
),
),
),
);
}
}
// => UI switches between English and Indonesian on locale changeKey Takeaway: Store translations in ARB files under lib/l10n/; run flutter gen-l10n to generate typed AppLocalizations; access with AppLocalizations.of(context)!.messageKey; configure supportedLocales and localizationsDelegates in MaterialApp.
Why It Matters: Internationalization cannot be added as an afterthought. Hard-coded strings in widgets require a full codebase search to localize. Building with flutter_localizations from the start means adding a new language requires only a new ARB file, not code changes. For global applications, proper i18n including right-to-left (TextDirection.rtl) support for Arabic and Hebrew is a market requirement, not a nice-to-have.
Group 25: Deferred Loading and Deployment
Example 66: Deferred Loading (Code Splitting)
Dart’s deferred as keyword splits code into separate JavaScript bundles loaded on demand. This reduces the initial bundle size of Flutter Web apps, improving time-to-first-render. Load heavy features (charts, editors, rarely-accessed screens) lazily.
// Heavy feature module (saved as lib/features/chart_feature.dart)
// import 'package:fl_chart/fl_chart.dart'; // => Heavy dependency only in this file
library chart_feature;
import 'package:flutter/material.dart';
class ChartPage extends StatelessWidget {
const ChartPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('Chart feature loaded!\n'
'(In production: fl_chart or similar heavy library)'),
),
);
}
}
// ===
// main app file (lib/main.dart):
import 'package:flutter/material.dart';
// Deferred import: code is NOT included in the initial bundle
// => Loaded as a separate .js chunk when loadLibrary() is called
import 'features/chart_feature.dart' deferred as chartFeature;
void main() => runApp(const MaterialApp(home: DeferredDemo()));
class DeferredDemo extends StatefulWidget {
const DeferredDemo({super.key});
@override
State<DeferredDemo> createState() => _DeferredDemoState();
}
class _DeferredDemoState extends State<DeferredDemo> {
bool _isLoaded = false; // => Whether deferred library is loaded
bool _isLoading = false; // => Download in progress
Future<void> _loadChartFeature() async {
setState(() => _isLoading = true);
// loadLibrary() downloads the deferred chunk from the server
await chartFeature.loadLibrary(); // => Network request for .js chunk
// => After this, chartFeature.ChartPage can be instantiated
// => Subsequent calls to loadLibrary() are instant (already cached)
setState(() {
_isLoaded = true;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Deferred Loading')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Chart feature uses deferred loading.\n'
'It is not in the initial bundle.',
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
if (!_isLoaded)
ElevatedButton(
onPressed: _isLoading ? null : _loadChartFeature,
child: _isLoading
? const Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(width: 16, height: 16,
child: CircularProgressIndicator(strokeWidth: 2)),
SizedBox(width: 8),
Text('Loading chart module...'),
],
)
: const Text('Load Chart Feature'),
)
else
// chartFeature.ChartPage() only available after loadLibrary()
const chartFeature.ChartPage(), // => Instantiate the deferred widget
],
),
),
);
}
}
// => ChartPage downloads on demand, not in initial page loadKey Takeaway: import '...' deferred as name excludes the module from the initial bundle; await name.loadLibrary() downloads it on demand; classes in the module can only be used after loadLibrary() completes.
Why It Matters: Flutter Web’s initial bundle can grow to 5-10MB for apps with many dependencies. Deferred loading splits heavy features into separate chunks. Users accessing only the main features never download the chart library, editor, or admin panel code. This pattern can reduce Time to Interactive by 40-60% for feature-rich apps. Google and the Flutter team recommend deferred loading as the primary bundle size optimization for production Flutter Web.
Example 67: Docker and nginx Deployment
Deploying Flutter Web with Docker and nginx is the production-standard approach for containerized deployments. The multi-stage build compiles the app in a Flutter SDK container, then copies only the built assets into a minimal nginx container.
// This example shows deployment configuration files, not Dart code.
// The Flutter app code is standard - the deployment is what matters here.
// Dockerfile (place in project root)
/*
# Stage 1: Build Flutter Web app
FROM ghcr.io/cirruslabs/flutter:3.22.0 AS builder
WORKDIR /app
# Copy dependency files first for Docker layer caching
COPY pubspec.yaml pubspec.lock ./
RUN flutter pub get # => Download dependencies (cached if unchanged)
# Copy source code
COPY . .
# Build for web with CanvasKit renderer for pixel-perfect output
RUN flutter build web \
--release \
--web-renderer canvaskit \
--dart-define=FLUTTER_WEB_CANVASKIT_URL=canvaskit/
# => Outputs to build/web/
# Stage 2: Serve with nginx (only built assets, no Flutter SDK)
FROM nginx:alpine AS production
# Copy nginx config that handles Flutter Web SPA routing
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built Flutter Web files from builder stage
COPY --from=builder /app/build/web /usr/share/nginx/html
# => Final image is ~50MB instead of ~2GB with Flutter SDK
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
*/
// nginx.conf (place in project root)
/*
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Enable gzip for faster transfer
gzip on;
gzip_types text/plain text/css application/javascript application/json
application/wasm image/svg+xml;
# Cache static assets aggressively (they have content hashes in filenames)
location ~* \.(js|wasm|png|jpg|svg|ico|css|json)$ {
expires 1y;
add_header Cache-Control "public, immutable";
# => Browsers cache assets for 1 year (safe because filenames include hash)
}
# SPA fallback: any unknown path serves index.html
# Required for Flutter Web path-based URLs (GoRouter)
location / {
try_files $uri $uri/ /index.html;
# => /about/team -> tries file, tries dir, falls back to index.html
# => Flutter JS app then handles routing client-side
}
# Health check endpoint for load balancers
location /health {
return 200 'OK';
add_header Content-Type text/plain;
}
}
*/
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: DeploymentInfoPage()));
class DeploymentInfoPage extends StatelessWidget {
const DeploymentInfoPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Deployment')),
body: const Padding(
padding: EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Build commands:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('flutter build web --release --web-renderer canvaskit'),
SizedBox(height: 16),
Text('Docker commands:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('docker build -t my-flutter-web .'),
Text('docker run -p 8080:80 my-flutter-web'),
SizedBox(height: 16),
Text('Key nginx configuration:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('• try_files \$uri /index.html → SPA routing fallback'),
Text('• expires 1y → Cache hashed assets forever'),
Text('• gzip on → Compress text assets for transfer'),
],
),
),
);
}
}
// => Multi-stage Docker build produces minimal nginx image serving Flutter WebKey Takeaway: Multi-stage Dockerfile: build in Flutter SDK container, copy build/web/ to nginx container; nginx must include try_files $uri /index.html for SPA routing; cache static assets with expires 1y since Flutter uses content-hashed filenames.
Why It Matters: The try_files $uri /index.html directive is the single most important nginx configuration for Flutter Web. Without it, users who access https://myapp.com/products/42 directly get a 404 - nginx looks for a file at that path, finds nothing, and fails. With it, all paths fall back to Flutter’s index.html, which then handles routing client-side via GoRouter. Every production Flutter Web deployment requires this configuration.
Example 68: Environment Configuration
Production Flutter Web apps need different API endpoints, feature flags, and keys per environment. --dart-define passes compile-time constants; --dart-define-from-file reads from a JSON file. This avoids hard-coded environment values in source code.
import 'package:flutter/material.dart';
// Constants read from --dart-define at build time
// Build commands:
// Dev: flutter run -d chrome --dart-define=API_URL=http://localhost:8080
// Prod: flutter build web --dart-define=API_URL=https://api.myapp.com --dart-define=ENV=production
class AppConfig {
// String.fromEnvironment reads --dart-define values at compile time
static const apiUrl = String.fromEnvironment(
'API_URL',
defaultValue: 'http://localhost:8080', // => Fallback for local dev
);
static const environment = String.fromEnvironment(
'ENV',
defaultValue: 'development', // => Default to development
);
static const bool isProduction = environment == 'production';
// => true only when built with --dart-define=ENV=production
static const featureFlags = {
'newDashboard': bool.fromEnvironment('FEATURE_NEW_DASHBOARD', defaultValue: false),
// => Enable with --dart-define=FEATURE_NEW_DASHBOARD=true
};
}
void main() {
// Log config at startup (remove in production)
if (!AppConfig.isProduction) {
// ignore: avoid_print
print('API URL: ${AppConfig.apiUrl}'); // => Shows resolved API URL in debug
// ignore: avoid_print
print('Environment: ${AppConfig.environment}');
}
runApp(const MaterialApp(home: ConfigDemo()));
}
class ConfigDemo extends StatelessWidget {
const ConfigDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Environment Config')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Current Configuration',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text('API URL: ${AppConfig.apiUrl}'), // => Compile-time constant
Text('Environment: ${AppConfig.environment}'),
Text('Production: ${AppConfig.isProduction}'),
],
),
),
),
const SizedBox(height: 16),
// Conditionally show dev tools only in non-production
if (!AppConfig.isProduction)
Card(
color: Colors.orange.shade50,
child: const Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Expanded(child: Text('Development mode: debug tools visible')),
],
),
),
),
// Feature flag conditional rendering
if (AppConfig.featureFlags['newDashboard'] == true)
const Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('New Dashboard feature is enabled'),
),
),
],
),
),
);
}
}
// => API URL and feature flags set at build time via --dart-defineKey Takeaway: String.fromEnvironment('KEY', defaultValue: '...') reads --dart-define=KEY=value compile-time constants; these are baked into the bundle at build time, not runtime; never use --dart-define for secrets (they are visible in the compiled JS).
Why It Matters: Hard-coded API endpoints are the most common source of environment bugs - production apps accidentally calling development APIs, or development builds hitting production databases. --dart-define with environment-specific CI/CD pipeline variables solves this cleanly. Feature flags enable trunk-based development where incomplete features are deployed but disabled in production, reducing long-lived feature branches and their associated merge conflicts.
Example 69: Custom RenderObject
RenderObject is Flutter’s lowest-level rendering primitive. Subclassing RenderBox gives you complete control over layout and painting - bypassing the widget layer entirely. Use this for maximum performance in custom layout algorithms or highly optimized rendering.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; // => Import rendering layer
// Custom RenderBox: lays out children in a circle
class RenderCircleLayout extends RenderBox
with ContainerRenderObjectMixin<RenderBox, BoxParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, BoxParentData> {
double _radius; // => Circle radius in logical pixels
RenderCircleLayout({required double radius}) : _radius = radius;
double get radius => _radius;
set radius(double value) {
if (_radius == value) return;
_radius = value;
markNeedsLayout(); // => Trigger layout recalculation
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! BoxParentData) {
child.parentData = BoxParentData(); // => Initialize parent data for each child
}
}
@override
void performLayout() {
// Report our own size
size = Size(_radius * 2, _radius * 2); // => We take a square of diameter 2*radius
var child = firstChild;
int index = 0;
final childCount = this.childCount;
while (child != null) {
child.layout(const BoxConstraints(), parentUsesSize: true); // => Unconstrained children
// Position each child evenly around the circle
final angle = (index / childCount) * 2 * 3.14159; // => Angle in radians
final x = _radius + _radius * 0.7 * (angle * 0.0).abs() - child.size.width / 2;
final y = _radius - _radius * 0.7 - child.size.height / 2;
// Simplified positioning for example clarity
final parentData = child.parentData as BoxParentData;
parentData.offset = Offset(
_radius + (_radius * 0.7) * (index % 2 == 0 ? 1 : -1) - child.size.width / 2,
_radius * (index / childCount) - child.size.height / 2,
);
child = childAfter(child);
index++;
}
}
@override
void paint(PaintingContext context, Offset offset) {
// Draw circle background
context.canvas.drawCircle(
offset + Offset(_radius, _radius), // => Center of circle
_radius,
Paint()..color = Colors.blue.shade50, // => Light blue background
);
// Paint children
defaultPaint(context, offset); // => Paint each child at its offset
}
}
// Widget wrapper for the custom RenderObject
class CircleLayout extends MultiChildRenderObjectWidget {
final double radius;
const CircleLayout({
super.key,
required this.radius,
required super.children,
});
@override
RenderObject createRenderObject(BuildContext context) =>
RenderCircleLayout(radius: radius); // => Create the RenderBox
@override
void updateRenderObject(BuildContext context, RenderCircleLayout renderObject) {
renderObject.radius = radius; // => Update when widget rebuilds
}
}
void main() => runApp(const MaterialApp(home: RenderObjectDemo()));
class RenderObjectDemo extends StatelessWidget {
const RenderObjectDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Custom RenderObject')),
body: Center(
child: CircleLayout(
radius: 120, // => 120px radius circle
children: List.generate(
6,
(i) => Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.primaries[i * 2],
shape: BoxShape.circle,
),
child: Center(child: Text('${i + 1}',
style: const TextStyle(color: Colors.white))),
),
),
),
),
);
}
}
// => Six colored circles positioned by custom RenderBox layout algorithmKey Takeaway: Subclass RenderBox, override performLayout() for custom layout algorithms and paint() for custom drawing; expose as a widget via MultiChildRenderObjectWidget; markNeedsLayout() triggers relayout when properties change.
Why It Matters: Custom RenderObject is the escape hatch for performance-critical or layout-algorithm-critical situations where the widget layer adds overhead. Radial menus, custom grid systems, physics-based layouts, and high-frequency animated layouts are candidates. Understanding RenderObject also makes you a better Flutter debugger - you understand what the widget layer is actually doing under the hood when you use layout widgets like Row, Column, and Stack.
Example 70: BlocListener and Side Effects
BlocListener triggers side effects (navigation, snackbars, dialogs) in response to state changes without rebuilding UI. Separate BlocListener for side effects from BlocBuilder for UI updates. BlocConsumer combines both when you need both behaviors in one widget.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Authentication state
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState { final String username; AuthAuthenticated(this.username); }
class AuthError extends AuthState { final String message; AuthError(this.message); }
// Authentication cubit
class AuthCubit extends Cubit<AuthState> {
AuthCubit() : super(AuthInitial());
Future<void> login(String username, String password) async {
emit(AuthLoading()); // => Show loading state
await Future.delayed(const Duration(seconds: 1)); // => Simulate API call
if (password == 'correct') {
emit(AuthAuthenticated(username)); // => Login success
} else {
emit(AuthError('Invalid credentials')); // => Login failed
}
}
}
void main() => runApp(BlocProvider(
create: (_) => AuthCubit(),
child: const MaterialApp(home: LoginPage()),
));
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BlocListener')),
body: BlocConsumer<AuthCubit, AuthState>(
// listener fires for side effects - NOT for UI rebuilds
listener: (context, state) {
if (state is AuthAuthenticated) {
// Navigate on success - side effect, not UI
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Welcome, ${state.username}!')),
);
// In real app: context.go('/dashboard')
}
if (state is AuthError) {
// Show error dialog - side effect, not UI
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Login Failed'),
content: Text(state.message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
},
// builder handles UI rendering only
builder: (context, state) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (state is AuthLoading)
const CircularProgressIndicator() // => Show spinner during login
else ...[
ElevatedButton(
onPressed: () => context.read<AuthCubit>().login('Alice', 'correct'),
child: const Text('Login (correct password)'),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => context.read<AuthCubit>().login('Alice', 'wrong'),
child: const Text('Login (wrong password)'),
),
],
],
),
);
},
),
);
}
}
// => Authentication uses listener for navigation/dialogs, builder for loading stateKey Takeaway: BlocListener handles side effects (navigation, dialogs, snackbars) without rebuilding UI; BlocBuilder handles UI state; BlocConsumer combines both; never trigger side effects inside BlocBuilder.builder as it can be called multiple times.
Why It Matters: Putting navigation or showDialog calls inside BlocBuilder.builder is a common mistake - builder can be called multiple times for the same state, causing multiple dialogs or multiple navigation pushes. BlocListener fires exactly once per state change and is the correct place for all side effects. This separation between UI updates and side effects is a core BLoC pattern that prevents hard-to-debug duplicate actions in production.
Example 71: Testing with BlocTest
bloc_test package provides blocTest - a concise helper for testing Cubit and BLoC state transitions. It acts as a Given/When/Then test structure: set up initial state, trigger actions, assert expected states.
// test/auth_cubit_test.dart
// Run: flutter test test/auth_cubit_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart'; // => bloc_test: ^9.1.0
// Re-use AuthCubit from Example 70 (abbreviated here)
// class AuthCubit extends Cubit<AuthState> { ... }
void main() {
group('AuthCubit', () {
// blocTest declares Given/When/Then in one function
blocTest<AuthCubit, AuthState>(
'emits [AuthLoading, AuthAuthenticated] on successful login',
// build: creates the cubit under test
build: () => AuthCubit(), // => Fresh cubit for each test
// act: performs the action to test
act: (cubit) => cubit.login('Alice', 'correct'), // => Trigger login with correct password
// expect: list of states emitted in order
expect: () => [
isA<AuthLoading>(), // => First: loading state emitted
isA<AuthAuthenticated>() // => Then: authenticated state emitted
.having((s) => s.username, 'username', 'Alice'), // => With username 'Alice'
],
);
blocTest<AuthCubit, AuthState>(
'emits [AuthLoading, AuthError] on failed login',
build: () => AuthCubit(),
act: (cubit) => cubit.login('Alice', 'wrong'), // => Wrong password
expect: () => [
isA<AuthLoading>(),
isA<AuthError>()
.having((s) => s.message, 'message', 'Invalid credentials'),
// => Error state with correct message
],
);
blocTest<AuthCubit, AuthState>(
'has AuthInitial as initial state',
build: () => AuthCubit(),
act: (_) {}, // => No action
expect: () => const [], // => No state emissions (stays at initial)
verify: (cubit) {
expect(cubit.state, isA<AuthInitial>()); // => Assert initial state directly
},
);
});
// Widget test with BLoC
testWidgets('LoginPage shows loading during auth', (tester) async {
await tester.pumpWidget(
BlocProvider(
create: (_) => AuthCubit(),
child: const MaterialApp(home: LoginPage()),
),
);
// Tap login button
await tester.tap(find.text('Login (correct password)'));
await tester.pump(); // => Pump once to process setState
// Check loading state renders correctly
expect(find.byType(CircularProgressIndicator), findsOneWidget); // => Spinner is shown
});
}
// => blocTest provides concise Given/When/Then BLoC testingKey Takeaway: blocTest(build, act, expect) tests state sequences; isA<StateType>().having(getter, name, value) asserts typed state properties; each blocTest gets a fresh cubit instance from build.
Why It Matters: Cubit and BLoC logic tests with bloc_test are among the fastest, most valuable tests you can write. They test all business logic - validation, async operations, error handling, state transitions - in milliseconds without spinning up a browser or device. A comprehensive bloc test suite catches regressions before they reach production and documents the intended state machine behavior for future maintainers.
Example 72: Isolates for CPU-Intensive Work
Flutter’s main thread handles UI rendering and must complete each frame in 16ms (60fps). CPU-intensive work - JSON parsing, image processing, encryption, compression - must run on a background Isolate to prevent frame drops. compute() is the simplest way to run a function in a background isolate.
import 'dart:async';
import 'package:flutter/foundation.dart'; // => compute() is in foundation
import 'package:flutter/material.dart';
// CPU-intensive function to run on background isolate
// IMPORTANT: this function must be a top-level or static function (not a closure)
List<int> _computePrimes(int max) {
// Sieve of Eratosthenes - CPU intensive for large numbers
final sieve = List<bool>.filled(max + 1, true);
sieve[0] = sieve[1] = false;
for (int i = 2; i * i <= max; i++) {
if (sieve[i]) {
for (int j = i * i; j <= max; j += i) {
sieve[j] = false; // => Mark multiples as non-prime
}
}
}
return [for (int i = 2; i <= max; i++) if (sieve[i]) i];
// => Returns list of all primes up to max
}
void main() => runApp(const MaterialApp(home: IsolateDemo()));
class IsolateDemo extends StatefulWidget {
const IsolateDemo({super.key});
@override
State<IsolateDemo> createState() => _IsolateDemoState();
}
class _IsolateDemoState extends State<IsolateDemo> {
String _result = '';
bool _isComputing = false;
final _limitController = TextEditingController(text: '1000000');
Future<void> _computeOnMainThread() async {
setState(() { _isComputing = true; _result = 'Computing...'; });
final start = DateTime.now();
// This BLOCKS the UI thread - frame drops during computation
final primes = _computePrimes(
int.parse(_limitController.text)); // => UI freezes here
final elapsed = DateTime.now().difference(start).inMilliseconds;
setState(() {
_result = 'MAIN THREAD: ${primes.length} primes found in ${elapsed}ms\n'
'(UI was frozen during computation)';
_isComputing = false;
});
}
Future<void> _computeOnIsolate() async {
setState(() { _isComputing = true; _result = 'Computing in background...'; });
final start = DateTime.now();
// compute() spawns a background Isolate, runs the function there
// The UI thread remains free to render frames during computation
final primes = await compute(
_computePrimes, // => Top-level function to run
int.parse(_limitController.text), // => Argument passed to function
// => Dart serializes the argument, sends to isolate, deserializes result
);
final elapsed = DateTime.now().difference(start).inMilliseconds;
if (mounted) {
setState(() {
_result = 'ISOLATE: ${primes.length} primes found in ${elapsed}ms\n'
'(UI remained responsive during computation)';
_isComputing = false;
});
}
}
@override
void dispose() {
_limitController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Isolates')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
TextField(
controller: _limitController,
decoration: const InputDecoration(
labelText: 'Find primes up to:',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
Row(
children: [
ElevatedButton(
onPressed: _isComputing ? null : _computeOnMainThread,
child: const Text('Main Thread\n(blocks UI)'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _isComputing ? null : _computeOnIsolate,
style: ElevatedButton.styleFrom(backgroundColor: Colors.teal),
child: const Text('Background Isolate\n(non-blocking)'),
),
],
),
const SizedBox(height: 24),
if (_isComputing)
const LinearProgressIndicator(), // => Animates smoothly during isolate work
if (_result.isNotEmpty) ...[
const SizedBox(height: 16),
Text(_result),
],
],
),
),
);
}
}
// => compute() keeps UI responsive during CPU-intensive prime calculationKey Takeaway: Use compute(topLevelFunction, argument) for CPU-intensive work; only top-level or static functions work with compute; the main thread remains free to render frames while the isolate runs; check mounted after await compute.
Why It Matters: Frame drops caused by CPU-intensive main-thread work are the number one cause of janky animations in Flutter apps. A single large JSON decode can drop multiple frames. compute() is the production solution - it serializes data to a background isolate, performs the work without touching the main thread, and returns the result. Parsing API responses with thousands of items, encrypting files, and processing images all belong in isolates.
Example 73: Custom Scroll Physics
ScrollPhysics controls how scroll views decelerate, bounce, and snap. Custom physics enable features like snapping to items, minimum scroll velocity, or custom overscroll behavior. Combine with PageView for horizontal card carousels that snap to each card.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ScrollPhysicsDemo()));
class ScrollPhysicsDemo extends StatelessWidget {
const ScrollPhysicsDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Scroll Physics')),
body: Column(
children: [
// BouncingScrollPhysics: iOS-style bounce at edges
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text('Bouncing Physics (iOS style):',
style: TextStyle(fontWeight: FontWeight.bold)),
),
SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(), // => Rubber-band bounce at ends
itemCount: 10,
itemBuilder: (context, i) => Container(
width: 100,
margin: const EdgeInsets.all(8),
color: Colors.blue.shade100,
child: Center(child: Text('Card $i')),
),
),
),
],
),
),
// PageScrollPhysics with snapping
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text('Snapping PageView:',
style: TextStyle(fontWeight: FontWeight.bold)),
),
SizedBox(
height: 120,
child: PageView.builder(
// PageView uses PageScrollPhysics by default - snaps to each page
controller: PageController(viewportFraction: 0.85),
// => viewportFraction < 1 shows edges of adjacent cards
itemCount: 5,
itemBuilder: (context, i) => Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text('Snap Card ${i + 1}',
style: const TextStyle(fontSize: 18)),
),
),
),
),
],
),
),
// NeverScrollableScrollPhysics: disable scrolling
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text('Disabled scrolling (NeverScrollable):',
style: TextStyle(fontWeight: FontWeight.bold)),
),
SizedBox(
height: 80,
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(), // => Cannot scroll
// => Use when outer scroll view manages scrolling
itemCount: 3,
itemBuilder: (context, i) => ListTile(title: Text('Item $i')),
),
),
],
),
),
],
),
);
}
}
// => Three scroll behaviors: bouncing, snapping, and disabledKey Takeaway: BouncingScrollPhysics adds iOS-style overscroll bounce; PageScrollPhysics snaps to full pages; NeverScrollableScrollPhysics disables scroll (for nested scrollables controlled by a parent); PageController.viewportFraction shows adjacent cards.
Why It Matters: Scroll physics significantly affect perceived quality. A horizontal card carousel that snaps to each card (PageView) feels polished; one that drifts between cards feels broken. NeverScrollableScrollPhysics is essential when embedding a ListView inside a SingleChildScrollView - without it, the two scroll views fight each other, causing scroll events to be swallowed or ignored.
Example 74: Accessibility Testing
Flutter’s accessibility test tools verify that your app is usable by assistive technologies. SemanticsController in tests checks the semantics tree directly. The flutter_driver accessibility checker finds common issues automatically.
// test/accessibility_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class AccessibleButton extends StatelessWidget {
final VoidCallback onPressed;
final String label;
const AccessibleButton({super.key, required this.onPressed, required this.label});
@override
Widget build(BuildContext context) {
return Semantics(
label: label, // => Accessible name for screen readers
button: true, // => Identifies as interactive button
child: GestureDetector(
onTap: onPressed,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.blue,
child: Icon(Icons.favorite, color: Colors.white,
semanticLabel: label), // => Icon's own semantic label
),
),
);
}
}
void main() {
testWidgets('AccessibleButton has correct semantics', (tester) async {
bool tapped = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: AccessibleButton(
label: 'Add to favorites',
onPressed: () => tapped = true,
),
),
),
);
// Find by semantics label
final buttonFinder = find.bySemanticsLabel('Add to favorites');
expect(buttonFinder, findsOneWidget); // => Widget found by accessibility label
// Check semantics properties
final semanticsNode = tester.getSemantics(buttonFinder);
expect(semanticsNode.label, 'Add to favorites'); // => Label is set
expect(semanticsNode.hasFlag(SemanticsFlag.isButton), isTrue); // => Identified as button
// Tap via semantics (as a screen reader user would)
await tester.tap(buttonFinder);
await tester.pump();
expect(tapped, isTrue); // => Tap was received
// Check minimum touch target size (WCAG requires 44x44px)
final renderBox = tester.renderObject<RenderBox>(
find.byType(AccessibleButton));
expect(renderBox.size.width, greaterThanOrEqualTo(44)); // => Width >= 44px
expect(renderBox.size.height, greaterThanOrEqualTo(44)); // => Height >= 44px
});
testWidgets('Form fields have accessible labels', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Padding(
padding: EdgeInsets.all(16),
child: TextField(
decoration: InputDecoration(
labelText: 'Email address', // => Provides accessible label
border: OutlineInputBorder(),
),
),
),
),
),
);
// TextField with labelText should be findable by label
expect(find.bySemanticsLabel('Email address'), findsOneWidget);
// => Screen readers announce field as "Email address, text field"
});
}
// => Run: flutter test test/accessibility_test.dartKey Takeaway: Use find.bySemanticsLabel to verify screen reader labels in tests; tester.getSemantics inspects semantics properties; check minimum touch target sizes (44x44px per WCAG) in widget tests.
Why It Matters: Accessibility testing catches issues before deployment that manual testing misses - missing button labels, incorrect focus order, touch targets that are too small. Automated accessibility tests in CI prevent regressions. The 44x44 logical pixel minimum touch target rule comes from both Apple’s HIG and Google’s Material guidelines and is verified by WCAG 2.1 Success Criterion 2.5.5 (Target Size). Violations exclude users with motor impairments.
Example 75: Golden Tests
Golden tests capture a widget’s rendered appearance as a PNG file (the “golden” reference image). Future test runs compare against the golden. Any pixel difference fails the test, catching accidental visual regressions in UI changes.
// test/golden_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
// Widget to golden test
class UserProfileCard extends StatelessWidget {
final String name;
final String role;
final Color avatarColor;
const UserProfileCard({
super.key,
required this.name,
required this.role,
required this.avatarColor,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
backgroundColor: avatarColor,
radius: 24,
child: Text(name[0], // => First letter of name
style: const TextStyle(color: Colors.white, fontSize: 20)),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(role, style: const TextStyle(color: Colors.grey)),
],
),
],
),
),
);
}
}
void main() {
testWidgets('UserProfileCard golden test', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false), // => Pin theme to prevent drift
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(16),
child: UserProfileCard(
name: 'Alice Johnson',
role: 'Flutter Developer',
avatarColor: const Color(0xFF0173B2),
),
),
),
),
);
// matchesGoldenFile compares screenshot to stored reference image
await expectLater(
find.byType(UserProfileCard),
matchesGoldenFile('goldens/user_profile_card.png'),
// => First run: creates the golden file
// => Subsequent runs: compares against stored golden
// => To update golden: flutter test --update-goldens
);
});
// Test multiple variants
testWidgets('UserProfileCard with long name', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Scaffold(
body: SizedBox(
width: 300, // => Constrained width for text wrapping test
child: UserProfileCard(
name: 'Alexandra Constantine-Worthington',
role: 'Senior Flutter Web Engineer',
avatarColor: const Color(0xFF029E73),
),
),
),
),
);
await expectLater(
find.byType(UserProfileCard),
matchesGoldenFile('goldens/user_profile_card_long_name.png'),
// => Catches layout issues with long text that unit tests miss
);
});
}
// => Run: flutter test test/golden_test.dart
// => Update goldens: flutter test --update-goldensKey Takeaway: matchesGoldenFile('path.png') creates or compares a pixel-accurate snapshot; update goldens with flutter test --update-goldens after intentional visual changes; pin ThemeData version to prevent false failures from framework theme updates.
Why It Matters: Golden tests catch visual regressions that no other test type detects - a color shift from a theme update, a layout change from a dependency upgrade, or a font change from a new Flutter version. They are especially valuable for design system components (buttons, cards, badges) where pixel consistency across the app is required. Without golden tests, visual regressions reach production and require hotfixes.
Example 76: Custom Clipper
ClipPath with a CustomClipper<Path> clips a widget to any arbitrary shape. This enables non-rectangular cards, wave-shaped headers, diagonal dividers, and custom shape badges - design elements that cannot be achieved with BorderRadius alone.
import 'package:flutter/material.dart';
// Define a custom wave-shaped clip path
class WaveClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
// Start at top-left
path.lineTo(0, size.height - 40); // => Down to near bottom-left
// Create a smooth wave across the bottom
final firstControlPoint = Offset(size.width / 4, size.height);
final firstEndPoint = Offset(size.width / 2, size.height - 40);
path.quadraticBezierTo( // => Bezier curve for wave
firstControlPoint.dx, firstControlPoint.dy,
firstEndPoint.dx, firstEndPoint.dy,
);
final secondControlPoint = Offset(size.width * 3 / 4, size.height - 80);
final secondEndPoint = Offset(size.width, size.height - 40);
path.quadraticBezierTo(
secondControlPoint.dx, secondControlPoint.dy,
secondEndPoint.dx, secondEndPoint.dy,
);
path.lineTo(size.width, 0); // => Up to top-right
path.close(); // => Close path back to origin
return path;
}
// shouldReclip returns true when the clip shape needs to be recalculated
@override
bool shouldReclip(WaveClipper oldClipper) => false; // => Shape never changes
}
// Diagonal section clipper
class DiagonalClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(0, size.height - 40); // => Down left side
path.lineTo(size.width, size.height); // => Diagonal to bottom-right
path.lineTo(size.width, 0); // => Up right side
path.close();
return path;
}
@override
bool shouldReclip(DiagonalClipper oldClipper) => false;
}
void main() => runApp(const MaterialApp(home: ClipperDemo()));
class ClipperDemo extends StatelessWidget {
const ClipperDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Custom Clipper')),
body: Column(
children: [
// Wave-shaped header section
ClipPath(
clipper: WaveClipper(), // => Apply wave clip to this widget
child: Container(
height: 200,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF0173B2), Color(0xFF029E73)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const Center(
child: Text('Wave Header',
style: TextStyle(color: Colors.white, fontSize: 28,
fontWeight: FontWeight.bold)),
),
),
),
// => Container is clipped to wave shape - content below wave is cut off
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
// Diagonal section card
ClipPath(
clipper: DiagonalClipper(),
child: Container(
width: 160,
height: 80,
color: Colors.orange.shade300,
child: const Center(
child: Text('Diagonal', style: TextStyle(color: Colors.white)),
),
),
),
const SizedBox(width: 16),
// Circle clip using ClipOval
ClipOval(
child: Container(
width: 80,
height: 80,
color: Colors.teal.shade300,
child: const Center(
child: Text('Oval', style: TextStyle(color: Colors.white)),
),
),
),
],
),
),
],
),
);
}
}
// => Wave header, diagonal card, and oval clip demonstrate custom clippingKey Takeaway: ClipPath with CustomClipper<Path> clips any widget to a custom shape defined by a Dart Path; shouldReclip controls when the path recalculates; ClipOval and ClipRRect are convenience widgets for oval and rounded rectangle clips.
Why It Matters: Custom clipping is how production apps achieve the distinctive visual identity that makes them stand out from generic Material implementations. Wave-shaped section dividers, diagonal hero images, and hexagonal avatar frames all use ClipPath. Flutter’s clip approach applies to any widget - Images, Videos, Containers, ListView - making the pattern versatile. The shouldReclip optimization prevents expensive path recalculation on every frame for static shapes.
Example 77: SelectionArea and Text Copying
SelectionArea enables text selection and copying in Flutter Web - critical for content sites where users need to copy text. Without it, Flutter Web text is not selectable, which violates user expectations and WCAG 1.4.12 (Text Spacing).
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: SelectionAreaDemo()));
class SelectionAreaDemo extends StatelessWidget {
const SelectionAreaDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('SelectionArea')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// WITHOUT SelectionArea: text cannot be selected/copied
Container(
padding: const EdgeInsets.all(12),
color: Colors.red.shade50,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Non-selectable text (no SelectionArea):',
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('This text cannot be selected or copied by the user. '
'This is the default Flutter Web behavior. '
'Users cannot copy code snippets or article text.'),
],
),
),
// => Cannot select text above
const SizedBox(height: 16),
// WITH SelectionArea: all descendant text is selectable
SelectionArea(
// SelectionArea makes all descendant Text widgets selectable
child: Container(
padding: const EdgeInsets.all(12),
color: Colors.teal.shade50,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Selectable text (inside SelectionArea):',
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('This text CAN be selected and copied. '
'Click and drag to select, then Ctrl+C to copy. '
'Essential for documentation sites.'),
SizedBox(height: 8),
// Code snippets especially need to be selectable
Text(
'flutter build web --web-renderer canvaskit --release',
style: TextStyle(
fontFamily: 'monospace', // => Monospace for code
backgroundColor: Color(0xFFE8E8E8),
),
),
],
),
),
),
// => All text inside SelectionArea is selectable
const SizedBox(height: 16),
// Wrap entire article pages in SelectionArea at the top level
const Card(
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
'Best practice: Wrap the Scaffold body in SelectionArea '
'for content sites. Place SelectionArea high in the tree '
'to enable selection across the entire page.',
),
),
),
],
),
),
);
}
}
// => Text inside SelectionArea can be selected, copied, and right-click searchedKey Takeaway: Wrap content pages in SelectionArea to enable text selection; place it high in the widget tree (wrapping Scaffold body or even MaterialApp’s home) for site-wide selection; code documentation sites require this by default.
Why It Matters: Non-selectable text is one of the most common complaints about Flutter Web applications. Users expect to copy article text, code snippets, error messages, and contact information. Some browser accessibility tools also rely on text selection for functionality. For documentation sites, technical blogs, and any content-first web application, SelectionArea should be applied at the root level from day one.
Example 78: Image Optimization and WebP
Web performance requires optimized image delivery. Flutter Web supports WebP images natively. Combine responsive image sizing with BoxFit, lazy loading via ListView.builder, and proper placeholder patterns to minimize Largest Contentful Paint (LCP).
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ImageOptimizationDemo()));
class ImageOptimizationDemo extends StatelessWidget {
const ImageOptimizationDemo({super.key});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
// Choose image size based on screen resolution
final imageWidth = screenWidth > 800 ? 1200 : 600; // => Serve 2x on wide screens
return Scaffold(
appBar: AppBar(title: const Text('Image Optimization')),
body: SingleChildScrollView(
child: Column(
children: [
// Hero/banner image: use BoxFit.cover with fixed height
// Request exact pixel dimensions from CDN to avoid over-fetching
SizedBox(
width: double.infinity,
height: 250,
child: Image.network(
'https://picsum.photos/$imageWidth/500', // => Responsive width
fit: BoxFit.cover, // => Fill container, crop excess
// Fade in from placeholder for smooth loading
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded) return child; // => Cache hit: show immediately
return AnimatedOpacity(
opacity: frame == null ? 0.0 : 1.0, // => Fade in on load
duration: const Duration(milliseconds: 300),
child: child,
);
},
// Provide alt text via Semantics
semanticLabel: 'Hero banner image', // => Screen reader description
),
),
// => Image width adapts to screen size; fades in when loaded
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text('Image Best Practices:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
// Feature list
...const [
'✓ Use WebP format for 25-35% smaller files vs JPEG',
'✓ Request exact dimensions from CDN (avoid scaling in browser)',
'✓ Use frameBuilder for smooth fade-in (avoids layout jump)',
'✓ Provide semanticLabel for screen readers',
'✓ Use ListView.builder for lazy loading in lists',
'✓ Use cached_network_image for repeat visits',
'✓ Provide width/height constraints to prevent layout shifts',
].map((tip) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
const SizedBox(width: 8),
Expanded(child: Text(tip)),
],
),
)),
],
),
),
],
),
),
);
}
}
// => Responsive image sizes, smooth fade-in, and accessibility labelsKey Takeaway: Serve appropriately-sized images based on MediaQuery.of(context).size; use frameBuilder for smooth fade-in; always provide semanticLabel; WebP images are 25-35% smaller than JPEG at equivalent quality.
Why It Matters: Images are typically 60-70% of a web page’s byte weight. Serving a 1200px image on a 375px mobile screen wastes 70% of bandwidth. Largest Contentful Paint (LCP) - Google’s core web vital - is usually determined by the hero image. Smooth fade-in prevents the jarring layout shift that occurs when images load after text, improving Cumulative Layout Shift (CLS). These metrics directly affect SEO ranking and user retention.
Example 79: SliverPersistentHeader and Pinned Headers
SliverPersistentHeader creates headers that can pin (stick) to the top while content scrolls below them. This pattern implements sticky section headers in long lists, a common requirement in contacts apps, product catalogs, and alphabetical indexes.
import 'package:flutter/material.dart';
// Custom SliverPersistentHeaderDelegate for sticky section header
class StickyHeaderDelegate extends SliverPersistentHeaderDelegate {
final String title;
final Color color;
const StickyHeaderDelegate({required this.title, required this.color});
@override
double get minExtent => 40; // => Minimum height when scrolled to min
@override
double get maxExtent => 40; // => Maximum height when fully expanded
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
// shrinkOffset: how much the header has collapsed (0 = full, maxExtent-minExtent = full collapse)
return Container(
height: maxExtent,
color: color, // => Section color
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.white,
),
),
);
}
@override
bool shouldRebuild(StickyHeaderDelegate oldDelegate) =>
title != oldDelegate.title || color != oldDelegate.color;
// => Only rebuild when content changes
}
void main() => runApp(const MaterialApp(home: StickyHeaderDemo()));
class StickyHeaderDemo extends StatelessWidget {
const StickyHeaderDemo({super.key});
static const sections = [
('Today', Color(0xFF0173B2), ['Email client', 'Design review', 'Team standup']),
('Yesterday', Color(0xFF029E73), ['Deploy to staging', 'Code review', 'Database migration']),
('This Week', Color(0xFFDE8F05), ['Quarterly planning', 'Interview candidate', 'Sprint retrospective']),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sticky Headers')),
body: CustomScrollView(
slivers: [
for (final section in sections) ...[
// SliverPersistentHeader with pinned: true creates sticky header
SliverPersistentHeader(
pinned: true, // => Header stays at top when scrolled past
delegate: StickyHeaderDelegate(
title: section.$1, // => Section title (record field)
color: section.$2, // => Section color
),
),
// SliverList for the section items
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
leading: Icon(Icons.circle, size: 10, color: section.$2),
title: Text(section.$3[index]),
),
childCount: section.$3.length, // => Number of items in this section
),
),
],
],
),
);
}
}
// => Section headers pin to top as content scrolls through themKey Takeaway: SliverPersistentHeader with pinned: true creates sticky section headers that remain visible at the top; SliverPersistentHeaderDelegate controls min/max heights; set minExtent == maxExtent for fixed-height headers.
Why It Matters: Sticky section headers are essential UX in any long, categorized list - contacts, order history, a glossary, or alphabetical indexes. The Sliver approach renders more efficiently than wrapping each section in a Column because all sections participate in one scroll physics simulation. pinned: true keeps the active section header visible, helping users maintain context of where they are in a long list.
Example 80: Production Checklist and Performance Audit
This final example summarizes the production readiness checks for a Flutter Web application - the non-negotiable items before shipping to real users.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ProductionChecklistPage()));
class ProductionChecklistPage extends StatefulWidget {
const ProductionChecklistPage({super.key});
@override
State<ProductionChecklistPage> createState() => _ProductionChecklistPageState();
}
class _ProductionChecklistPageState extends State<ProductionChecklistPage> {
// Track check completion state
final Map<String, bool> _checks = {
'const constructors throughout widget tree': false,
'RepaintBoundary on animated widgets': false,
'ListView.builder for all lists (no plain ListView with children)': false,
'Images have semanticLabel for accessibility': false,
'All buttons have tooltips (WCAG 2.1)': false,
'SelectionArea wraps content pages': false,
'FocusNode keyboard navigation in forms': false,
'GoRouter with PathUrlStrategy for clean URLs': false,
'flutter build web --release --web-renderer canvaskit': false,
'nginx try_files fallback for SPA routing': false,
'Cache-Control headers on hashed assets': false,
'gzip enabled in nginx for JS/WASM/CSS': false,
'manifest.json configured for PWA': false,
'OpenGraph meta tags in index.html': false,
'All futures assigned in initState (not in build)': false,
'AnimationControllers disposed in dispose()': false,
'TextEditingControllers disposed in dispose()': false,
'ScrollControllers disposed in dispose()': false,
};
@override
Widget build(BuildContext context) {
final completed = _checks.values.where((v) => v).length;
final total = _checks.length;
return Scaffold(
appBar: AppBar(
title: const Text('Production Checklist'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(6),
child: LinearProgressIndicator(
value: completed / total, // => Completion percentage
backgroundColor: Colors.blue.shade200,
valueColor: const AlwaysStoppedAnimation(Colors.white),
),
),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'$completed / $total items complete',
style: Theme.of(context).textTheme.titleMedium,
),
),
Expanded(
child: ListView.builder(
itemCount: _checks.length,
itemBuilder: (context, index) {
final entry = _checks.entries.elementAt(index);
return CheckboxListTile(
title: Text(entry.key),
value: entry.value, // => Checked state for this item
onChanged: (value) => setState(() =>
_checks[entry.key] = value ?? false), // => Toggle check
controlAffinity: ListTileControlAffinity.leading,
dense: true,
);
},
),
),
if (completed == total)
Container(
padding: const EdgeInsets.all(16),
color: Colors.teal.shade50,
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle, color: Colors.teal),
SizedBox(width: 8),
Text('Ready for production!',
style: TextStyle(color: Colors.teal,
fontWeight: FontWeight.bold, fontSize: 16)),
],
),
),
],
),
);
}
}
// => Production checklist tracks readiness across performance, accessibility, and deploymentKey Takeaway: Production Flutter Web requires attention to performance (const, RepaintBoundary, ListView.builder), accessibility (semantics, keyboard navigation, touch targets), routing (GoRouter, PathUrlStrategy), and deployment (nginx SPA fallback, asset caching, gzip).
Why It Matters: This checklist distills the most common production failures seen in real Flutter Web deployments. Each item prevents a specific class of problem: missing dispose() calls cause memory leaks, missing nginx try_files breaks direct URL access, missing const causes frame drops, and missing semantics labels fail accessibility audits. A team that completes this checklist ships a Flutter Web application that performs correctly, loads quickly, is accessible to all users, and deploys reliably in containerized infrastructure.