One Codebase, Every Screen
Flutter builds iOS, Android, web, and desktop apps from a single Dart codebase. This track takes you from your first line of Dart to a published app — widgets, state, networking, Firebase, and release. Everything you learn goes into one small app you build session by session: DayList, a personal task manager.
How to use this guide
- Run every example in DartPad for the first module, then move to a local Flutter project.
- Type the code yourself — do not copy-paste the demonstrations.
- Complete each practice before opening the answer key.
- Finish every session with its project step — that is what turns lessons into an app.
- Keep a notes file with your apps, mistakes, and fixes.
The app you'll build: DayList
A personal task manager — one simple entity (a Task) that every module grows. By the last session you will have built, tested, and shipped it.
- Module 1 · Dart: the task model and console logic — add, complete, filter, save.
- Module 2 · Flutter Foundations: the real UI — task list, add-task dialog, theme.
- Module 3 · Navigation and State: detail screen, validated form, Provider state, saved preferences.
- Module 4 · Networking and Data: sync with an API, offline storage, loading/error/empty states, pagination.
- Module 5 · Firebase: sign-in, realtime per-user tasks, attachments, due-date reminders.
- Module 6 · Polish and Deployment: animations, responsive layout, tests, a signed release build.
- Module 7 · Capstone: finish the feature set, break it, fix it, and publish.
What's inside
- Module 1 · Dart Essentials — 6 sessions · ready
- Module 2 · Flutter Foundations — 7 sessions · ready
- Module 3 · Navigation and State — 6 sessions · ready
- Module 4 · Networking and Data — 5 sessions · ready
- Module 5 · Firebase and Backend — 5 sessions · ready
- Module 6 · Polish and Deployment — 5 sessions · ready
- Module 7 · Capstone — 3 sessions · ready
Module 1 · Dart Essentials
Dart Basics
By the end of this session, you can:
- Run Dart code in DartPad.
- Declare variables with var, final, and const.
- Explain null safety with ? and ??.
Sources: Dart Language Tour · dart.dev/language
Theory
Flutter is written in Dart, a typed language that stays terse. Every program starts in main(). Types include int, double, String, and bool — but var lets the compiler infer them. final is set once at runtime; const is fixed at compile time.
Null safety means a variable can only be null if you say so with ?. The ?? operator gives a fallback for a null value.
Demonstration
void main() {
var name = 'Ahmad'; // inferred String
final city = 'Peshawar'; // set once
const pi = 3.14159; // compile-time constant
int age = 25;
double price = 9.99;
bool isStudent = true;
// Null safety: ? allows null, ?? gives a fallback value.
String? nickname = null; // the ? is Dart syntax: "may be null"
print(nickname ?? 'no nickname'); // ?? = use the right side if null
}Open dartpad.dev and run it. Now remove the ? from nickname — the analyzer refuses, because null safety caught a crash before it happened.
Practice
- Declare your name, age, and city using var, final, and const.
- Create a nullable int and print it with a ?? fallback.
- What is the difference between final and const?
- Why does null safety exist?
Answer key and troubleshooting notes
final is locked at runtime when first assigned; const is known at compile time. Null safety turns null crashes into compile errors — the most common source of app crashes disappears before the app runs.
Project step: Create daylist.dart and declare the app constants: appName and defaultPriority with final and const, plus a nullable dueDate set to null.
Quick questions
1. When would you use var instead of an explicit type?
When the initializer makes the type obvious — var keeps the code short while the compiler still enforces the type.
2. What does ?? do?
It returns the left value unless it is null, in which case it returns the right fallback value.
Wrap Up
Dart is typed but terse — var for inference, final for set-once, const for constants, ? for maybe-null.
Functions and Control Flow
By the end of this session, you can:
- Write functions with named parameters.
- Use if/else, switch, and loops.
- Write one-line arrow functions.
Sources: Dart Language Tour · dart.dev/language
Theory
Functions bundle logic and take parameters. Dart supports named parameters in curly braces with defaults — Flutter's own APIs use them constantly, so reading them is essential. Control flow is familiar: if / else, switch, for, while, and for-in.
Demonstration
String greet(String name, {String greeting = 'Hello'}) {
return '$greeting, $name!';
}
int add(int a, int b) => a + b; // arrow function
void main() {
print(greet('Ahmad')); // Hello, Ahmad!
print(greet('Ahmad', greeting: 'Salam'));
for (var i = 1; i <= 5; i++) {
if (i % 2 == 0) print('$i is even');
}
}The arrow syntax => is shorthand for a function whose body is a single returned expression.
Practice
- Write a function that greets by name with a customizable greeting.
- Convert a two-line function into an arrow function.
- Print FizzBuzz for 1–15 (Fizz for 3, Buzz for 5, FizzBuzz for both).
- Iterate over a list with for-in.
Answer key and troubleshooting notes
Named parameters make call sites read like sentences: greet(name, greeting: 'Salam') is self-documenting, which is why every Flutter widget constructor uses them.
Project step: Add three functions to daylist.dart: priorityLabel(int), isOverdue(DateTime?), and taskSummary(String title, {bool isCompleted = false, int priority = 1}) using named parameters.
Quick questions
1. What is the difference between positional and named parameters?
Positional arguments must come in order; named arguments are passed by name with defaults, so order does not matter and call sites stay readable.
2. When is the arrow syntax appropriate?
For single-expression functions — it trades a block body for one clean line.
Wrap Up
Named parameters keep calls readable — and Flutter's APIs use them everywhere.
Collections
By the end of this session, you can:
- Use List, Set, and Map.
- Transform collections with map and where.
- Pick the right collection for the job.
Sources: Dart Language Tour · dart.dev/language
Theory
A List keeps ordered items, a Set keeps unique items, and a Map stores key → value pairs. Functional methods do the heavy lifting: where filters, map transforms, reduce combines. Most app data is one of these three shapes.
Demonstration
void main() {
final students = ['Ali', 'Sara', 'Bilal'];
students.add('Hina');
print(students.length); // 4
final ages = {'Ali': 20, 'Sara': 22};
print(ages['Ali']); // 20
final adults = ages.keys
.where((name) => ages[name]! >= 21)
.toList();
print(adults); // [Sara]
}Chain the methods: take a collection, filter it, transform it, convert it back to a list — this pattern appears in every Flutter screen that shows data.
Practice
- Build a list of five cities and print its length.
- Filter the list to cities starting with a chosen letter.
- Store names and ages in a map, then list everyone over 18.
- What is the difference between a List and a Set?
Answer key and troubleshooting notes
A List preserves order and allows duplicates; a Set stores each value once. Use where for filtering and map for transforming — both return a new iterable, so finish with toList() when you need a list.
Project step: Store tasks in a List of maps: add four tasks, filter the unfinished ones with where, and print the count of completed tasks.
Quick questions
1. When would you choose a Set over a List?
When duplicates would be a bug — tags, IDs, or selected items — and you want automatic uniqueness.
2. What does map() do?
It transforms every element with a function and returns the transformed results, leaving the original collection unchanged.
Wrap Up
List for order, Set for uniqueness, Map for lookup — and where() filters anything.
Classes and Objects
By the end of this session, you can:
- Define a class with fields and a constructor.
- Use named constructor parameters.
- Add methods and use toString.
Sources: Dart Language Tour · dart.dev/language
Theory
A class bundles data and behavior. The constructor initializes the fields; Dart's named parameters make construction readable. Methods operate on the object's data. Every Flutter widget is a class with a build method — this session is the foundation of all of Flutter.
Demonstration
class Student {
final String name;
final int age;
Student({required this.name, required this.age});
String introduce() => 'I am $name, $age years old.';
@override
String toString() => 'Student($name, $age)';
}
void main() {
final s = Student(name: 'Ahmad', age: 25);
print(s.introduce());
print(s);
}required forces the caller to provide the field; final makes the object immutable — the same conventions Flutter widgets follow.
Practice
- Define a Book class with title and pages.
- Add a method that describes the book.
- Instantiate three books and print each description.
- Why are the fields final in the example?
Answer key and troubleshooting notes
Immutable objects are predictable: once created they never change, which avoids a whole class of bugs and lets Flutter rebuild screens safely.
Project step: Replace the task maps with a Task class: id, title, isCompleted, priority, dueDate — with a named constructor, a copyWith method, and toString. This class is the heart of the whole app.
Quick questions
1. What does required mean on a constructor parameter?
The caller must supply it — the compiler rejects calls that leave it out, catching mistakes at build time.
2. How are Flutter widgets related to classes?
Every widget is a class whose build() method returns the UI — so a class with data plus a method is already a widget in miniature.
Wrap Up
A class bundles data and behavior — Flutter widgets are classes with build().
Async: Futures and async/await
By the end of this session, you can:
- Explain what a Future is.
- Await a Future with async/await.
- Handle errors with try/catch.
Sources: Dart Language Tour · dart.dev/language
Theory
Network calls and file reads take time. A Future<T> is a value that will arrive later. Marking a function async lets you await a Future — code pauses there until the value is ready, without blocking the whole app. Errors are caught with try / catch.
Demonstration
Future<String> fetchName() async {
await Future.delayed(Duration(seconds: 2));
return 'Ahmad';
}
void main() async {
try {
final name = await fetchName();
print(name);
} catch (e) {
print('Failed: $e');
}
}await never freezes the app — only this function waits.
Practice
- Write an async function that returns a number after a delay.
- Await it in main and print the result.
- Add a try/catch around a function that throws.
- What does async mark on a function?
Answer key and troubleshooting notes
async allows await inside the function and makes it return a Future. try/catch turns runtime failures into handled states instead of crashes.
Project step: Write Future<List<Task>> loadTasks() and Future<void> saveTasks(List<Task>) that simulate storage with Future.delayed, and wrap the load in try/catch with a fallback empty list.
Quick questions
1. What is the difference between a Future and a plain value?
A plain value is available now; a Future is a promise of a value that finishes later — you await it to get the real value.
2. Why does the UI stay responsive during await?
The Dart event loop suspends only the awaiting function and keeps processing other events, so animations and taps continue.
Wrap Up
Futures are Flutter's heartbeat — every network call is an await away.
First Console Apps
By the end of this session, you can:
- Combine lists, functions, and classes into small programs.
- Break a problem into functions.
- Debug with print and reasoning.
Sources: Dart Language Tour · dart.dev/language
Theory
Before any UI, practice pure logic: model the data, write the functions, run main(). Every Flutter screen is this same pattern with widgets on top — the logic is identical. Work in DartPad; keep each app in one file; run after every change.
Demonstration
void main() {
final grades = {'Ali': 85, 'Sara': 92, 'Bilal': 78};
double average(Map<String, int> grades) =>
grades.values.reduce((a, b) => a + b) / grades.length;
final best = grades.entries
.reduce((a, b) => a.value >= b.value ? a : b);
print('Average: ${average(grades)}');
print('Top: ${best.key} (${best.value})');
}The thought process: what is the data (a map)? What do we want (average, best)? One small function per answer, then print the results.
Practice
- Build a to-do list app: add, remove, and print tasks.
- Build a temperature converter: Celsius to Fahrenheit.
- Build a quiz app: questions, answers, and a score.
- When your program misbehaves, print the state at each step and explain what changed.
Answer key and troubleshooting notes
Console apps force you to separate data from logic — the exact separation Flutter asks for later: state (data) in one place, UI (widgets) in another.
Project step: Finish the console version of DayList: a menu loop with add, list, complete, and delete, loading the list with await loadTasks() at start and saving after every change. Module 1 complete — you have the app's brain.
Quick questions
1. How do you break a large problem into functions?
Name the sub-results you need (average, top student) and write one function per result — each becomes testable on its own.
2. Why practice without a UI first?
Widgets add noise. Getting the logic right in the console means every UI bug later is about the screen, not the data.
Wrap Up
Console apps teach logic without UI noise — every Flutter screen is this logic plus widgets.
Module 2 · Flutter Foundations
Flutter Setup and First App
By the end of this session, you can:
- Install the Flutter SDK and run flutter doctor.
- Create a new project and run it on a device.
- Use hot reload while developing.
Sources: docs.flutter.dev/get-started
Theory
Flutter needs the SDK, an editor (VS Code or Android Studio), and at least one device — an emulator, a physical phone, or the desktop/web target. flutter doctor checks every dependency and tells you what is missing. A project starts with flutter create.
flutter doctor flutter create daylist cd daylist flutter run
Hot reload re-sends only your changed code to the running app in under a second — state survives. Hot restart rebuilds from scratch. That loop is the reason Flutter development feels fast.
Demonstration
Save a file, press r, and the change appears immediately.
Practice
- Run flutter doctor and fix anything it flags.
- Create a project and run the default counter app.
- Change the counter text and hot reload without restarting.
- What is the difference between hot reload and hot restart?
Answer key and troubleshooting notes
Hot reload keeps the app's state and patches only changed code; hot restart discards state and runs main() again. A clean flutter doctor means the SDK, editor, and device are all connected.
Project step: Create the daylist project, run the default counter app, and rename it to DayList — this project will hold everything from now on.
Quick questions
1. What does flutter doctor tell you?
It reports the health of every dependency — SDK version, editor plugins, device toolchain — and lists concrete fixes for anything missing.
2. Why is the hot reload loop valuable?
You see the effect of every edit within a second, so experiments are nearly free and layout changes are tuned visually.
Wrap Up
flutter create makes the project, flutter run makes it live, and hot reload makes it fast.
Widgets, Widgets, Widgets
By the end of this session, you can:
- Explain the widget tree.
- Write a StatelessWidget.
- Compose Scaffold, AppBar, Text, and Center.
Sources: docs.flutter.dev/ui/widgets-intro
Theory
In Flutter everything on screen is a widget — buttons, padding, text, even the app itself. Widgets compose into a tree, and each build() method describes a piece of the UI. Flutter redraws the tree whenever something changes.
Demonstration
import 'package:flutter/material.dart';
void main() => runApp(const DayListApp());
class DayListApp extends StatelessWidget {
const DayListApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('DayList')),
body: const Center(child: Text('No tasks yet')),
),
);
}
}MaterialApp is the app root; Scaffold gives the page its frame (app bar, body); Center positions the text. Four widgets, one screen.
Practice
- Explain what a widget tree is.
- Write a StatelessWidget that shows your name centered on screen.
- Add an AppBar with a different title.
- Why is composition preferred over inheritance in Flutter?
Answer key and troubleshooting notes
A widget tree is the nested structure of widgets returned by build() methods. Composition means small widgets combine into bigger ones, which keeps each widget single-purpose and testable.
Project step: Replace the counter app with a DayListApp StatelessWidget: an AppBar titled DayList and a centered placeholder message.
Quick questions
1. What does build() return?
A widget — the description of this widget's UI. Flutter calls it when the widget is created or its dependencies change.
2. Is MaterialApp a widget?
Yes — the root widget that configures navigation, theme, and rendering for the whole app.
Wrap Up
Widgets are lego bricks — build() describes the UI, and Flutter draws it.
Layouts: Row, Column, and Friends
By the end of this session, you can:
- Compose Row and Column.
- Use Expanded and mainAxisAlignment.
- Pad and space children.
Sources: docs.flutter.dev/ui/layout
Theory
Row lays children out horizontally, Column vertically. mainAxisAlignment positions children along that axis; Expanded gives a child the remaining space. Padding and SizedBox control breathing room.
Const gotcha: const only works when everything inside is const too. Writing const TaskTile(task: Task(...)) fails with "isn't a const constructor" — because Task is built at runtime. Drop the const and it compiles.
Demonstration
Row(
children: [
const Icon(Icons.circle_outlined),
const SizedBox(width: 12),
Expanded(child: Text('Buy groceries')),
const Icon(Icons.star, color: Colors.orange),
],
)This row is already a task row: a leading icon, a title that takes the space, and a trailing priority star. The Expanded title pushes the star to the right edge.
Practice
- Build a row with an icon, a text, and a trailing icon.
- Center three icons horizontally using mainAxisAlignment.
- What does Expanded do?
- Stack a Column inside a Padding and explain the order.
Answer key and troubleshooting notes
Expanded claims all leftover space along the row's main axis after fixed-size children are placed. Padding wraps a widget with space on all sides; the column inside is laid out inside that space.
Project step: Build a TaskTile widget: a Row with a checkbox icon, the task title in Expanded, and a priority star on the right.
Quick questions
1. What is the difference between mainAxisAlignment and crossAxisAlignment?
Main axis follows the layout direction (horizontal for Row, vertical for Column); cross axis is the perpendicular one.
2. When would you use SizedBox instead of Padding?
SizedBox reserves exact space (or acts as a gap); Padding adds space around an existing child.
Wrap Up
Rows lay out horizontally, Columns vertically — Expanded shares the leftover space.
State and setState
By the end of this session, you can:
- Explain StatelessWidget versus StatefulWidget.
- Rebuild the UI with setState.
- Describe where widget state lives.
Sources: docs.flutter.dev/ui/interactivity
Theory
A StatelessWidget has no memory; its UI depends only on its inputs. A StatefulWidget holds state in a companion State object. Calling setState tells Flutter "my state changed — rebuild me".
Demonstration
class ToggleTile extends StatefulWidget {
const ToggleTile({super.key});
@override
State<ToggleTile> createState() => _ToggleTileState();
}
class _ToggleTileState extends State<ToggleTile> {
bool _isCompleted = false;
void _toggle() {
setState(() {
_isCompleted = !_isCompleted;
});
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(_isCompleted ? Icons.check_circle : Icons.circle_outlined),
onPressed: _toggle,
);
}
}Tap the icon: setState flips _isCompleted, and build() runs again with the new value. Without setState the icon would never change.
Practice
- Convert a stateless tile into a StatefulWidget.
- Toggle a boolean with setState and show the value as text.
- What happens if you change the variable without calling setState?
- Why does StatefulWidget have two classes?
Answer key and troubleshooting notes
Changing a field without setState leaves the UI stale — Flutter does not know to rebuild. The two classes separate the immutable widget config from the mutable State that survives rebuilds.
Project step: Make TaskTile interactive: tapping the checkbox icon toggles isCompleted with setState and changes the icon to check_circle.
Quick questions
1. What exactly does setState do?
It marks the widget dirty and schedules a rebuild, then build() runs again with the updated state.
2. Where should a field live if two widgets both need it?
It should be lifted to a shared parent (or a state manager) — state that one widget owns alone is invisible to the others.
Wrap Up
setState says 'the UI changed — redraw me'.
Text, Images, and Icons
By the end of this session, you can:
- Style text with TextStyle.
- Use Icon and IconButton.
- Add network images with a fallback.
Sources: docs.flutter.dev/ui/assets
Theory
Text takes a string and an optional TextStyle (size, weight, color, decoration). IconButton is a tappable icon with built-in feedback. Image.network loads from a URL — always give it an errorBuilder because networks fail.
Demonstration
Text(
'Buy groceries',
style: TextStyle(
fontWeight: FontWeight.bold,
decoration: isCompleted ? TextDecoration.lineThrough : null,
),
)
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => print('delete pressed'),
)
Image.network(
url,
errorBuilder: (context, error, stack) => const Icon(Icons.broken_image),
)Practice
- Style a title in bold with strikethrough.
- Add a delete IconButton that prints on tap.
- Load a network image with an errorBuilder fallback.
- Why does Image.network need an errorBuilder?
Answer key and troubleshooting notes
Network requests fail — the server may be down or the URL wrong. Without an errorBuilder the widget throws and breaks the screen; with one, the user sees a graceful fallback.
Project step: Give TaskTile a nicer look: bold task titles with strikethrough when completed, a delete IconButton, and a fallback image for task attachments.
Quick questions
1. What is the difference between Icon and IconButton?
Icon only draws the glyph. IconButton wraps it with tap handling, padding, and the Material ripple.
2. Where do asset images live?
In the assets/ folder, declared in pubspec.yaml under flutter.assets, then loaded with Image.asset.
Wrap Up
Text conveys, icons hint, images illustrate — and every one of them is still a widget.
Styling and Themes
By the end of this session, you can:
- Define a ThemeData for the whole app.
- Use a seeded ColorScheme with Material 3.
- Apply the theme from any widget.
Sources: docs.flutter.dev/cookbook/design/theming
Theory
Styling every widget by hand does not scale. ThemeData sets the app's colors, text styles, and component shapes in one place; every Material widget inherits it. ColorScheme.fromSeed generates a consistent Material 3 palette from a single color.
Demonstration
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
useMaterial3: true,
),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
home: const HomeScreen(),
)Read the theme anywhere with Theme.of(context) — the seeded orange now tints buttons, switches, and the app bar automatically.
Practice
- Seed a Material 3 theme from one color.
- Add a dark theme and system theme mode.
- Read the primary color with Theme.of(context) and use it on an icon.
- Why theme at the app level instead of per widget?
Answer key and troubleshooting notes
Per-widget styling duplicates decisions and drifts. One ThemeData makes the app consistent and lets a single change restyle every screen.
Project step: Give DayList its theme: a deep-orange Material 3 seed, a dark theme that follows the system, and consistent text styles across the app.
Quick questions
1. What does ColorScheme.fromSeed do?
It derives a full, harmonious palette (primary, secondary, surfaces) from one seed color using the Material 3 tonal system.
2. How does a widget access the theme?
Through Theme.of(context), which returns the nearest ThemeData walking up the widget tree.
Wrap Up
Theme once, inherit everywhere — ThemeData is the app's wardrobe.
Building the Home Screen
By the end of this session, you can:
- Compose a full screen from widgets.
- Add AppBar actions.
- Show an empty state conditionally.
Sources: docs.flutter.dev/ui/widgets
Theory
A screen is a Scaffold with an AppBar and a body. AppBar actions place buttons on the right. Conditional UI is just a Dart expression: show the list when there are tasks, an empty-state widget otherwise.
Demonstration
Scaffold(
appBar: AppBar(
title: const Text('DayList'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => print('open add screen'),
),
],
),
body: tasks.isEmpty
? const Center(child: Text('No tasks yet'))
: Column(children: tasks.map((task) => TaskTile(task: task)).toList()),
)Practice
- Add two actions to an AppBar.
- Render one widget or another based on a condition.
- Explain what Scaffold provides.
- Where does the + button for adding tasks belong?
Answer key and troubleshooting notes
Scaffold provides the page frame — app bar, body, snackbars, drawers. The + button belongs in AppBar actions, the standard place for primary screen actions.
Project step: Assemble the HomeScreen: an AppBar with a + action, the list of TaskTiles, and a centered empty state when the list is empty.
Quick questions
1. Why is the empty state important?
A blank screen looks broken. A friendly empty state tells the user what the screen is for and what to do next.
2. What is the difference between AppBar actions and a FloatingActionButton?
Actions sit in the app bar for secondary commands; the FAB floats above the content for the single most important action.
Wrap Up
A screen is just a widget tree with a Scaffold at the top.
Module 3 · Navigation and State
Named Routes and Arguments
By the end of this session, you can:
- Define a routes map.
- Navigate with pushNamed.
- Pass typed arguments.
Sources: docs.flutter.dev/cookbook/navigation/named-routes
Theory
Named routes replace constructor calls with string names declared once on MaterialApp — like URLs for your app. Arguments ride along via the arguments parameter and are read with ModalRoute.of(context).
Demonstration
MaterialApp(
routes: {
'/home': (context) => const HomeScreen(),
'/task': (context) => const TaskDetailScreen(),
},
);
Navigator.pushNamed(context, '/task', arguments: task);
// read it back:
final task = ModalRoute.of(context)!.settings.arguments as Task;Practice
- Declare routes for home and task detail.
- Navigate with pushNamed and pass a task.
- Read the arguments and display the task.
- When are named routes worth it?
Answer key and troubleshooting notes
Named routes centralize navigation — every screen name lives in one map, deep links are possible, and callers do not import the target screen's file. Worth it the moment an app has more than two screens.
Project step: Convert DayList navigation to named routes ('/home' and '/task') and pass the selected Task as arguments.
Quick questions
1. What is the risk with reading arguments?
They arrive as Object, so the cast (as Task) can fail if the wrong type is sent — keep the argument type documented or use a typed helper.
2. What does onGenerateRoute add?
It handles unknown route names and lets you build routes dynamically instead of a static map.
Wrap Up
Named routes turn navigation into URLs — easier to read and maintain.
Forms and Validation
By the end of this session, you can:
- Build a Form with TextFormField.
- Write validators.
- Save values on submit.
Sources: docs.flutter.dev/cookbook/forms/validation
Theory
A Form groups fields and runs their validators together. A TextFormField's validator returns an error string, or null when valid. On submit, formKey.currentState!.validate() checks everything, then save() copies the values out.
Demonstration
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: TextFormField(
decoration: const InputDecoration(labelText: 'Title'),
validator: (value) =>
value == null || value.trim().isEmpty ? 'Title is required' : null,
onSaved: (value) => _title = value ?? '',
),
)
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
}Practice
- Build a form with a title field.
- Write a validator that rejects empty input.
- Save the value and print it on submit.
- What does the validator's return value mean?
Answer key and troubleshooting notes
Returning a string shows it as the field's error text; returning null marks the field valid. validate() runs every validator and returns true only if all pass.
Project step: Build the AddTaskScreen: a Form with title (required) and priority fields, a submit button that only accepts valid input, and an inline error for an empty title.
Quick questions
1. When does onSaved run?
Only when you call save() after validation passes — it gives you the cleaned values at the right moment.
2. Why use a GlobalKey<FormState>?
It lets the widget holding the form reach the FormState from anywhere to validate and save it.
Wrap Up
The Form collects, validates, and saves — three jobs, one widget.
Lists and ListView
By the end of this session, you can:
- Use ListView.builder for long lists.
- Render each task with itemBuilder.
- Explain lazy building.
Sources: docs.flutter.dev/cookbook/lists/long-lists
Theory
A Column builds every child up front; ListView.builder builds only the rows visible on screen and recycles them as you scroll — a thousand tasks costs the same as ten.
Demonstration
ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return TaskTile(task: task);
},
)Practice
- Render the task list with ListView.builder.
- Add separators between rows.
- Scroll with a hundred tasks and watch memory stay flat.
- Why not just use a Column?
Answer key and troubleshooting notes
A Column constructs every child when the screen builds, so a long list gets slow and heavy. ListView.builder constructs on demand, so only visible rows exist.
Project step: Replace the hand-built task Column with a ListView.builder of TaskTiles — the home screen now scrolls with any number of tasks.
Quick questions
1. What are itemCount and itemBuilder?
itemCount tells the list how many rows exist; itemBuilder is called for each visible index and returns that row's widget.
2. What does ListView.separated add?
A separator widget between every pair of rows — dividers without manual interleaving.
Wrap Up
ListView.builder only builds what fits on screen — the long list stays cheap.
State Management: Provider
By the end of this session, you can:
- Create a ChangeNotifier store.
- Provide it at the app root.
- Watch it from widgets.
Sources: docs.flutter.dev/data-and-backend/state-mgmt/simple
Theory
setState only works for one widget. When many screens share data, lift it into a ChangeNotifier — a store that calls notifyListeners() when it changes. ChangeNotifierProvider hands it down the tree, and context.watch rebuilds any widget when it changes.
Demonstration
class TaskStore extends ChangeNotifier {
final List<Task> _tasks = [];
List<Task> get tasks => List.unmodifiable(_tasks);
void add(Task task) {
_tasks.add(task);
notifyListeners();
}
void toggle(String id) {
final index = _tasks.indexWhere((t) => t.id == id);
_tasks[index] = _tasks[index].copyWith(isCompleted: !_tasks[index].isCompleted);
notifyListeners();
}
}
// main():
ChangeNotifierProvider(
create: (_) => TaskStore(),
child: const DayListApp(),
)
// any widget:
final store = context.watch<TaskStore>();Practice
- Move the task list into a TaskStore.
- Provide it at the app root.
- Watch it from the home screen and add from the form.
- What happens if you forget notifyListeners()?
Answer key and troubleshooting notes
The store changes but no widget is told — the UI silently goes stale. notifyListeners() is the announcement that makes watch() rebuild.
Project step: Create TaskStore with add, toggle, and remove, provide it at the app root, and rewire the home list and add form through Provider — the app now shares state properly.
Quick questions
1. What is the difference between watch and read?
watch subscribes the widget to changes (rebuilds on notify); read grabs the store once without subscribing — use it inside callbacks.
2. Why put the provider above MaterialApp?
Every route in the app can then reach the same store — screens come and go, the store lives above them all.
Wrap Up
Provider holds the truth; widgets watch it and rebuild when it changes.
Module 4 · Networking and Data
HTTP and REST APIs
By the end of this session, you can:
- Add the http package.
- Fetch data with GET.
- Check status codes before parsing.
Sources: docs.flutter.dev/data-and-backend/networking
Theory
Most backends speak REST over HTTP: a URL identifies a resource, a verb describes the action (GET read, POST create, PUT/PATCH update, DELETE remove), and a status code reports the result. In Flutter the http package makes the call; everything is async.
Demonstration
import 'package:http/http.dart' as http;
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/todos'),
);
if (response.statusCode == 200) {
print(response.body); // the JSON string
} else {
print('Failed: ${response.statusCode}');
}Practice
- Add http to pubspec.yaml.
- Fetch the todos list and print the body.
- Force a 404 by hitting a bad URL and print the status.
- What does a 200 versus a 404 mean?
Answer key and troubleshooting notes
200 means success — the body is what you asked for. 404 means the resource does not exist. Other families: 4xx is the client's mistake, 5xx is the server's.
Project step: Write an ApiClient with fetchTasks() that GETs the todo list from a public test API and returns the raw JSON string.
Quick questions
1. Why is every http call async?
The network round trip takes hundreds of milliseconds; awaiting it keeps the UI responsive instead of freezing.
2. What is the difference between GET and POST?
GET reads a resource; POST creates a new one, usually sending a JSON body with the new data.
Wrap Up
Every API call is a Future — await it, check the status, then parse.
JSON Parsing and Models
By the end of this session, you can:
- Decode JSON with jsonDecode.
- Write fromJson and toJson factories.
- Map API data onto the Task model.
Sources: docs.flutter.dev/data-and-backend/serialization/json
Theory
JSON arrives as a string; jsonDecode turns it into maps and lists. The rule: convert at the edge — a fromJson factory builds a typed model, and the rest of the app never touches raw JSON again.
Demonstration
import 'dart:convert';
class RemoteTask {
final int id;
final String title;
final bool isCompleted;
RemoteTask({required this.id, required this.title, required this.isCompleted});
factory RemoteTask.fromJson(Map<String, dynamic> json) => RemoteTask(
id: json['id'] as int,
title: json['title'] as String,
isCompleted: json['completed'] as bool,
);
Map<String, dynamic> toJson() =>
{'id': id, 'title': title, 'completed': isCompleted};
}
// decode a list:
final decoded = jsonDecode(response.body) as List<dynamic>;
final tasks = decoded
.map((item) => RemoteTask.fromJson(item as Map<String, dynamic>))
.toList();Practice
- Parse one JSON object into a RemoteTask.
- Parse a list of them.
- Write toJson for the model.
- Why not pass raw maps around the app?
Answer key and troubleshooting notes
Raw maps have no compile-time checks — a typo like json['title'] failing at runtime crashes. Typed models move the failure to the parsing edge and make every other screen safe and autocompleted.
Project step: Add fromJson/toJson to a RemoteTask model and convert the API response into List<Task> your UI already understands.
Quick questions
1. Why is fromJson a factory constructor?
A factory can return a new object from data instead of just initializing fields — it is the idiomatic Dart way to build an instance from JSON.
2. What happens if a JSON field is missing?
The cast throws. Use json['x'] as String? and defaults for optional fields, or validate the schema at the edge.
Wrap Up
Convert once at the edge — the rest of the app speaks models, not JSON.
Loading, Error, and Empty States
By the end of this session, you can:
- Handle all four async UI states.
- Use FutureBuilder.
- Add a retry button.
Sources: docs.flutter.dev/cookbook/networking/fetch-data
Theory
Every screen that loads data has four moods: loading (spinner), error (message + retry), empty (nothing to show), and data. FutureBuilder watches a Future and rebuilds as it completes — a clean way to cover all four in one place.
Demonstration
FutureBuilder<List<Task>>(
future: api.fetchTasks(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return ErrorState(
message: 'Could not load tasks',
onRetry: () => setState(() {}),
);
}
final tasks = snapshot.data ?? [];
if (tasks.isEmpty) {
return const Center(child: Text('No tasks yet'));
}
return TaskList(tasks: tasks);
},
)Practice
- Render a spinner while a future is waiting.
- Show an error widget with a retry button.
- Show an empty message when the result is empty.
- What are the four states called?
Answer key and troubleshooting notes
Loading, error, empty, and data. Handling all four is what separates a finished screen from a demo — the user always sees something honest.
Project step: Add a sync screen to DayList: a spinner while fetching, a retry button on error, an empty message when the API returns nothing, and the list when it works.
Quick questions
1. What does connectionState tell you?
Whether the future is waiting, done, or (for streams) active — it lets you show the spinner before the data exists.
2. Why pass a callback into the error state?
So the error widget can trigger a retry without knowing how the data is loaded — the parent owns the fetch, the child owns the button.
Wrap Up
Every async screen has four moods: loading, error, empty, and data.
Persistence: SQLite (sqflite)
By the end of this session, you can:
- Open a database and create a table.
- Insert, query, update, and delete rows.
- Load persisted tasks at startup.
Sources: docs.flutter.dev/cookbook/persistence/sqlite
Theory
sqflite is a real SQL database on the device. The pattern: open once, define the schema in onCreate, then run CRUD with SQL. Tasks saved here survive restarts — the local source of truth.
Demonstration
final db = await openDatabase(
'daylist.db',
version: 1,
onCreate: (db, version) => db.execute(
'CREATE TABLE tasks('
'id INTEGER PRIMARY KEY, '
'title TEXT, is_completed INTEGER, priority INTEGER)',
),
);
await db.insert('tasks', {'title': 'Buy milk', 'is_completed': 0, 'priority': 1});
final rows = await db.query('tasks', where: 'is_completed = ?', whereArgs: [0]);
await db.update('tasks', {'is_completed': 1}, where: 'id = ?', whereArgs: [1]);
await db.delete('tasks', where: 'id = ?', whereArgs: [1]);Practice
- Create a database with a tasks table.
- Insert three tasks and query the unfinished ones.
- Update one task to completed and verify the change.
- Why is sqflite a better home for tasks than shared_preferences?
Answer key and troubleshooting notes
SQLite gives structure, queries, and scaling; shared_preferences is flat key-value storage. A real app's data belongs in a database, its settings in preferences.
Project step: Give TaskStore a TaskDatabase: persist every add, toggle, and remove to SQLite so tasks survive app restarts, and load them at startup.
Quick questions
1. When does onCreate run?
Only the first time the database file is created — the schema lives there, and version bumps trigger migrations instead.
2. Why use whereArgs instead of string interpolation?
Arguments are bound as values, preventing SQL injection and quoting bugs — the query plan stays correct too.
Wrap Up
SQLite is the real database on the phone — tables, queries, and transactions.
Pagination and Infinite Scroll
By the end of this session, you can:
- Fetch data one page at a time.
- Detect the scroll end with a ScrollController.
- Append pages without duplicates.
Sources: docs.flutter.dev/ui/widgets/scrolling
Theory
APIs cap responses — a page of 20, not all 10,000. Pagination fetches page by page. A ScrollController reports when the user nears the bottom, the screen fetches the next page, and the rows are appended.
Demonstration
final _controller = ScrollController()..addListener(() {
if (_controller.position.pixels >=
_controller.position.maxScrollExtent - 200) {
_loadNextPage();
}
});
Future<void> _loadNextPage() async {
if (_loading) return; // guard against duplicate fetches
_loading = true;
final page = await api.fetchTasks(page: _page);
setState(() {
tasks.addAll(page);
_page += 1;
});
_loading = false;
}Practice
- Fetch page one of the todos API.
- Trigger a fetch when the user scrolls near the bottom.
- Guard against double fetches.
- Show a small loading footer while the next page loads.
Answer key and troubleshooting notes
The listener compares scroll pixels to maxScrollExtent minus a margin. The _loading flag prevents a second fetch before the first finishes; the footer signals progress to the user.
Project step: Paginate the remote task list: load 20 at a time and fetch the next page automatically as the user scrolls to the bottom, with a loading footer.
Quick questions
1. Why paginate at all?
Transferring and rendering thousands of rows at once is slow and expensive; pages keep startup fast and memory flat.
2. What is the risk of appending pages naively?
Duplicates — the same row can arrive twice if fetches overlap. The loading guard and a stable page counter prevent it.
Wrap Up
Fetch a page, render it, and pull the next one when the user reaches the bottom.
Module 5 · Firebase and Backend
Firebase Setup and Authentication
By the end of this session, you can:
- Create a Firebase project and add the SDK.
- Sign in with email and password.
- Watch the auth state and sign out.
Sources: firebase.flutter.dev/docs/auth/usage
Theory
Firebase gives apps a hosted backend: authentication, a database, storage, and messaging. After adding the config files, FirebaseAuth handles sign-in. The authStateChanges() stream tells you whenever the user logs in or out — the app's security boundary.
Demonstration
final auth = FirebaseAuth.instance;
final user = await auth.signInWithEmailAndPassword(
email: email,
password: password,
);
await auth.signOut();
StreamBuilder<User?>(
stream: auth.authStateChanges(),
builder: (context, snapshot) {
final user = snapshot.data;
return user == null ? const SignInScreen() : const HomeScreen();
},
)Practice
- Create a Firebase project and add the config to the app.
- Sign in with a test account and print the user's UID.
- Wire authStateChanges to switch between sign-in and home.
- What is the UID used for?
Answer key and troubleshooting notes
The UID is the user's stable identity — every per-user record (tasks) stores it, so queries filter by user and data never mixes between accounts.
Project step: Add a SignInScreen to DayList with email/password fields, error messages, and a sign-out button — the app now knows who is using it.
Quick questions
1. Why listen to authStateChanges instead of checking once?
Auth changes over time — login, logout, token refresh. The stream pushes every change, so the UI always matches reality.
2. Where does the sign-out button belong?
Typically in an account or settings screen — signing out should flip the stream, and the StreamBuilder will switch back to the sign-in screen automatically.
Wrap Up
FirebaseAuth hands you a signed-in user and a stream that tells you when it changes.
Firestore CRUD
By the end of this session, you can:
- Add and read Firestore documents.
- Update and delete them.
- Store tasks per user.
Sources: firebase.flutter.dev/docs/firestore/usage
Theory
Firestore is a document database in the cloud: collections hold documents, each a JSON-like map. Tasks become documents in a tasks collection, with a userId field so every query filters to the signed-in user.
Demonstration
final firestore = FirebaseFirestore.instance;
// create
final doc = await firestore.collection('tasks').add({
'title': title,
'isCompleted': false,
'priority': 1,
'userId': user.uid,
});
// update
await firestore.collection('tasks').doc(doc.id).update({'isCompleted': true});
// delete
await firestore.collection('tasks').doc(doc.id).delete();
// read only my tasks
final snapshot = await firestore
.collection('tasks')
.where('userId', isEqualTo: user.uid)
.get();Practice
- Add a task document with a userId field.
- Update its isCompleted flag.
- Query only the signed-in user's tasks.
- Why does every document carry userId?
Answer key and troubleshooting notes
Firestore has no per-user isolation by default — the userId field is the filter that enforces it. Without it, every user would see every task.
Project step: Replace local persistence with Firestore: tasks are stored per user (userId field), and add, toggle, and delete now hit the cloud.
Quick questions
1. What is the difference between a collection and a document?
A collection is a group of documents; a document is one record with fields. Collections contain documents, and documents can contain subcollections.
2. Why use add() instead of a fixed document id?
add() generates a unique id for each record — predictable ids would collide when two users create tasks at once.
Wrap Up
Firestore is a document database in the cloud — collections of documents you can query and update live.
Streams and StreamBuilder
By the end of this session, you can:
- Explain Stream versus Future.
- Subscribe to Firestore snapshots.
- Render a live-updating list.
Sources: firebase.flutter.dev/docs/firestore/usage
Theory
A Future delivers one value; a Stream delivers many, over time. Firestore's snapshots() is a stream that emits every time the data changes. StreamBuilder rebuilds on each emission — realtime UI with no manual refresh.
Demonstration
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('tasks')
.where('userId', isEqualTo: user.uid)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) return const ErrorState();
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final tasks = snapshot.data!.docs
.map((doc) => Task.fromFirestore(doc))
.toList();
return TaskList(tasks: tasks);
},
)Practice
- Explain how a Stream differs from a Future.
- Subscribe to the user's tasks with snapshots().
- Render the docs as Task objects.
- Why does the list update without any refresh code?
Answer key and troubleshooting notes
The stream pushes a new snapshot on every Firestore change, and StreamBuilder rebuilds its child for each one — the update path is the subscription itself.
Project step: Wrap the task list in a StreamBuilder on the user's tasks query — changes made on any device appear instantly.
Quick questions
1. When is snapshot.hasData false?
Before the first event arrives — the builder shows the spinner until the stream emits, then renders the data.
2. What does QuerySnapshot.docs contain?
The documents matching the query, each with its fields and id — map them into your models at the edge.
Wrap Up
Streams push data — StreamBuilder redraws the screen the moment Firestore changes.
Firebase Storage
By the end of this session, you can:
- Upload a file to Storage.
- Get its download URL.
- Attach an image to a task.
Sources: firebase.flutter.dev/docs/storage/usage
Theory
Firebase Storage holds files — images, audio, attachments. The pattern: pick a file, upload it to a path, save the returned download URL on the task document. Storage keeps the bytes; Firestore keeps the pointer.
Demonstration
final ref = FirebaseStorage.instance
.ref('tasks/${taskId}.jpg');
await ref.putFile(file);
final url = await ref.getDownloadURL();
await FirebaseFirestore.instance
.collection('tasks')
.doc(taskId)
.update({'imageUrl': url});Practice
- Upload an image to a task-specific path.
- Fetch the download URL and store it on the task.
- Display the image in the detail screen.
- Why store the URL instead of the file itself in Firestore?
Answer key and troubleshooting notes
Firestore documents are small and structured; files are large bytes. Storing the URL keeps documents light while Storage serves the content — the right tool for each job.
Project step: Let users attach one image per task: pick it, upload it to Storage, save the download URL on the task, and display it in the detail screen.
Quick questions
1. What does a Storage ref path represent?
A location in the bucket, like a file path — organizing paths per task or per user keeps uploads tidy and securable by rules.
2. What is a download URL?
A public or signed link that fetches the file — anyone with it can load the image, so rules matter for private files.
Wrap Up
Storage holds the files, Firestore holds the metadata, and the URL connects them.
Notifications and Reminders
By the end of this session, you can:
- Explain local versus push notifications.
- Schedule a local reminder.
- Request notification permission.
Sources: firebase.flutter.dev/docs/messaging/usage
Theory
Local notifications are scheduled on the device — perfect for due-date reminders. Push notifications (FCM) arrive from a server — announcements and sync events. A task app mostly needs local reminders; FCM comes later when a backend sends messages.
Demonstration
final plugin = FlutterLocalNotificationsPlugin();
await plugin.initialize(const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
));
await plugin.zonedSchedule(
task.hashCode,
'Task due soon',
'${task.title} is due today',
tz.TZDateTime.from(task.dueDate!, tz.local),
const NotificationDetails(
android: AndroidNotificationDetails('tasks', 'Task reminders'),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
);Practice
- Initialize a notifications plugin.
- Schedule a reminder for a task's due date.
- Handle permission denial gracefully.
- When would you use FCM instead?
Answer key and troubleshooting notes
FCM delivers messages from your server to devices — chat messages, broadcasts, sync triggers. Local notifications need no server at all, which is why reminders live on-device.
Project step: Schedule a local reminder when a task has a due date — DayList now nudges you before tasks are due.
Quick questions
1. What must happen before any notification shows?
The app requests permission and the user grants it — schedule only after the permission state is known.
2. Why schedule with an id like task.hashCode?
The id is how you later cancel or update that specific notification — reusing the same id replaces the old reminder instead of stacking duplicates.
Wrap Up
Local notifications remind on schedule; FCM brings messages from the server.
Module 6 · Polish and Deployment
Animations
By the end of this session, you can:
- Use AnimatedContainer and AnimatedOpacity.
- Explain implicit versus explicit animations.
- Animate task completion.
Sources: docs.flutter.dev/ui/animations
Theory
Implicit animations animate a property when it changes — hand the widget a duration and it tweens between values on its own. Explicit animations give you an AnimationController for full control. Start implicit; most polish needs nothing more.
Demonstration
AnimatedOpacity( opacity: task.isCompleted ? 0.4 : 1.0, duration: const Duration(milliseconds: 250), child: TaskTile(task: task), ) AnimatedContainer( duration: const Duration(milliseconds: 200), color: selected ? Colors.orange.shade50 : Colors.transparent, child: ... )
Practice
- Fade a completed task to 40% opacity.
- Animate a container's color on selection.
- Explain the difference between implicit and explicit animations.
- Why does every animation need a duration?
Answer key and troubleshooting notes
The duration defines the tween window — without it the change would be instant. Implicit animations manage the controller for you; explicit ones hand you the controller for sequencing and physics.
Project step: Animate DayList: tasks fade and shrink slightly when completed, and the empty state fades in with AnimatedOpacity.
Quick questions
1. When does AnimatedOpacity animate?
When its opacity value changes between builds — Flutter tweens from the old value to the new one over the duration.
2. What would you use an explicit animation for?
Sequences, repeats, staggered multi-step motion, or anything needing manual control of time — the implicit widgets cover single-property tweens.
Wrap Up
Implicit animations animate when a property changes — give it a duration and Flutter tweens it.
Responsive Design and SafeArea
By the end of this session, you can:
- Use MediaQuery and LayoutBuilder.
- Wrap screens in SafeArea.
- Build a two-pane tablet layout.
Sources: docs.flutter.dev/ui/layout/adaptive
Theory
LayoutBuilder tells you how much space a widget has; MediaQuery describes the screen itself. SafeArea pads content away from notches and system bars. Combine them: one widget on a phone, a side-by-side list-and-detail on a wide screen.
Demonstration
SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final wide = constraints.maxWidth > 600;
return wide
? Row(children: [
SizedBox(width: 320, child: TaskList()),
const Expanded(child: TaskDetailPlaceholder()),
])
: const TaskList();
},
),
)Practice
- Wrap the home screen in SafeArea.
- Switch layouts with LayoutBuilder at 600px.
- Read the screen size with MediaQuery.
- What is the difference between MediaQuery and LayoutBuilder?
Answer key and troubleshooting notes
MediaQuery describes the whole display (size, orientation, text scale); LayoutBuilder describes the space available to one widget — use it for layout decisions that must react to their container.
Project step: Make DayList responsive: SafeArea everywhere, and a two-pane layout (list + detail side by side) on screens wider than 600px.
Quick questions
1. What does SafeArea protect against?
System intrusions — notches, punch-holes, and system navigation bars — by insetting content away from them.
2. Why 600px as the breakpoint?
It is a common tablet threshold in Material guidance — narrow screens stack, wide screens show master-detail side by side.
Wrap Up
LayoutBuilder asks how much room you have; SafeArea keeps you off the notch.
Testing
By the end of this session, you can:
- Write a unit test for TaskStore.
- Write a widget test for TaskTile.
- Run flutter test.
Sources: docs.flutter.dev/testing
Theory
Unit tests check pure logic — does add() really add? Widget tests pump a widget in a fake environment and assert on what it renders. Both run with flutter test in seconds and lock behavior in place.
Demonstration
// unit test
import 'package:flutter_test/flutter_test.dart';
test('TaskStore toggles a task', () {
final store = TaskStore();
final task = Task(id: '1', title: 'Buy milk');
store.add(task);
store.toggle('1');
expect(store.tasks.first.isCompleted, isTrue);
});
// widget test
testWidgets('TaskTile renders the title', (tester) async {
final task = Task(id: '1', title: 'Buy milk');
await tester.pumpWidget(
MaterialApp(home: Scaffold(body: TaskTile(task: task))),
);
expect(find.text('Buy milk'), findsOneWidget);
});Practice
- Write a unit test for TaskStore.add and toggle.
- Write a widget test that expects a task title.
- Run flutter test and read the results.
- What is the difference between the two test kinds?
Answer key and troubleshooting notes
Unit tests exercise logic without UI; widget tests pump a widget tree and verify rendering and interaction. Unit tests are fast and many; widget tests are fewer and target key screens.
Project step: Write tests for DayList: a unit test for TaskStore add/toggle and a widget test that pumps TaskTile and expects the task title.
Quick questions
1. What does pumpWidget do?
It builds the widget in a test environment and renders one frame, ready for finders and expectations.
2. Why test a small widget like TaskTile?
Tiles are repeated everywhere — a broken tile breaks every screen, so the cheap test pays off broadly.
Wrap Up
Tests lock behavior in place — the task you fix today cannot quietly break tomorrow.
Build and Release
By the end of this session, you can:
- Build a release APK and appbundle.
- Set the app icon and splash.
- Manage the version in pubspec.
Sources: docs.flutter.dev/deployment/android
Theory
A release build strips debug tooling, shrinks the code, and signs the artifact. Android ships an appbundle for the Play Store (the store splits it per device); a plain APK is useful for direct testing. The app icon, splash, and version all live in project config.
Demonstration
flutter build apk --release flutter build appbundle flutter build ios --release # pubspec.yaml version: 1.0.0+1 # 1.0.0 = version shown, 1 = build number # icon and splash: flutter_launcher_icons + flutter_native_splash
Practice
- Set a custom app icon and splash.
- Bump the version in pubspec.yaml.
- Build a release APK and install it on a device.
- What is the difference between an APK and an appbundle?
Answer key and troubleshooting notes
The APK is a complete installable for one device configuration; the appbundle is a publishing format the Play Store splits into optimized APKs per device — smaller downloads, one upload.
Project step: Give DayList its icon and splash, bump the version in pubspec.yaml, and produce a release APK.
Quick questions
1. What does the +1 in the version mean?
The build number — every store submission must increase it, while the version string is what users see.
2. Why are debug and release builds different sizes?
Release strips debugging symbols and the VM's JIT support and applies tree shaking, so the same app ships far smaller and faster.
Wrap Up
A release build strips debugging and shrinks the app — the same code, half the size.
Publishing to Stores
By the end of this session, you can:
- Prepare a store listing.
- Gather screenshots and a privacy policy.
- Complete the submission checklist.
Sources: docs.flutter.dev/deployment
Theory
The store is a product page, not a technical step: name, short and long descriptions, screenshots, content rating, and a privacy policy. Both Google Play and the App Store review the listing and the app — the checklist is the same discipline as the rest of the course.
Demonstration
Store submission checklist: 1. Release build signed (appbundle / archive) 2. App name + short description (80 chars) 3. Long description — what DayList does 4. Screenshots per required device size 5. Feature graphic / icon at required resolutions 6. Content rating questionnaire 7. Privacy policy URL 8. Support contact + category 9. Version name matches pubspec
Practice
- Write the 80-character short description for DayList.
- Capture screenshots of the main screens.
- Draft the privacy policy lines (what data does the app store?).
- What belongs in the store listing besides the binary?
Answer key and troubleshooting notes
Everything the user sees before installing: description, screenshots, rating, and policy. The binary proves the app works; the listing decides whether anyone installs it.
Project step: Prepare DayList's store listing: name, short description, screenshots, and a privacy policy page — the release checklist from the previous session.
Quick questions
1. Why do stores require a privacy policy?
The app handles user data (Firebase auth, cloud tasks), and store policies require declaring how it is collected and used before distribution.
2. Why must the version increase for every update?
Stores reject uploads with a lower or equal version — the number is the release ledger.
Wrap Up
The store is a product page — screenshots, description, and privacy sell the app.
Module 7 · Capstone
Capstone Brief
By the end of this session, you can:
- Define the definition of done for DayList.
- Map every feature back to its module.
- Plan the integration order.
Sources: docs.flutter.dev
Theory
The capstone is not new features — it is assembling everything the modules built into one finished app. Write the definition of done first: the features that must work and the evidence that proves each one.
Demonstration
DayList — definition of done 1. Sign in / sign out works, auth state gates the UI 2. Add, toggle, and delete tasks, synced per user 3. Realtime updates via Firestore stream 4. Task images attach and display 5. Due-date reminders fire 6. Offline list falls back to SQLite cache 7. Animations, SafeArea, two-pane tablet layout 8. Unit + widget tests green (flutter test) 9. flutter analyze reports no issues 10. Release APK installed and manually tested
Practice
- Write your own definition of done for the app.
- For each item, name the module that built it.
- List the evidence command or test for each item.
- Why define done before building?
Answer key and troubleshooting notes
A checklist turns 'is it finished?' into ten small yes/no questions — progress becomes measurable, and scope creep is visible the moment it happens.
Project step: Write DayList's definition of done: the ten features that must work, and the evidence command or test for each.
Quick questions
1. What is the difference between MVP and the full feature set?
The MVP is the smallest app that is useful (auth + CRUD + sync); the rest is polish. Ship the MVP path first, then layer the extras.
2. Why pair every feature with evidence?
A feature without a way to verify it is a claim. The evidence — a test, a command, a manual step — makes done objective.
Wrap Up
Define done before building — a checklist turns a big app into small wins.
Build the Capstone
By the end of this session, you can:
- Assemble the modules into one app.
- Pass analyze and tests.
- Run the manual QA pass.
Sources: docs.flutter.dev/testing
Theory
Integration order matters: wire state and navigation first, then data, then cloud, then polish. After every integration run the full verification loop — flutter analyze, flutter test, and a manual pass of the checklist.
Demonstration
# integration order 1. Theme + routes + TaskStore wiring 2. SQLite persistence behind TaskStore 3. Firestore sync + StreamBuilder list 4. Auth gate with authStateChanges 5. Storage attachments + detail screen 6. Reminders + notifications permission 7. Animations + responsive two-pane 8. Tests for store and tiles # verify after each step flutter analyze flutter test flutter run # manual pass
Practice
- Follow the integration order step by step.
- After each step, run flutter analyze and flutter test.
- Complete a manual pass of your definition of done.
- What does flutter analyze catch that tests do not?
Answer key and troubleshooting notes
analyze catches static issues — unused imports, type errors, lints — before the app even runs. Tests catch behavioral regressions. Together they cover code quality and behavior.
Project step: Complete any missing DayList feature and pass the full loop: flutter analyze clean, all tests green, and a manual run of the done checklist.
Quick questions
1. Why integrate in that order?
Each layer depends on the last — state before data, data before cloud, polish last — so every step lands on a working app instead of a broken pile.
2. What belongs in the manual pass?
The things tests cannot feel: scrolling smoothness, notifications firing, sign-in on a real device, and the store-facing screens.
Wrap Up
Assemble what you built module by module — the capstone is integration, not new features.
Capstone Review and Rubric
By the end of this session, you can:
- Score the app against a rubric.
- Break and fix three features with evidence.
- Write the release note.
Sources: docs.flutter.dev
Theory
The final review proves understanding: score the app honestly, then break it on purpose — remove a route, flip a field name, kill a listener — and fix each break with the evidence method. If you can break and repair it, you own it.
Demonstration
| Area | Excellent | Needs work |
|---|---|---|
| State | Single store, widgets watch, no stale UI. | setState scattered, screens out of sync. |
| Data | Typed models, edge parsing, offline cache. | Raw maps, crashes on missing fields. |
| Cloud | Per-user sync, realtime updates, clean sign-out. | Shared data, manual refresh. |
| Robustness | All four async states, retries, error text. | Spinners forever, silent failures. |
| Polish | Animations, responsive, SafeArea, tests. | Single-layout, untested, janky. |
Practice
- Score DayList on each rubric row.
- Break a route, a field name, and a listener — fix each with evidence.
- Write the one-paragraph release note.
- What does the break/fix phase teach that building does not?
Answer key and troubleshooting notes
Building proves you can configure; breaking and fixing proves you understand cause and effect. A developer who can repair the app can own it in production.
Project step: Run DayList's final review: score the rubric honestly, break three features and fix them with evidence, and write the one-paragraph release note.
Quick questions
1. Why score yourself honestly?
An inflated score hides the gaps that will cost you in an interview or a production outage — the rubric is a mirror, not a trophy.
2. What should the release note contain?
What the app does, who it is for, and the one-line story of how it was built — the summary a reviewer or employer reads first.
Wrap Up
Ship it, break it, fix it — then show the work.
Study Plan
Follow this sequence, and do not skip the console apps — they are where the logic gets solid.
- Dart first — work through Module 1 in DartPad. Type every example and rebuild each practice app without looking.
- Install Flutter — follow the official setup guide and run the default counter app once before Module 2.
- Widgets daily — one small screen per day: a card, a list, a form. Small screens beat big ones.
- State before data — master setState and Provider patterns before touching APIs.
- One real API — pick a public JSON API and build a screen around it; that exercise covers half of Module 4.
- Capstone last — only start the capstone when Modules 1–6 are done; it assumes every pattern.
Rule of thumb: if you can rebuild yesterday's practice app from an empty file, you understood it. If not, that is today's task.
Learning Resources
The complete study stack for this track — official documentation, the browser playground, and the package ecosystem.
Official documentation
- Flutter documentation — Guides, API reference, and cookbooks — the primary source.
- Dart language tour — The complete language walkthrough used by every session in Module 1.
- DartPad — Run Dart in the browser — no install needed for the first modules.
Packages and community
- pub.dev — The package registry — HTTP, state, Firebase, and 40,000 more.
- Flutter YouTube — Official videos, widget of the week, and conference talks.