Flutter · Dart essentials

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

Session 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

  1. Declare your name, age, and city using var, final, and const.
  2. Create a nullable int and print it with a ?? fallback.
  3. What is the difference between final and const?
  4. 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.

Session 2 · Dart Essentials

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

  1. Write a function that greets by name with a customizable greeting.
  2. Convert a two-line function into an arrow function.
  3. Print FizzBuzz for 1–15 (Fizz for 3, Buzz for 5, FizzBuzz for both).
  4. 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.

Session 3 · Dart Essentials

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

  1. Build a list of five cities and print its length.
  2. Filter the list to cities starting with a chosen letter.
  3. Store names and ages in a map, then list everyone over 18.
  4. 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.

Session 4 · Dart Essentials

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

  1. Define a Book class with title and pages.
  2. Add a method that describes the book.
  3. Instantiate three books and print each description.
  4. 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().

Session 5 · Dart Essentials

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');
  }
}
Call fetchName()returns a Future immediately
→
awaitmain pauses, UI stays alive
→
Value arrivesexecution continues
→
Error?catch handles it

await never freezes the app — only this function waits.

Practice

  1. Write an async function that returns a number after a delay.
  2. Await it in main and print the result.
  3. Add a try/catch around a function that throws.
  4. 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.

Session 6 · Dart Essentials

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

  1. Build a to-do list app: add, remove, and print tasks.
  2. Build a temperature converter: Celsius to Fahrenheit.
  3. Build a quiz app: questions, answers, and a score.
  4. 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

Session 7 · 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

flutter doctorSDK, editor, device checks
→
flutter createthe project skeleton
→
flutter runapp on the device
→
hot reloadr / R in the terminal

Save a file, press r, and the change appears immediately.

Practice

  1. Run flutter doctor and fix anything it flags.
  2. Create a project and run the default counter app.
  3. Change the counter text and hot reload without restarting.
  4. 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.

Session 8 · Flutter Foundations

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

  1. Explain what a widget tree is.
  2. Write a StatelessWidget that shows your name centered on screen.
  3. Add an AppBar with a different title.
  4. 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.

Session 9 · Flutter Foundations

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

  1. Build a row with an icon, a text, and a trailing icon.
  2. Center three icons horizontally using mainAxisAlignment.
  3. What does Expanded do?
  4. 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.

Session 10 · Flutter Foundations

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

  1. Convert a stateless tile into a StatefulWidget.
  2. Toggle a boolean with setState and show the value as text.
  3. What happens if you change the variable without calling setState?
  4. 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'.

Session 11 · Flutter Foundations

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

  1. Style a title in bold with strikethrough.
  2. Add a delete IconButton that prints on tap.
  3. Load a network image with an errorBuilder fallback.
  4. 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.

Session 12 · Flutter Foundations

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

  1. Seed a Material 3 theme from one color.
  2. Add a dark theme and system theme mode.
  3. Read the primary color with Theme.of(context) and use it on an icon.
  4. 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.

Session 13 · Flutter Foundations

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

  1. Add two actions to an AppBar.
  2. Render one widget or another based on a condition.
  3. Explain what Scaffold provides.
  4. 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

Session 14 · Navigation and State

Routes and the Navigator

By the end of this session, you can:

  • Push and pop routes.
  • Pass a task into a new screen.
  • Return to the previous screen.

Sources: docs.flutter.dev/cookbook/navigation/navigation-basics

Theory

Navigator manages a stack of screens. Navigator.push puts a new route on top; Navigator.pop removes it. Data travels into the new screen through its constructor — plain Dart, no magic.

Demonstration

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => TaskDetailScreen(task: task),
  ),
);

// inside TaskDetailScreen:
Navigator.pop(context);
HomeScreenstack bottom
→
pushTaskDetailScreen on top
→
popback to HomeScreen

Practice

  1. Push a detail screen when a task is tapped.
  2. Pass the tapped task into the screen.
  3. Pop back and confirm the home screen still shows the list.
  4. Where does context come from in Navigator.push?
Answer key and troubleshooting notes

context is the BuildContext of the widget calling push — it locates the nearest Navigator in the tree. The task travels as a constructor argument, so the detail screen receives a fully typed Task.

Project step: Tap a task to open a TaskDetailScreen that receives the Task in its constructor and shows title, priority, and due date, with a back button.

Quick questions

1. What does the route stack look like after push?

The new screen sits on top of the old one; the old screen stays alive underneath and reappears when the new one pops.

2. Why pass data through the constructor?

It is typed and explicit — the compiler checks it, and the screen declares exactly what it needs.

Wrap Up

Navigator is a stack — push to go forward, pop to come back.

Session 15 · 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

  1. Declare routes for home and task detail.
  2. Navigate with pushNamed and pass a task.
  3. Read the arguments and display the task.
  4. 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.

Session 16 · Navigation and State

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

  1. Build a form with a title field.
  2. Write a validator that rejects empty input.
  3. Save the value and print it on submit.
  4. 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.

Session 17 · Navigation and State

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

  1. Render the task list with ListView.builder.
  2. Add separators between rows.
  3. Scroll with a hundred tasks and watch memory stay flat.
  4. 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.

Session 18 · Navigation and State

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

  1. Move the task list into a TaskStore.
  2. Provide it at the app root.
  3. Watch it from the home screen and add from the form.
  4. 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.

Session 19 · Navigation and State

Local Storage: shared_preferences

By the end of this session, you can:

  • Add the shared_preferences package.
  • Save and read simple settings.
  • Apply a default when nothing is saved.

Sources: docs.flutter.dev/cookbook/persistence/key-value

Theory

shared_preferences persists small key-value settings — strings, bools, ints — across app restarts. It is a settings drawer, not a database: tasks belong in SQLite or the cloud, preferences belong here.

Demonstration

final prefs = await SharedPreferences.getInstance();
await prefs.setString('sortOrder', 'priority');

final order = prefs.getString('sortOrder') ?? 'date';
print(order); // priority

Practice

  1. Add shared_preferences to pubspec.yaml.
  2. Save a sort order and read it back.
  3. Handle the first-run case where nothing is saved.
  4. Why not store the task list in shared_preferences?
Answer key and troubleshooting notes

It is designed for tiny values, not structured collections — no queries, no relations, and loading everything into memory on every read. Tasks deserve a real store; preferences only remember choices like sort order and theme.

Project step: Persist the user's sort order (date or priority) in shared_preferences and apply it when DayList starts.

Quick questions

1. Why are all prefs operations async?

They read and write platform storage, which takes time — awaiting keeps the UI thread free.

2. What is the safe pattern for a missing key?

Use the null-aware default: getString(key) ?? fallback, so first launch behaves like the default is already saved.

Wrap Up

shared_preferences is the drawer for small settings — not your task database.

Module 4 · Networking and Data

Session 20 · 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

  1. Add http to pubspec.yaml.
  2. Fetch the todos list and print the body.
  3. Force a 404 by hitting a bad URL and print the status.
  4. 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.

Session 21 · Networking and Data

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

  1. Parse one JSON object into a RemoteTask.
  2. Parse a list of them.
  3. Write toJson for the model.
  4. 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.

Session 22 · Networking and Data

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

  1. Render a spinner while a future is waiting.
  2. Show an error widget with a retry button.
  3. Show an empty message when the result is empty.
  4. 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.

Session 23 · Networking 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

  1. Create a database with a tasks table.
  2. Insert three tasks and query the unfinished ones.
  3. Update one task to completed and verify the change.
  4. 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.

Session 24 · Networking and Data

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

  1. Fetch page one of the todos API.
  2. Trigger a fetch when the user scrolls near the bottom.
  3. Guard against double fetches.
  4. 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

Session 25 · 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

  1. Create a Firebase project and add the config to the app.
  2. Sign in with a test account and print the user's UID.
  3. Wire authStateChanges to switch between sign-in and home.
  4. 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.

Session 26 · Firebase and Backend

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

  1. Add a task document with a userId field.
  2. Update its isCompleted flag.
  3. Query only the signed-in user's tasks.
  4. 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.

Session 27 · Firebase and Backend

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

  1. Explain how a Stream differs from a Future.
  2. Subscribe to the user's tasks with snapshots().
  3. Render the docs as Task objects.
  4. 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.

Session 28 · Firebase and Backend

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

  1. Upload an image to a task-specific path.
  2. Fetch the download URL and store it on the task.
  3. Display the image in the detail screen.
  4. 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.

Session 29 · Firebase and Backend

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

  1. Initialize a notifications plugin.
  2. Schedule a reminder for a task's due date.
  3. Handle permission denial gracefully.
  4. 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

Session 30 · 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

  1. Fade a completed task to 40% opacity.
  2. Animate a container's color on selection.
  3. Explain the difference between implicit and explicit animations.
  4. 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.

Session 31 · Polish and Deployment

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

  1. Wrap the home screen in SafeArea.
  2. Switch layouts with LayoutBuilder at 600px.
  3. Read the screen size with MediaQuery.
  4. 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.

Session 32 · Polish and Deployment

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

  1. Write a unit test for TaskStore.add and toggle.
  2. Write a widget test that expects a task title.
  3. Run flutter test and read the results.
  4. 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.

Session 33 · Polish and Deployment

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

  1. Set a custom app icon and splash.
  2. Bump the version in pubspec.yaml.
  3. Build a release APK and install it on a device.
  4. 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.

Session 34 · Polish and Deployment

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

  1. Write the 80-character short description for DayList.
  2. Capture screenshots of the main screens.
  3. Draft the privacy policy lines (what data does the app store?).
  4. 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

Session 35 · 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

  1. Write your own definition of done for the app.
  2. For each item, name the module that built it.
  3. List the evidence command or test for each item.
  4. 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.

Session 36 · Capstone

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

  1. Follow the integration order step by step.
  2. After each step, run flutter analyze and flutter test.
  3. Complete a manual pass of your definition of done.
  4. 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.

Session 37 · Capstone

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

AreaExcellentNeeds work
StateSingle store, widgets watch, no stale UI.setState scattered, screens out of sync.
DataTyped models, edge parsing, offline cache.Raw maps, crashes on missing fields.
CloudPer-user sync, realtime updates, clean sign-out.Shared data, manual refresh.
RobustnessAll four async states, retries, error text.Spinners forever, silent failures.
PolishAnimations, responsive, SafeArea, tests.Single-layout, untested, janky.

Practice

  1. Score DayList on each rubric row.
  2. Break a route, a field name, and a listener — fix each with evidence.
  3. Write the one-paragraph release note.
  4. 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.

Before you build

Study Plan

Follow this sequence, and do not skip the console apps — they are where the logic gets solid.

  1. Dart first — work through Module 1 in DartPad. Type every example and rebuild each practice app without looking.
  2. Install Flutter — follow the official setup guide and run the default counter app once before Module 2.
  3. Widgets daily — one small screen per day: a card, a list, a form. Small screens beat big ones.
  4. State before data — master setState and Provider patterns before touching APIs.
  5. One real API — pick a public JSON API and build a screen around it; that exercise covers half of Module 4.
  6. 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

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.