Flutter, explained for beginners
Flutter is Google's open-source toolkit for building natively compiled apps for mobile, web, and desktop — from a single Dart codebase. This page is a compact reference: the mental model, the syntax you'll actually use, runnable snippets, and a glossary of the jargon people throw around.
01 · What is Flutter?
Flutter is a UI toolkit plus a framework plus a rendering engine. You write Dart code that describes what the screen should look like, and Flutter draws every pixel itself instead of handing widgets off to the platform's native UI kit.
Three things in one box
- Dart — the language you write. Familiar if you know Java, C#, JavaScript, or Swift.
- Flutter framework — the widget library
(
material,cupertino,widgets), layout, animation, navigation, gestures. - Flutter engine — C++ rendering layer (Impeller/Skia) that compiles your widget tree into GPU draw calls.
What you get
- One codebase, many targets — Android, iOS, web, Windows, macOS, Linux.
- Hot reload — save a file, see the change in under a second, without losing your app's state.
- Pixel-identical UI across platforms, because Flutter paints the widgets itself.
- Rich built-in widget set — Material Design and iOS-style Cupertino out of the box.
What it is not
- Not a website builder. Flutter web renders to canvas/HTML but is not a replacement for a content site.
- Not "React Native with Dart" — there is no bridge to native widgets for the UI. Only platform channels cross into native code.
- Not a backend. You'll pair it with an API, Firebase, or similar.
02 · How Flutter works (the mental model)
If you remember one diagram, remember this one: you write widgets → Flutter builds a widget tree → elements → render objects → pixels.
| Layer | What it is | You touch it? |
|---|---|---|
| Widget | An immutable description of part of the UI. Cheap to create, thrown away often. | Yes — constantly |
| Element | The long-lived glue that holds a widget's position in the tree and its state. | Rarely |
| RenderObject | Does layout (size + position) and painting. | Almost never |
| Layer / GPU | Final composited frame sent to the screen. | No |
Key idea: widgets are immutable blueprints. When something changes, you don't mutate a widget — you rebuild it with new values. Flutter diffs the new tree against the old one and only repaints what actually changed.
The golden rule of layout
Constraints go down. Sizes go up. Parent sets position.
A parent widget passes constraints (min/max width & height) to its child. The child picks a size within those constraints. The parent then decides where to place it. Most "why is my widget overflowing?" bugs are this rule biting you.
03 · Getting set up
- Install the Flutter SDK and add it to your
PATH. - Install a platform toolchain: Android Studio (Android) and/or Xcode (iOS/macOS).
- Install an editor: VS Code (with the Flutter extension) or Android Studio.
- Run
flutter doctorand fix anything marked with an ✗.
# Verify your setup
flutter doctor
# See available devices / emulators
flutter devices
# Create a new project
flutter create my_app
cd my_app
# Run it (with hot reload active)
flutter run
# While running: press r = hot reload, R = hot restart, q = quit
Hot reload vs. hot restart. Hot reload injects changed code and keeps your app state (your counter is still at 7). Hot restart throws away state and starts the app fresh. If hot reload ever behaves oddly, hit hot restart before you debug anything.
04 · Dart in 10 minutes
Dart is the language Flutter is written in. It's statically typed with type inference, garbage collected, and — importantly — has sound null safety.
// Type inference
var name = 'Flutter'; // String
final version = 3.24; // set once, at runtime
const pi = 3.14159; // compile-time constant (must be known at compile time)
// Explicit types
int count = 3;
double ratio = 0.5;
String greeting = 'Hello';
bool isReady = true;
// Collections
List<String> fruits = ['apple', 'banana'];
Map<String, int> scores = {'Ada': 10, 'Linus': 9};
Set<int> ids = {1, 2, 3};
// String interpolation
print('$greeting, $name! Version $version'); // simple
print('Total: ${count + 1}'); // expression
Null safety — the part that trips people up
String a = 'always non-null'; // cannot be null
String? b; // MAY be null — the ? makes it nullable
// b.length; // ❌ compile error: b might be null
if (b != null) {
print(b.length); // ✅ promoted to non-null inside the check
}
// Null-aware operators
print(b?.length); // null if b is null, otherwise b.length
print(b ?? 'fallback'); // 'fallback' if b is null
b ??= 'default'; // assign only if currently null
Functions
// Classic
int add(int a, int b) {
return a + b;
}
// Arrow syntax for single expressions
int add2(int a, int b) => a + b;
// Optional named parameters (the Flutter style)
void greet({required String name, String greeting = 'Hello'}) {
print('$greeting, $name');
}
greet(name: 'Ada'); // Hello, Ada
greet(name: 'Ada', greeting: 'Hi'); // Hi, Ada
// Optional positional
void log(String msg, [int? level]) { /* ... */ }
// Functions are values
final doubleFn = (int x) => x * 2;
Classes
class Point {
final double x;
final double y;
// Constructor with initializer shorthand
const Point(this.x, this.y);
// Named constructor
Point.origin() : x = 0, y = 0;
double distanceTo(Point other) {
final dx = x - other.x;
final dy = y - other.y;
return math.sqrt(dx * dx + dy * dy);
}
@override
String toString() => 'Point($x, $y)';
}
final p = Point(1, 2);
final o = const Point.origin();
Collections, loops, and the bits you'll use daily
final numbers = [1, 2, 3, 4, 5];
// Functional helpers
final doubled = numbers.map((n) => n * 2).toList(); // [2,4,6,8,10]
final evens = numbers.where((n) => n.isEven).toList(); // [2,4]
final sum = numbers.reduce((a, b) => a + b); // 15
final hasBig = numbers.any((n) => n > 4); // true
// Spread & collection-if (great for building widget lists)
final more = [0, ...numbers, if (hasBig) 6];
// for-in
for (final n in numbers) { print(n); }
// switch expression (Dart 3)
final label = switch (numbers.length) {
0 => 'empty',
1 => 'one',
_ => 'many',
};
Classes you'll meet constantly: Future & Stream
// Future = a value that arrives later (one value)
Future<String> fetchName() async {
await Future.delayed(const Duration(seconds: 1));
return 'Ada';
}
// Stream = a sequence of values arriving over time
Stream<int> countdown(int from) async* {
for (var i = from; i > 0; i--) {
yield i;
await Future.delayed(const Duration(seconds: 1));
}
}
await for (final n in countdown(3)) {
print(n); // 3, 2, 1
}
05 · Widgets — the building blocks
Everything you see in a Flutter app is a widget: layout, text, images, padding, animation, even the app itself. Widgets nest inside each other to form the widget tree.
StatelessWidget vs. StatefulWidget
| StatelessWidget | StatefulWidget | |
|---|---|---|
| Holds changing data? | No — output depends only on its inputs | Yes — keeps a State object that can change |
| Rebuilds when | Its parent rebuilds, or inputs change | You call setState() (or the parent rebuilds) |
| Use for | Static UI: labels, icons, cards, layouts | Interactive UI: counters, forms, toggles, anything animated |
Reach for stateless first. Only make a widget stateful when it genuinely owns data that changes over time. Most beginners make everything stateful and end up with a mess.
// ---- Stateless ----
class Greeting extends StatelessWidget {
final String name;
const Greeting({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Text('Hello, $name');
}
}
// ---- Stateful ----
class Toggle extends StatefulWidget {
const Toggle({super.key});
@override
State<Toggle> createState() => _ToggleState();
}
class _ToggleState extends State<Toggle> {
bool _on = false;
@override
Widget build(BuildContext context) {
return Switch(
value: _on,
onChanged: (v) => setState(() => _on = v),
);
}
}
The widget catalogue you need on day one
| Widget | Job |
|---|---|
Text | Draws a string, with optional style. |
Icon | Draws a glyph from a font, e.g. Icons.add. |
Image | Shows an image from assets, network, or memory. |
Container | A box: size, colour, padding, border, radius, shadow. |
Padding | Adds space inside around its child. |
SizedBox | A fixed-size gap, or forces a child to a size. |
Row / Column | Lays children out horizontally / vertically. |
Stack | Overlays children on top of each other. |
Expanded / Flexible | Takes up leftover space in a Row/Column. |
ListView | A scrollable, lazily-built list. |
Card | A Material surface with rounded corners and elevation. |
Scaffold | The page skeleton: app bar, body, FAB, drawer, snack bars. |
AppBar | The top bar with title and actions. |
Center | Centres its child. |
GestureDetector | Adds tap/press/drag callbacks to any widget. |
06 · Your first app, line by line
import 'package:flutter/material.dart';
// 1. Every Dart program starts at main().
void main() => runApp(const MyApp());
// 2. The root widget of the app.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// 3. MaterialApp wires up theming, routing and localization.
return MaterialApp(
title: 'Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.indigo,
useMaterial3: true,
),
home: const HomePage(),
);
}
}
// 4. A page. Scaffold gives us the standard page structure.
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: const Center(
child: Text('Hello, Flutter!'),
),
);
}
}
Why all the const?
A const widget is built once at compile time and reused. When
the parent rebuilds, Flutter sees the identical instance and skips the
subtree entirely. Sprinkling const is the single easiest
performance win in Flutter — and your editor will suggest it.
07 · State & setState
State is any data that can change while the app is running
and that affects what's on screen. In a StatefulWidget, you
mutate that data inside setState(), which tells Flutter
"this widget is dirty, rebuild it."
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
void _increment() {
setState(() {
_count++; // mutate state inside setState
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Text(
'$_count',
style: Theme.of(context).textTheme.displayMedium,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
}
Never call setState() from inside build().
It schedules another rebuild, which calls build, which calls setState…
infinite loop. Also don't do heavy work inside setState's
callback — only the state mutation belongs there.
Lifting state up
When two widgets need the same piece of state, move it to their closest common ancestor and pass it down as a parameter. The child that changes it gets a callback.
class Parent extends StatefulWidget {
const Parent({super.key});
@override
State<Parent> createState() => _ParentState();
}
class _ParentState extends State<Parent> {
String _name = 'World';
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Hello, $_name'),
NameField(
onNameChanged: (value) => setState(() => _name = value),
),
],
);
}
}
class NameField extends StatelessWidget {
final ValueChanged<String> onNameChanged;
const NameField({super.key, required this.onNameChanged});
@override
Widget build(BuildContext context) {
return TextField(onChanged: onNameChanged);
}
}
When setState isn't enough
For small apps, setState plus lifting is plenty. Once state
needs to be shared across many screens, look at:
- Provider — the classic, gentle introduction to shared state.
- Riverpod — Provider's more testable, compile-safe successor.
- Bloc / Cubit — event-driven, popular on larger teams.
- ValueNotifier + ValueListenableBuilder — built-in, no package needed.
Beginner advice: don't install a state-management package
on day one. Build two or three apps with plain setState first.
You'll understand why the packages exist, which makes them much
easier to learn.
08 · Layout
Row, Column, and the main/cross axis
Column(
// Column's main axis is vertical, cross axis is horizontal.
mainAxisAlignment: MainAxisAlignment.center, // along the column
crossAxisAlignment: CrossAxisAlignment.start, // across the column
children: [
const Text('Title'),
const SizedBox(height: 8),
Row(
children: [
const CircleAvatar(child: Icon(Icons.person)),
const SizedBox(width: 12),
// Expanded fills the leftover horizontal space.
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Ada Lovelace'),
Text('Mathematician', style: TextStyle(color: Colors.grey)),
],
),
),
],
),
],
)
Padding vs. Margin
Flutter has no margin property. Padding adds space
inside a widget's bounds. To get "margin", wrap the widget in a
Container with margin:, or put a
SizedBox between siblings.
Container(
width: 200,
height: 100,
margin: const EdgeInsets.all(16), // outside
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black12),
boxShadow: const [
BoxShadow(blurRadius: 8, color: Colors.black12, offset: Offset(0, 2)),
],
),
child: const Text('Card-like box'),
)
Common constraint errors and their fixes
| Error | Cause | Fix |
|---|---|---|
| "RenderFlex overflowed by N pixels" | Row/Column children are too big for the available space | Wrap the flexible child in Expanded or Flexible, or make the whole thing scrollable |
| "Vertical viewport was given unbounded height" | A ListView inside a Column with no height limit |
Wrap the list in Expanded, or use shrinkWrap: true (slower) |
| "BoxConstraints forces an infinite width" | A widget asking for infinite size inside an unbounded parent | Give it a size: SizedBox, Expanded, or ConstrainedBox |
| "Incorrect use of ParentDataWidget" | Expanded/Flexible not a direct child of Row/Column/Flex |
Move it so its immediate parent is a Flex widget |
09 · Lists & scrolling
For anything longer than a handful of items, use
ListView.builder. It builds items lazily as they scroll into
view, so a list of 10,000 rows costs about the same as a list of 10.
class Todo {
final String title;
final String subtitle;
const Todo(this.title, this.subtitle);
}
class TodoList extends StatelessWidget {
final List<Todo> todos;
const TodoList({super.key, required this.todos});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: todos.length,
// Optional: separate the rows
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
leading: const Icon(Icons.check_circle_outline),
title: Text(todo.title),
subtitle: Text(todo.subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => print('Tapped ${todo.title}'),
);
},
);
}
}
Empty lists matter. A ListView with zero items
shows a blank screen that looks broken. Handle it:
Widget build(BuildContext context) {
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (error != null) {
return Center(child: Text('Something went wrong: $error'));
}
if (todos.isEmpty) {
return const Center(child: Text('Nothing here yet.'));
}
return ListView.builder(/* ... */);
}
Making a whole page scroll
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Heading'),
SizedBox(height: 12),
Text('A long body of text that will scroll...'),
// ...
],
),
)
For mixed content — a header plus a list — use
CustomScrollView with slivers, or the simpler
ListView with a header item at index 0.
10 · Input, buttons & forms
ElevatedButton(
onPressed: () => print('tapped'),
child: const Text('Primary'),
)
OutlinedButton.icon(
onPressed: _submit,
icon: const Icon(Icons.send),
label: const Text('Send'),
)
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
)
// Disable any button by passing null
ElevatedButton(onPressed: null, child: const Text('Disabled'))
class SignupForm extends StatefulWidget {
const SignupForm({super.key});
@override
State<SignupForm> createState() => _SignupFormState();
}
class _SignupFormState extends State<SignupForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
@override
void dispose() {
_emailController.dispose(); // always dispose controllers
super.dispose();
}
void _submit() {
// validate() runs every validator and returns true if all pass
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Welcome, ${_emailController.text}')),
);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'you@example.com',
),
validator: (value) {
if (value == null || value.isEmpty) return 'Email is required';
if (!value.contains('@')) return 'Enter a valid email';
return null; // null means "valid"
},
),
const SizedBox(height: 16),
ElevatedButton(onPressed: _submit, child: const Text('Sign up')),
],
),
);
}
}
Always dispose() your controllers.
TextEditingController, AnimationController,
ScrollController and FocusNode hold resources.
Forget to dispose them and you leak memory.
Handling taps on anything
// No visual feedback — good for custom hit areas
GestureDetector(
onTap: () => print('tapped'),
onLongPress: () => print('held'),
child: const Icon(Icons.favorite),
)
// Material ripple feedback — prefer this for tappable surfaces
InkWell(
onTap: () => print('tapped'),
borderRadius: BorderRadius.circular(8),
child: const Padding(
padding: EdgeInsets.all(12),
child: Text('Tap me'),
),
)
12 · Async, networking & JSON
Future<String> fetchGreeting() async {
await Future.delayed(const Duration(seconds: 1));
return 'Hello from the server';
}
Future<void> main() async {
print('start');
final greeting = await fetchGreeting(); // pauses here, doesn't block the UI
print(greeting);
print('end');
}
// Output: start → (1s pause) → Hello from the server → end
// pubspec.yaml:
// dependencies:
// http: ^1.2.0
import 'dart:convert';
import 'package:http/http.dart' as http;
class Todo {
final int id;
final String title;
final bool completed;
const Todo({required this.id, required this.title, required this.completed});
// Named constructor: build from a decoded JSON map
factory Todo.fromJson(Map<String, dynamic> json) => Todo(
id: json['id'] as int,
title: json['title'] as String,
completed: json['completed'] as bool? ?? false,
);
}
Future<List<Todo>> fetchTodos() async {
final uri = Uri.parse('https://jsonplaceholder.typicode.com/todos');
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Request failed with status ${response.statusCode}');
}
final List<dynamic> data = jsonDecode(response.body) as List<dynamic>;
return data
.map((item) => Todo.fromJson(item as Map<String, dynamic>))
.toList();
}
Showing async data: FutureBuilder
class TodoScreen extends StatefulWidget {
const TodoScreen({super.key});
@override
State<TodoScreen> createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
late Future<List<Todo>> _future;
@override
void initState() {
super.initState();
// Create the future ONCE, not inside build().
_future = fetchTodos();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Todo>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final todos = snapshot.data ?? const [];
if (todos.isEmpty) {
return const Center(child: Text('No todos.'));
}
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, i) => ListTile(title: Text(todos[i].title)),
);
},
);
}
}
Never create the Future inside build().
Every rebuild would fire a fresh network request. Create it in
initState (or in a state-management layer) and store it.
Using BuildContext across an await
Future<void> _save() async {
await api.save();
// The user may have navigated away while we waited.
if (!mounted) return; // in a State object
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Saved!')),
);
}
13 · Project structure & commands
my_app/
├── lib/ # ← all your Dart code lives here
│ ├── main.dart # entry point
│ ├── models/ # plain data classes
│ ├── screens/ # one file per page
│ ├── widgets/ # reusable UI pieces
│ └── services/ # API clients, storage
├── test/ # unit & widget tests
├── android/ ios/ web/ # platform-specific shells
├── assets/ # images, fonts, JSON you ship
├── pubspec.yaml # dependencies + assets + metadata
└── analysis_options.yaml # linter rules
name: my_app
description: A sample Flutter app.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '^3.5.0'
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
provider: ^6.1.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter:
uses-material-design: true
assets:
- assets/images/
# fonts:
# - family: MyFont
# fonts:
# - asset: assets/fonts/MyFont-Regular.ttf
| Command | What it does |
|---|---|
flutter create name | Scaffold a new project. |
flutter run | Build & launch on the selected device. |
flutter run -d chrome | Run in a browser. |
flutter devices | List connected devices and emulators. |
flutter pub get | Download the packages in pubspec.yaml. |
flutter pub add http | Add a dependency and update pubspec. |
flutter pub outdated | Show packages with newer versions. |
flutter analyze | Static analysis / lint check. |
flutter test | Run the test suite. |
dart format . | Auto-format your code. |
flutter clean | Delete build artifacts when things get weird. |
flutter build apk | Produce a release Android build. |
14 · Common beginner mistakes
| Mistake | Why it hurts | Do this instead |
|---|---|---|
setState inside build() |
Infinite rebuild loop | Call it from callbacks or lifecycle methods |
Creating a Future in build() |
Refires the request on every rebuild | Create it in initState and store it |
Forgetting dispose() |
Memory leaks, "setState after dispose" errors | Dispose controllers, focus nodes, listeners |
Using context after await |
The widget may be gone | Check if (!mounted) return; first |
Huge build() methods |
Hard to read, everything rebuilds together | Extract small widgets into their own classes |
Using Column for long lists |
Builds every child at once; slow and memory heavy | Use ListView.builder |
Ignoring const |
Unnecessary rebuilds | Let the linter add const for you |
| Extracting widgets as methods | The whole parent still rebuilds | Extract as a StatelessWidget class |
Forgetting super.key |
Widget identity/state bugs in lists | Always const MyWidget({super.key}); |
| Hardcoding sizes everywhere | Breaks on other screen sizes | Use Expanded, Flexible, relative units |
Widget method vs. widget class
// ❌ Works, but the whole thing rebuilds together and you lose const.
Widget _buildHeader() {
return const Text('Header');
}
// ✅ Separate widget: Flutter can skip rebuilding it when nothing changed.
class _Header extends StatelessWidget {
const _Header();
@override
Widget build(BuildContext context) => const Text('Header');
}
15 · Cheat sheet
| I want to… | Use |
|---|---|
| Show text | Text('hi') |
| Add space | SizedBox(height: 16) |
| Add padding | Padding(padding: EdgeInsets.all(16), child: …) |
| Stack vertically | Column(children: […]) |
| Stack horizontally | Row(children: […]) |
| Fill remaining space | Expanded(child: …) |
| Centre something | Center(child: …) |
| Scroll a list | ListView.builder(…) |
| Scroll arbitrary content | SingleChildScrollView(child: …) |
| Make a tappable card | Card(child: InkWell(onTap: …, child: …)) |
| Show a button | ElevatedButton(onPressed: …, child: …) |
| Show a loading spinner | CircularProgressIndicator() |
| Show a toast-like message | ScaffoldMessenger.of(context).showSnackBar(…) |
| Open a new screen | Navigator.push(context, MaterialPageRoute(…)) |
| Go back | Navigator.pop(context) |
| Get screen size | MediaQuery.sizeOf(context) |
| Read the theme | Theme.of(context).colorScheme.primary |
| Rebuild on state change | setState(() { … }) |
| Run code once on mount | initState() |
| Clean up on unmount | dispose() |
| Show an image from the web | Image.network(url) |
| Add a fixed-size gap in a Row | SizedBox(width: 8) |
16 · Glossary
The vocabulary you'll meet in docs, Stack Overflow answers and code review.
- Widget core
- An immutable description of part of the UI. Widgets are cheap, disposable configuration objects — think of them as blueprints, not the building itself. Everything on screen is a widget, including layout, padding and gesture handling.
- Element internals
-
The runtime counterpart to a widget. When Flutter builds a widget, it
creates (or reuses) an
Elementthat remembers where that widget sits in the tree and holds itsState. You almost never touch elements directly. - RenderObject internals
- The object that actually performs layout (computing size and position) and painting. Each widget creates one, but you'll only meet them if you write a custom render widget.
- Widget tree
-
The nested structure formed by your widgets.
MyAppcontainsMaterialAppcontainsScaffoldcontainsColumncontainsText, and so on. - BuildContext
-
A handle to where a widget is in the tree. It's how you look up
inherited data —
Theme.of(context),MediaQuery.of(context),Navigator.of(context). If you call it after anawait, checkmountedfirst. build()-
The method that returns the widget tree for a widget. It runs often —
keep it cheap and free of side effects: no network calls, no
setState, noFuturecreation. - StatelessWidget
-
A widget with no mutable state. Its appearance depends only on its
constructor arguments. Example:
Text,Icon, a static card. - StatefulWidget
-
A widget that owns mutable state via a companion
Stateobject. Changing that state (insidesetState) triggers a rebuild. Example: a counter, a form, a checkbox. - State
-
The long-lived object attached to a
StatefulWidget. It survives rebuilds, hasinitState/disposelifecycle hooks, and holds your mutable fields. setState()-
The method you call to say "this widget's state changed, please rebuild
it." Mutate your fields inside the callback. Never call it during
build(). - Hot reload
-
Flutter's superpower: injects edited code into a running app in under a
second while preserving state. Triggered with
rin the terminal or the ⚡ button in your IDE. - Hot restart
- Restarts the app from scratch, discarding all state. Slower than hot reload, but resets everything. Use it when hot reload gives strange results.
constconstructor- Marks a widget as compile-time constant so Flutter can create it once and reuse the same instance across rebuilds. Free performance; your linter will nag you to add it.
- Key
-
An identity tag that helps Flutter match widgets between rebuilds.
ValueKeyfor list items that reorder,GlobalKeyto reach into a widget from outside (e.g.GlobalKey<FormState>). - Scaffold
-
The standard page skeleton: app bar, body, floating action button,
drawer, bottom bar, snack bars. Most screens are a
Scaffoldwrapped in aMaterialApp. - Material / Cupertino
- The two built-in design languages. Material is Google's (Android-flavoured, default). Cupertino mimics iOS. You can mix them.
- Theme
-
A central object holding colours, typography and component styles.
Set it once on
MaterialAppand read it anywhere withTheme.of(context). - Sliver
-
A scrollable piece of UI — the building block of
CustomScrollView.SliverAppBar,SliverList. Advanced, but the answer to "sticky header plus scrolling list". - Route
-
A single screen/page in the navigation stack. Pushed and popped with
Navigator. - InheritedWidget
-
The built-in mechanism for passing data down the tree without threading
it through every constructor.
ThemeandMediaQueryare InheritedWidgets. Most state-management packages are built on this. - Provider / Riverpod / Bloc
- Popular third-party state-management approaches. Provider is the friendliest starting point; Riverpod is more modern and testable; Bloc is event-driven and common in larger codebases.
- pubspec.yaml
-
Your project's manifest: name, dependencies, dev dependencies, assets,
fonts. Edited directly, or via
flutter pub add. - pub.dev
- The official package repository for Dart and Flutter. Check a package's "likes", "pub points" and last-updated date before adopting it.
- Platform channel / MethodChannel
- The bridge that lets Dart call native Kotlin/Swift/Java/C++ code (and vice versa). Needed for platform-specific APIs. You can usually find a pub package that wraps it for you.
- Isolate
-
Dart's unit of concurrency — a separate memory heap and event loop.
Use
Isolate.run()orcompute()for heavy work (parsing huge JSON, image processing) so the UI thread stays smooth. - Event loop
- Dart runs your synchronous code on one thread with an event loop. Async functions don't create threads — they yield control and resume later. Blocking that loop blocks the whole UI.
- Impeller / Skia
- The rendering backends. Skia is the original; Impeller is the newer engine that precompiles shaders to avoid first-run animation jank.
- DevTools
-
A browser-based debugging suite: widget inspector, performance timeline,
memory profiler, network view. Launch it from your IDE or
flutter runoutput. - JIT / AOT
- Just-In-Time compilation (used in debug mode for fast hot reload) versus Ahead-Of-Time compilation (used for release builds for fast startup and no VM overhead).
- Golden test
- A widget test that renders a widget and compares the pixels to a stored reference image. Catches unintended visual changes.
- Semantics
-
The accessibility tree Flutter builds alongside the widget tree, used by
screen readers. The
Semanticswidget lets you annotate custom UI.
17 · Idioms & Flutter-isms
Short phrases the community repeats. Each one encodes a real design rule.
- "Everything is a widget."
- There is no separate styling or layout language. Padding, alignment, gestures, animation, theming — all widgets. If you want to change something, you wrap it in another widget.
- "Widgets are immutable blueprints."
-
You don't change a widget; you replace it with a new one describing the
new state. That's why
build()returns a fresh tree each time instead of mutating the old one. - "UI = f(state)."
- The core mental model. Your screen is a pure function of the current state. Change the state, and Flutter re-runs the function. Stop thinking "show/hide this view" and start thinking "what does the state say?"
- "Lift state up."
- When two widgets need the same data, move it to their nearest common ancestor and pass it down. Sibling widgets should never own shared state.
- "Constraints go down, sizes go up, parents set position."
- The layout algorithm in one line. Parent gives constraints → child picks a size within them → parent places the child. Every overflow and unbounded-height error comes from breaking this contract.
- "Composition over inheritance."
-
Don't subclass
Containerto make a fancy box. Build a new widget that contains the widgets you want. Flutter's whole widget library is built this way. - "Make it const."
-
The easiest performance advice in Flutter. A
constwidget can be reused across rebuilds, skipping a whole subtree of work. - "Keep
build()pure and cheap." -
build()can be called dozens of times per second. No side effects, no I/O, no expensive computation. Precompute ininitStateor a service. - "Extract a widget, don't extract a method."
-
Returning widgets from a helper method keeps them in the parent's
rebuild scope. A separate
StatelessWidgetclass lets Flutter skip it when its inputs haven't changed. - "If it's stateful, dispose it."
-
Anything with a lifecycle — controllers, focus nodes, listeners,
streams — must be cleaned up in
dispose(). - "Check
mountedafter everyawait." -
The widget may have been removed from the tree while you were waiting.
Calling
setStateor usingcontextafterwards throws. - "Push and pop."
- Navigation vocabulary from the route stack. You push a screen on top and pop it off. The back button pops.
- "It's a package for that."
-
The community's answer to almost any platform feature. Search
pub.devbefore writing platform channels by hand. - "Works on my emulator."
- The classic reminder that layout that fits one screen will overflow on another. Test on a small phone, a big phone and a tablet — or use the Device Preview package.
- "Just hot restart."
- When hot reload produces bizarre behaviour (stale state, changed constants not applying), restart before you debug.
18 · Acronyms
| Acronym | Stands for | In Flutter terms |
|---|---|---|
| SDK | Software Development Kit | The Flutter SDK you install and add to PATH. |
| UI | User Interface | Everything the user sees and touches. |
| UX | User Experience | How the whole interaction feels, not just how it looks. |
| IDE | Integrated Development Environment | VS Code or Android Studio, with the Flutter plugin. |
| CLI | Command-Line Interface | The flutter and dart commands. |
| API | Application Programming Interface | The functions a package exposes; also a remote HTTP service. |
| JSON | JavaScript Object Notation | The usual wire format for API data; decoded with jsonDecode. |
| HTTP | HyperText Transfer Protocol | How you fetch data; use the http or dio package. |
| REST | Representational State Transfer | A common style of HTTP API you'll consume. |
| JIT | Just-In-Time (compilation) | Debug mode — fast rebuilds, slower startup. |
| AOT | Ahead-Of-Time (compilation) | Release mode — native machine code, fast startup. |
| VM | Virtual Machine | The Dart VM that runs your debug app. |
| FAB | Floating Action Button | The round button in the corner of a Scaffold. |
| MD3 / M3 | Material Design 3 | The current Material spec; enable with useMaterial3: true. |
| HIG | Human Interface Guidelines | Apple's design spec — what Cupertino widgets follow. |
| OOP | Object-Oriented Programming | Dart's default paradigm; widgets are classes. |
| DRY | Don't Repeat Yourself | Extract repeated UI into reusable widgets. |
| KISS | Keep It Simple, Stupid | Reach for setState before a state-management framework. |
| YAGNI | You Aren't Gonna Need It | Don't build abstraction layers for screens that don't exist yet. |
| MVP | Minimum Viable Product | The smallest app worth shipping — often one flutter run away. |
| CI / CD | Continuous Integration / Continuous Delivery | Automated flutter test and flutter build pipelines. |
| FPS | Frames Per Second | The smoothness metric; 60 or 120 is the target, 16ms per frame. |
| DIP | Device-Independent Pixel | Flutter's logical unit; MediaQuery converts to physical pixels. |
| a11y | Accessibility | Screen readers, contrast, tap-target sizes. Flutter calls this Semantics. |
| i18n / l10n | Internationalization / Localization | Supporting multiple languages with flutter_localizations. |
Where to go next. Build something small and real — a
notes app, a weather app, a habit tracker. Read the official
Flutter Cookbook whenever you're stuck on a specific UI problem,
and search pub.dev before writing anything from scratch.