What is Flutter?
Flutter is Google's open-source UI toolkit for building natively compiled, cross-platform apps — iOS, Android, web, desktop, and embedded — from a single codebase. Instead of wrapping platform components, Flutter renders every pixel itself with its own engine (Impeller on modern versions), so the UI looks and behaves identically everywhere and stays consistent as the OS changes.
Apps are written in Dart, a typed, object-oriented language. In development, Flutter uses a JIT compiler to enable hot reload; for release it compiles Dart ahead-of-time to native machine code for fast startup and smooth 60/120fps rendering.
Dart basics
Dart 3 is sound null-safe: a type like String can never be null, while String? can. Use final for run-time constants and const for compile-time constants.
// Variables and null safety
final String name = 'Ada'; // inferred + immutable
int? age; // nullable, defaults to null
const pi = 3.14159; // compile-time constant
// Functions with named + optional params
String greet(String who, {String greeting = 'Hi'}) => '$greeting, $who!';
print(greet('Ada', greeting: 'Hello'));
// Collections + spread + collection-if
final nums = <int>[1, 2, 3];
final doubled = [for (final n in nums) n * 2]; // [2, 4, 6]
final tags = ['dart', if (age != null) 'has-age'];
// Records (Dart 3) and pattern matching
(int, String) pair = (200, 'OK');
final (code, msg) = pair; // destructure
switch (code) {
case 200: print('ok');
case >= 400: print('error');
}
// Classes
class User {
final String name;
final int age;
const User(this.name, this.age);
bool get isAdult => age >= 18;
}
Everything is a Widget
In Flutter the entire UI is a tree of widgets — not just buttons and text, but also padding, alignment, and even the app itself. Widgets are immutable descriptions of part of the UI; Flutter builds an element tree from them and reconciles changes efficiently. You compose complex UIs by nesting small widgets rather than subclassing.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
colorSchemeSeed: Colors.teal,
useMaterial3: true, // Material 3 by default
),
home: const HomePage(),
);
}
}
Hot reload
Save a file (or press r in the terminal) and Flutter injects updated source into the running app in under a second — preserving app state. Use hot restart (R) to reset state when you change main() or top-level fields.
StatelessWidget vs StatefulWidget
A StatelessWidget is immutable — given the same inputs it always renders the same way (a label, an icon, a static card). A StatefulWidget pairs an immutable widget with a mutable State object that persists across rebuilds and can change over time.
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0; // mutable state lives here
void _increment() {
setState(() => _count++); // schedules a rebuild of build()
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count: $_count', style: Theme.of(context).textTheme.headlineMedium),
FilledButton(onPressed: _increment, child: const Text('Add')),
],
);
}
}
setState and the build method
setState() marks the widget dirty and tells Flutter to re-run build(). The build() method must be pure and fast — it can be called many times per second, so never do network calls or heavy work inside it. Mutate fields inside the setState callback, then let build read them.
Use const widgets
Marking a widget subtree const lets Flutter skip rebuilding and re-diffing it entirely. Prefer const constructors wherever a widget's inputs never change — it is one of the cheapest performance wins available.
Layout Widgets
Layout in Flutter is composition. Column and Row arrange children vertically/horizontally; Expanded and Flexible divide remaining space; Stack overlaps children; Container and Padding add decoration and spacing.
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Dashboard', style: TextStyle(fontSize: 22)),
const SizedBox(height: 12),
Row(
children: [
Expanded( // takes 2/3 of the row
flex: 2,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.teal.shade50,
borderRadius: BorderRadius.circular(12),
),
child: const Text('Main'),
),
),
const SizedBox(width: 8),
Expanded(child: Container(color: Colors.grey.shade200, height: 60)),
],
),
],
),
)
// Overlap with Stack
Stack(
alignment: Alignment.bottomRight,
children: [
Image.network('https://picsum.photos/300'),
const Padding(padding: EdgeInsets.all(8), child: Icon(Icons.favorite)),
],
)
Material 3 Widgets
With useMaterial3: true, Flutter ships the latest Material Design components. A typical screen uses Scaffold as the structural shell, with an AppBar, body, and optional navigation.
Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton(onPressed: () {}, child: const Text('Filled')),
OutlinedButton(onPressed: () {}, child: const Text('Outlined')),
Card(child: ListTile(
leading: const Icon(Icons.person),
title: const Text('Ada Lovelace'),
subtitle: const Text('Engineer'),
trailing: const Icon(Icons.chevron_right),
)),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
bottomNavigationBar: NavigationBar(
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'),
],
),
)
Lists with ListView.builder
For long or dynamic lists, use ListView.builder, which lazily builds only the items currently on screen instead of all of them at once — essential for performance with large data sets.
final items = List<String>.generate(1000, (i) => 'Item #$i');
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('$index')),
title: Text(items[index]),
onTap: () => debugPrint('tapped ${items[index]}'),
);
},
)
// Separated variant adds dividers between items
ListView.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) => Text(items[i]),
)
Navigation
The imperative Navigator works with a stack of routes. It is fine for simple apps, but for deep links, web URLs, and nested navigation, the go_router package is the modern, declarative standard.
// Imperative Navigator
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const DetailPage(id: 42)),
);
Navigator.of(context).pop(); // go back
// Declarative with go_router
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (_, __) => const HomePage()),
GoRoute(
path: '/user/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return UserPage(id: id);
},
),
],
);
// Wire into MaterialApp.router
MaterialApp.router(routerConfig: router);
// Navigate by URL
context.go('/user/42'); // replace stack
context.push('/settings'); // push on top
State Management
setState handles local, ephemeral UI state. For state shared across screens, use a dedicated solution. Provider exposes objects down the tree via InheritedWidget; Riverpod is its compile-safe successor with no BuildContext requirement.
Provider
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() { _count++; notifyListeners(); }
}
// Provide at the top
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
);
// Read + rebuild on change
final count = context.watch<CounterModel>().count;
context.read<CounterModel>().increment(); // call without listening
Riverpod
import 'package:flutter_riverpod/flutter_riverpod.dart';
// A Notifier holds and mutates state
class Counter extends Notifier<int> {
@override
int build() => 0; // initial state
void increment() => state++;
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
// Wrap the app once
void main() => runApp(const ProviderScope(child: MyApp()));
// Consume inside a ConsumerWidget
class CounterView extends ConsumerWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return TextButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('$count'),
);
}
}
Async, Future & FutureBuilder
Dart handles asynchrony with Future (a single value later) and Stream (many values over time), using async/await. In the UI, FutureBuilder rebuilds based on a Future's state.
Future<String> loadName() async {
await Future.delayed(const Duration(seconds: 1));
return 'Ada';
}
FutureBuilder<String>(
future: loadName(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return Text('Hello, ${snapshot.data}');
},
)
Fetching data over HTTP
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<List<Map<String, dynamic>>> fetchPosts() async {
final res = await http.get(Uri.parse('https://api.example.com/posts'));
if (res.statusCode != 200) {
throw Exception('Failed: ${res.statusCode}');
}
final data = jsonDecode(res.body) as List;
return data.cast<Map<String, dynamic>>();
}
Forms & TextField
Use a TextField for standalone input, or wrap TextFormFields in a Form with a GlobalKey for validation.
final _formKey = GlobalKey<FormState>();
final _controller = TextEditingController();
Form(
key: _formKey,
child: Column(children: [
TextFormField(
controller: _controller,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || !value.contains('@')) return 'Invalid email';
return null;
},
),
FilledButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
debugPrint('Submitting ${_controller.text}');
}
},
child: const Text('Submit'),
),
]),
)
Flutter vs Alternatives
| Aspect | Flutter | React Native | Native (Kotlin/Swift) |
|---|---|---|---|
| Language | Dart | JS/TS | Kotlin / Swift |
| Rendering | Own engine (Impeller) | Native components via bridge | Native components |
| Codebase | Single, all platforms | Single (mostly) | One per platform |
| UI consistency | Identical everywhere | Follows platform look | Fully native |
| Best for | Custom, branded UIs | JS-heavy teams | Deep platform features |
Practice Exercises
- Build a counter app with a
StatefulWidgetthat increments and decrements, disabling the decrement button at zero. - Create a profile card using
Column,Row,Expanded, andContainerdecoration, wrapped in a Material 3Card. - Render a 500-item list with
ListView.builderand navigate to a detail screen on tap usinggo_routerpath parameters. - Fetch posts from a public REST API with the
httppackage and display them with aFutureBuilder, showing loading and error states. - Refactor a counter from
setStateto a RiverpodNotifierProvidershared across two screens. - Build a login
Formwith email and password validation, and print the values only when validation passes.