Start here Quick start · 6 you’ll reach for daily
New app flutter create my_app
Run flutter run -d chrome
Hot reload r · R for restart
Layout Column / Row / Expanded
State setState(() => …)
Navigate context.push('/x')
Target versions · pace Versions
Targets:
flutter ≥ 3.22
dart ≥ 3.4
material 3
go_router ≥ 14
riverpod ≥ 2.5
Impeller is the default renderer on iOS and Android (replacing Skia GLES).
Material 3 is the default theme. Null-safety is mandatory (Dart 3 dropped the legacy mode).
Riverpod 2 uses code generation;
flutter_bloc 8 still ships the
classic emit() API. Pinned to May 2026 ; check
docs.flutter.dev
for newer.
Install · run Setup
bash
Copy
# Install (macOS shown — Windows/Linux similar)
brew install --cask flutter # or download from flutter.dev
flutter --version
flutter doctor # verifies Xcode / Android SDK / chrome
# New app
flutter create my_app
cd my_app
flutter run # picks the only attached device
flutter run -d chrome # explicit target
flutter devices # list available targets
# Daily loop
r # hot reload in the running shell
R # hot restart (resets state)
q # quit
flutter doctor tells you exactly what’s missing — Android SDK licenses,
Xcode command-line tools, CocoaPods. Don’t skip it: a green checklist saves an afternoon
of guessing-why-iOS-won’t-build.
Common imports · pubspec deps Common imports
Core widgets ship in flutter/material.dart (or
cupertino.dart). Add packages to
pubspec.yaml and run flutter pub get.
import 'package:flutter/material.dart'; Material widgets — Scaffold, AppBar, etc.
import 'package:flutter/cupertino.dart'; iOS-style widgets.
import 'package:flutter/services.dart'; Clipboard, haptics, system UI overlays.
import 'dart:async'; Future, Stream, Timer.
import 'dart:convert'; jsonEncode / jsonDecode.
import 'package:http/http.dart' as http; Plain HTTP client.
import 'package:dio/dio.dart'; Richer HTTP (interceptors, cancel, timeout).
import 'package:go_router/go_router.dart'; Preferred Declarative routing.
import 'package:flutter_riverpod/flutter_riverpod.dart'; State management (compile-safe).
import 'package:flutter_bloc/flutter_bloc.dart'; BLoC pattern.
import 'package:shared_preferences/shared_preferences.dart'; Key/value persistence.
import 'package:hive/hive.dart'; Fast on-disk NoSQL.
import 'package:freezed_annotation/freezed_annotation.dart'; Sealed-class + immutability codegen.
import 'package:json_annotation/json_annotation.dart'; JSON serialization codegen.
class X extends StatelessWidget Pure function of inputs. No mutable state.
class X extends StatefulWidget → State<X> Holds state across rebuilds.
@override Widget build(BuildContext c) The render method. Returns a widget tree.
setState(() => field = newValue) Mark state dirty → rebuild.
initState() / dispose() Lifecycle: subscribe / unsubscribe.
didChangeDependencies() Runs when InheritedWidget ancestors change.
const MyWidget({super.key}) Always pass key & use const where possible.
BuildContext context Pointer into the widget tree. Use to look up ancestors.
Minimal app
javascript
Copy
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const Counter(),
);
}
}
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State createState() => _CounterState();
}
class _CounterState extends State {
int n = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(child: Text('$n', style: const TextStyle(fontSize: 48))),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => n++),
child: const Icon(Icons.add),
),
);
}
}
Compose widgets Layout
Linear & axis
Column(children: [...]) Vertical stack.
Row(children: [...]) Horizontal.
Expanded(child: …) Fills remaining space along the axis.
Flexible(flex: 2, child: …) Proportional sizing.
Spacer() Empty expandable space.
SizedBox(width: 12, height: 12) Fixed gap or box.
Wrap(spacing: 8, children: …) Like Row, wraps to next line when full.
Containers & padding
Container(padding: …, decoration: …) Box with paint + layout. Convenience wrapper.
Padding(padding: EdgeInsets.all(16), child: …) Padding only. Lighter than Container.
EdgeInsets.symmetric(horizontal: 12, vertical: 8) Common pattern.
Center(child: …) Centers child in available space.
Align(alignment: Alignment.topRight, child: …) Position child within parent.
Stack(children: [...]) Overlap widgets (z-axis).
Positioned(top: 0, right: 0, child: …) Absolute placement inside a Stack.
Scaffolds & surfaces
Scaffold(appBar: …, body: …, floatingActionButton: …) Standard page chrome.
AppBar(title: Text('…'), actions: [...]) Top app bar.
SafeArea(child: …) Insets around notch / status bar.
Card(child: …) Elevated rounded surface.
Material(child: …) Provides ink-splash + theming surface.
SingleChildScrollView(child: Column(…)) When content might overflow vertically.
Scrollables Lists & grids
ListView(children: […]) Static list. Builds all children eagerly.
ListView.builder(itemCount, itemBuilder) Preferred Lazy — only builds visible rows.
ListView.separated(itemCount, itemBuilder, separatorBuilder) Builder + divider widget between rows.
ListTile(title, subtitle, leading, trailing, onTap) Pre-styled row.
GridView.count(crossAxisCount: 2, children: …) Fixed-column grid.
GridView.builder(gridDelegate: …, itemBuilder: …) Lazy grid.
RefreshIndicator(onRefresh, child: ListView…) Pull-to-refresh.
CustomScrollView(slivers: [...]) Mix headers, lists, grids in one scroll view via slivers.
SliverAppBar(pinned: true, expandedHeight: 200) Collapsing app bar.
List with pull-to-refresh
javascript
Copy
import 'package:flutter/material.dart';
class Feed extends StatelessWidget {
final List items;
final Future Function() onRefresh;
const Feed({super.key, required this.items, required this.onRefresh});
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: onRefresh,
child: ListView.separated(
itemCount: items.length,
itemBuilder: (ctx, i) => ListTile(
title: Text(items[i]),
onTap: () => Navigator.pushNamed(ctx, '/detail', arguments: items[i]),
),
separatorBuilder: (_, __) => const Divider(height: 1),
),
);
}
}
// For infinite-scroll, swap to ListView.builder + a ScrollController
// that calls fetchMore() when controller.position.pixels >= maxScrollExtent - 200.
Routes · go_router Navigation
Imperative Navigator
Navigator.push(c, MaterialPageRoute(builder: (_) => Detail())) Push a new screen.
Navigator.pop(c) Pop back. Pass a value to return it.
Navigator.pushNamed(c, '/detail', arguments: x) Named route; arguments via ModalRoute.of(c).settings.
Navigator.pushReplacement(c, …) Swap current screen.
Navigator.pushAndRemoveUntil(c, route, (r) => false) Clear stack (post-login flow).
go_router (declarative) Preferred
GoRouter(routes: [GoRoute(path: '/', builder: …)]) Define the route table.
MaterialApp.router(routerConfig: r) Wire router to app.
context.go('/items/42') Replace the stack.
context.push('/items/42') Push on top.
context.pop() Pop.
state.pathParameters['id'] Read :id from URL.
state.uri.queryParameters['q'] Read query string.
ShellRoute(builder: …, routes: [...]) Wrap routes in a persistent shell (bottom nav bar).
go_router example
javascript
Copy
// pubspec.yaml: go_router: ^14.0.0
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (ctx, st) => const HomeScreen(),
routes: [
GoRoute(
path: 'item/:id',
builder: (ctx, st) => DetailScreen(id: st.pathParameters['id']!),
),
],
),
],
);
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) => MaterialApp.router(routerConfig: _router);
}
// Navigate from any widget:
// context.go('/item/42'); // replace stack
// context.push('/item/42'); // push on top
// context.pop(); // pop
From setState to Riverpod State management
Built-in
setState(() => field = v) Local widget state. Always start here.
InheritedWidget Low-level ancestor data propagation. Underlies everything else.
ValueNotifier<int>(0) + ValueListenableBuilder Tiny reactive value for shared state.
ChangeNotifier + ListenableBuilder Class-based observable. notifyListeners().
Riverpod 2 Preferred
ProviderScope(child: App()) Wrap main() root.
@riverpod int counter(Ref ref) => 0; Codegen provider declaration.
ref.watch(counterProvider) Subscribe; rebuilds on change.
ref.read(counterProvider.notifier).state++ Mutate without subscribing.
ConsumerWidget · build(c, WidgetRef ref) Widget with access to ref.
FutureProvider / StreamProvider Async providers; auto-emit AsyncValue.
flutter_bloc
class CounterCubit extends Cubit<int> {…} Sync state holder. emit(newState).
class XBloc extends Bloc<Event, State> {…} Event-driven state machine.
BlocProvider(create: (_) => CounterCubit()) Provide a bloc/cubit to the tree.
BlocBuilder<CounterCubit,int>(builder: …) Rebuild on state change.
context.read<CounterCubit>().increment() Get bloc without listening.
Futures · streams Async
Future<T> load() async { return …; } Async function returns a Future.
final r = await http.get(uri); Suspend until resolved. Only inside async.
try { … } on Exception catch (e) { … } Async errors are exceptions, not callbacks.
Future.wait([a, b]) Run in parallel; resolves when all do.
Stream<T> ticks() async* { yield …; } Generator stream.
await for (final v in stream) { … } Iterate a stream.
FutureBuilder<T>(future: …, builder: (c,snap) => …) Reactive widget that rebuilds on Future state.
StreamBuilder<T>(stream: …, builder: …) Same, for streams.
Timer.periodic(Duration(seconds: 1), (_) => …) Repeating callback. Cancel in dispose.
FutureBuilder example
javascript
Copy
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class Posts extends StatefulWidget {
const Posts({super.key});
@override
State createState() => _PostsState();
}
class _PostsState extends State {
late Future> _future;
@override
void initState() {
super.initState();
_future = _load();
}
Future> _load() async {
final r = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts?_limit=10'));
return jsonDecode(r.body) as List;
}
@override
Widget build(BuildContext context) {
return FutureBuilder>(
future: _future,
builder: (ctx, snap) {
if (snap.connectionState != ConnectionState.done) return const CircularProgressIndicator();
if (snap.hasError) return Text('Error: ${snap.error}');
final items = snap.data!;
return ListView(
children: [for (final p in items) ListTile(title: Text(p['title']))],
);
},
);
}
}
TextField(decoration: InputDecoration(labelText: 'Name')) Plain text input.
TextEditingController() Read / set the value. Dispose in dispose().
controller.text / controller.clear() Read / reset.
Form(key: formKey, child: Column(...)) Wraps multiple fields; lets you save/validate together.
TextFormField(validator: (v) => v.isEmpty ? 'err' : null) Field with validation.
formKey.currentState!.validate() Run all validators.
FocusNode + autofocus: true Programmatic focus.
Checkbox / Switch / Slider Standard interactive widgets.
DropdownButton<T>(items: …, onChanged: …) Material dropdown.
Material 3 Theming & styling
ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo) Generate a full M3 palette from one seed colour.
ColorScheme.fromSeed(seedColor: …) Manual seed-based scheme.
Theme.of(context).colorScheme.primary Read a theme colour inside build.
Theme.of(context).textTheme.titleLarge Read a text style.
MaterialApp(theme: light, darkTheme: dark, themeMode: …) Light/dark theme pair.
MediaQuery.of(context).size.width Screen size. Legacy Prefer LayoutBuilder for widget-local sizing.
LayoutBuilder(builder: (c, constraints) => …) Responsive: react to incoming constraints.
TextStyle(fontSize: 16, fontWeight: FontWeight.w600) Inline text styling.
Implicit & explicit Animation
Implicit (one-liners)
AnimatedContainer(duration: …, color: …) Animates whenever its props change.
AnimatedOpacity(opacity, duration) Fade.
AnimatedAlign / AnimatedPositioned Reposition.
TweenAnimationBuilder<T>(tween, duration, builder) Animate any value without a controller.
Hero(tag: 'avatar', child: …) Shared-element transition across routes.
Explicit (controllers)
AnimationController(vsync: this, duration: …) Drives [0, 1]. Needs TickerProviderStateMixin.
Tween<double>(begin: 0, end: 1).animate(ctrl) Map controller value to range.
CurvedAnimation(parent, curve: Curves.easeOut) Apply an easing curve.
AnimatedBuilder(animation, builder: (c, child) => …) Rebuild on each tick.
controller.forward() / reverse() / repeat() Drive it.
@override dispose() { ctrl.dispose(); super.dispose(); } Always dispose. Leaking controllers leak vsync ticks.
Platform.isIOS / isAndroid / isMacOS Branch by OS. dart:io.
kIsWeb Constant true on web; preserves tree-shaking.
MethodChannel('app/native').invokeMethod('x', args) Call Kotlin/Swift from Dart. Async.
EventChannel('app/sensor').receiveBroadcastStream() Subscribe to native event stream.
Pigeon (codegen for channels) Preferred Typesafe channels — generates Dart/Kotlin/Swift bindings from one spec.
dart:ffi Direct C ABI. Use for native libs without a method-channel hop.
SystemChrome.setPreferredOrientations([…]) Lock orientation.
Clipboard.setData(ClipboardData(text: '…')) Copy to clipboard.
HapticFeedback.lightImpact() Vibration / haptic.
Full app · ~60 lines End-to-end · List + Detail + Persist
Fetches a feed, navigates to a detail screen via go_router, persists a favourite to
SharedPreferences. Add the three packages to pubspec.yaml and run.
javascript
Copy
// pubspec.yaml: http, shared_preferences, go_router
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(const App());
final _router = GoRouter(routes: [
GoRoute(path: '/', builder: (c, s) => const ListScreen()),
GoRoute(
path: '/detail',
builder: (c, s) => DetailScreen(post: s.extra as Map),
),
]);
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext c) => MaterialApp.router(routerConfig: _router);
}
class ListScreen extends StatefulWidget {
const ListScreen({super.key});
@override
State createState() => _ListScreenState();
}
class _ListScreenState extends State {
List items = [];
@override
void initState() {
super.initState();
http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts?_limit=10'))
.then((r) => setState(() => items = jsonDecode(r.body)));
}
@override
Widget build(BuildContext c) => Scaffold(
appBar: AppBar(title: const Text('Posts')),
body: ListView(children: [
for (final p in items)
ListTile(title: Text(p['title']), onTap: () => c.push('/detail', extra: p)),
]),
);
}
class DetailScreen extends StatelessWidget {
final Map post;
const DetailScreen({super.key, required this.post});
Future _fav() async {
final p = await SharedPreferences.getInstance();
await p.setBool('fav:${post['id']}', true);
}
@override
Widget build(BuildContext c) => Scaffold(
appBar: AppBar(title: Text(post['title'])),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(post['body']),
TextButton(onPressed: _fav, child: const Text('Save ★')),
]),
),
);
}
flutter pub get Install deps from pubspec.yaml.
flutter pub add http Add a dependency.
flutter pub run build_runner build --delete-conflicting-outputs Codegen pass (freezed, json_serializable, riverpod_generator).
flutter analyze Static analysis. Fail-loud in CI.
dart format . Format every file in tree.
flutter test Run widget + unit tests.
flutter build apk --release Release APK. Use appbundle for Play Store.
flutter build ipa --release iOS archive. Needs Xcode + signing.
flutter build web --release Static site bundle.
flutter clean Wipe build/ when things get weird.
Best practice Good to know
Use const constructors religiously.
A const widget is reused across rebuilds instead of allocated.
The analyser will tell you which ones can be const — turn the lints on.
Hot reload preserves state; hot restart resets it.
Use r for UI tweaks, R when you change
initState, top-level main(), or app-wide singletons.
Prefer ListView.builder for anything > ~20 rows.
The default ListView(children: […]) builds every child eagerly —
easy memory bloat. .builder is lazy.
Common traps Watch out for
setState after dispose throws.
Async callbacks that complete after the widget unmounts will trip this. Guard with
if (!mounted) return; before calling setState.
BuildContext isn’t a global.
Using one across an await can yield a defunct context. Capture
what you need before awaiting (e.g. final messenger = ScaffoldMessenger.of(c);).
Adding a native package needs a full rebuild.
Hot reload won’t pick up new platform code. Stop the app and run flutter run again.
iOS may also need pod install in ios/.