Master Google Flutter and Dart — Build Beautiful, High-Performance Cross-Platform Apps for Mobile, Web, and Desktop
Course Description
This comprehensive, project-driven course takes you from absolute beginner to professional Flutter developer. You will learn Dart from the ground up, master the Flutter framework, design responsive and adaptive user interfaces, manage state professionally, integrate APIs and databases, implement authentication and security, optimize performance, write tests, apply clean architecture, and deploy production-ready applications to Android, iOS, Web, Windows, macOS, and Linux.
Every concept is taught with clear explanations, modern syntax, complete working code examples, common mistakes, best practices, exercises, and mini-projects. By the end of the course you will be able to independently design, develop, test, secure, optimize, and ship professional cross-platform applications.
Target Audience
- Absolute beginners with little or no prior mobile development experience
- Web developers who want to expand into mobile
- Students and career switchers aiming for Flutter developer roles
- Developers who know other languages and want to master Flutter professionally
Prerequisites
- Basic computer literacy
- Familiarity with any programming language is helpful but not required
- Willingness to practice daily and complete projects
Learning Objectives
By the end of this course you will be able to:
- Write clean, modern Dart code with null safety, async/await, streams, and advanced language features
- Build complex, responsive, and adaptive UIs using Flutter widgets
- Implement professional navigation, forms, and user input
- Choose and apply the right state-management solution (setState, Provider, Riverpod, BLoC)
- Consume REST APIs, handle authentication, and manage offline data
- Integrate Firebase services
- Apply Clean Architecture and SOLID principles
- Write unit, widget, and integration tests
- Optimize performance and diagnose issues with Flutter DevTools
- Deploy apps to Google Play, App Store, and the web
- Use modern tooling, CI/CD, and AI-assisted development
Estimated Learning Time
- Full-time intensive: 10–12 weeks
- Part-time (10–15 hours/week): 4–6 months
- Total guided content + practice: approximately 180–220 hours
Recommended Development Environment
- Visual Studio Code or Android Studio / IntelliJ IDEA
- Flutter SDK (stable channel)
- Dart SDK (bundled with Flutter)
- Android Emulator and/or physical Android device
- Xcode (macOS only) for iOS development
Required Hardware / Software
- Computer running Windows 10/11, macOS 12+, or Linux (Ubuntu 20.04+ recommended)
- At least 8 GB RAM (16 GB recommended)
- 20+ GB free disk space
- Stable internet connection for package downloads and API testing
Career Opportunities After Completing the Course
- Flutter Mobile Developer
- Cross-Platform Mobile Engineer
- Junior to Mid-level Flutter Developer
- Freelance Flutter Developer
- Mobile App Architect (with continued experience)
- Full-stack roles that include Flutter frontends
Course Structure Overview
- Level 1 — Introduction to Mobile Development and Flutter
- Level 2 — Installing and Configuring Flutter
- Level 3 — Dart Programming From Scratch
- Level 4 — Your First Flutter Application
- Level 5 — Flutter Widgets
- Level 6 — UI and Responsive Design
- Level 7 — Navigation and Routing
- Level 8 — User Input and Forms
- Level 9 — State Management
- Level 10 — Working With APIs
- Level 11 — Local Storage and Databases
- Level 12 — Firebase With Flutter
- Level 13 — Authentication and Security
- Level 14 — Advanced Flutter
- Level 15 — Flutter Architecture
- Level 16 — Testing
- Level 17 — Performance Optimization
- Level 18 — Packages and Plugins
- Level 19 — Device Features
- Level 20 — Flutter Web and Desktop
- Level 21 — App Deployment
- Level 22 — CI/CD
- Level 23 — AI + Flutter Development
- Level 24 — Professional Projects
- Level 25 — Flutter Best Practices
- Level 26 — Common Flutter Mistakes
- Level 27 — Flutter Developer Roadmap
- Level 28 — Exercises and Assessments
- Level 29 — Final Professional Certification Test
LEVEL 1 — Introduction to Mobile Development and Flutter
Lesson 1.1 — What Is Mobile Application Development?
Learning Objectives
- Understand what mobile application development is
- Distinguish between native and cross-platform approaches
- Recognize the main platforms (Android and iOS)
Why It Is Important
Almost every modern business needs a mobile presence. Understanding the landscape helps you choose the right technology and set realistic expectations.
Detailed Explanation
Mobile application development is the process of creating software that runs on smartphones and tablets. The two dominant platforms are Google’s Android and Apple’s iOS.
Android vs iOS
- Android is open-source, runs on a wide variety of devices, and uses Java or Kotlin as primary languages.
- iOS is closed-source, runs only on Apple devices, and primarily uses Swift (or Objective-C).
Native vs Cross-Platform
- Native development means writing separate codebases for Android and iOS. This usually gives the best performance and deepest platform integration but doubles development and maintenance cost.
- Cross-platform frameworks allow you to write most of the code once and run it on multiple platforms.
Flutter’s Position
Flutter is a cross-platform UI toolkit created by Google. It uses the Dart language and compiles to native machine code, delivering near-native performance while allowing a single codebase for Android, iOS, web, Windows, macOS, and Linux.
When Flutter Is the Best Choice
- You need to ship on multiple platforms quickly
- You want a single team and single codebase
- You value highly customizable, beautiful UIs
- Performance is important but you do not need deep platform-specific features that would force heavy platform channels
Common Mistakes
- Assuming Flutter is only for mobile (it also targets web and desktop)
- Choosing Flutter when the app requires extensive use of very new or obscure platform APIs without evaluating plugin availability
Best Practices
- Always evaluate the specific requirements of the project before choosing a technology
- Prototype critical features early
Practice Exercise
Write a short paragraph comparing the advantages of native development versus Flutter for a simple e-commerce application.
Mini Project
Research three real apps built with Flutter and note which platforms they support.
Lesson 1.2 — Flutter Architecture and Core Concepts
Learning Objectives
- Understand Flutter’s layered architecture
- Learn the roles of the Flutter Engine, Framework, and Embedder
- Grasp the concept of widgets and the widget tree
Detailed Explanation
Flutter has a layered architecture:
- Framework (Dart) — The high-level UI library written in Dart. This is what you interact with daily (Material, Cupertino, widgets, gestures, animation, etc.).
- Engine (C++) — Responsible for low-level rendering (Skia or Impeller), text layout, file I/O, and platform channels.
- Embedder — Platform-specific code that hosts the Flutter engine on Android, iOS, Windows, etc.
Widgets
In Flutter everything is a widget. A widget is an immutable description of part of the user interface. Widgets form a tree. When the state of the application changes, Flutter rebuilds the affected parts of the widget tree efficiently.
Hot Reload and Hot Restart
- Hot Reload injects updated code into the running Dart Virtual Machine while preserving the current application state. It is extremely fast and ideal for UI iteration.
- Hot Restart restarts the Dart VM and loses application state. Use it when you change main() or global variables.
Rendering System
Flutter draws every pixel itself using its own rendering engine. It does not rely on OEM widgets. This is why Flutter UIs look consistent across platforms and why customization is powerful.
Advantages of Flutter
- Single codebase for six platforms
- Excellent performance (AOT compilation)
- Highly expressive and flexible UI
- Fast development cycle (Hot Reload)
- Growing ecosystem and strong Google backing
Limitations
- App size can be larger than pure native apps
- Some platform-specific features still require platform channels or plugins
- Web support, while production-ready, still has differences from mobile
Flutter Compared With Other Technologies
Practice Exercise
Explain in your own words why Flutter’s “everything is a widget” philosophy simplifies UI development.
LEVEL 2 — Installing and Configuring Flutter
Lesson 2.1 — Installing Flutter on Windows, macOS, and Linux
Learning Objectives
- Install the Flutter SDK correctly on your operating system
- Configure the PATH environment variable
- Verify the installation with Flutter Doctor
Detailed Installation Steps (Conceptual)
Windows
- Download the latest stable Flutter SDK zip.
- Extract it to a location without spaces or special characters (example: C:\src\flutter).
- Add the flutter\bin folder to your system PATH.
- Open a new command prompt and run flutter doctor.
macOS
- Download the SDK.
- Extract it (commonly to ~/development/flutter).
- Add the bin directory to your shell profile (.zshrc or .bash_profile).
- Run flutter doctor.
Linux
- Download and extract the SDK.
- Add the bin path to ~/.bashrc or ~/.zshrc.
- Install required system dependencies listed by flutter doctor.
Essential Commands and Their Meaning
flutter doctor
Checks your environment and reports missing dependencies (Android toolchain, Xcode, VS Code extensions, etc.).
flutter --version
Shows the currently installed Flutter and Dart versions.
flutter create my_app
Creates a new Flutter project with the standard folder structure.
flutter run
Builds and runs the application on a connected device or emulator.
Android Studio / VS Code Setup
- Install Android Studio and the Android SDK.
- Create at least one virtual device (emulator).
- In VS Code, install the official Flutter and Dart extensions.
- On macOS, install Xcode from the App Store and run sudo xcode-select --switch /Applications/Xcode.app.
Common Installation Problems and Solutions
- “cmdline-tools component is missing” → Install Android SDK Command-line Tools via Android Studio SDK Manager.
- “Android licenses not accepted” → Run flutter doctor --android-licenses and accept all.
- PATH not found → Restart the terminal or computer after modifying PATH.
- Xcode issues on macOS → Open Xcode once and accept the license agreement.
Practice Exercise
Run flutter doctor -v and interpret every line of the output.
LEVEL 3 — Dart Programming From Scratch
Lesson 3.1 — Variables, Types, and Null Safety
Learning Objectives
- Declare variables correctly
- Understand Dart’s type system
- Use null safety confidently
Explanation
Dart is a strongly typed language with type inference. Since Dart 2.12, null safety is enabled by default.
Syntax
// Explicit type
String name = 'Flutter';
// Type inference
var age = 25;
final pi = 3.14159; // runtime constant
const gravity = 9.81; // compile-time constant
// Nullable types
String? nickname; // can be null
String nonNullable = 'Hello'; // cannot be null
Null-aware Operators
String? maybeName;
print(maybeName ?? 'Guest'); // if null, use 'Guest'
print(maybeName?.length); // safe access
maybeName ??= 'Anonymous'; // assign only if null
Common Mistakes
- Forgetting the ? when a variable can be null
- Using ! (null assertion) without checking, which can throw at runtime
Best Practices
- Prefer non-nullable types whenever possible
- Use late only when you can guarantee initialization before first read
- Prefer final over var when the value will not change
Practice Exercise
Create a small program that declares a nullable string, safely prints its length, and provides a default value.
Lesson 3.2 — Collections, Functions, and Control Flow
Key Topics Covered Thoroughly
- Lists, Sets, Maps
- for, while, do-while, for-in, forEach
- if / else, switch expressions (Dart 3)
- Functions with positional, named, and optional parameters
- Arrow functions
- Higher-order functions
Complete Example — Filtering a List
void main() {
final numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
final evenNumbers = numbers.where((n) => n.isEven).toList();
final doubled = numbers.map((n) => n * 2).toList();
print(evenNumbers); // [2, 4, 6, 8, 10]
print(doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
}
Classes, Inheritance, Mixins, and Generics are taught with progressive examples leading to real-world models (User, Product, Order, etc.).
Async Programming
Futures, async/await, Streams, and error handling with try/catch are covered with practical networking-style examples before moving to Flutter.
LEVEL 4 — Your First Flutter Application
Lesson 4.1 — Project Structure and Hello Flutter
Learning Objectives
- Understand the standard Flutter project structure
- Create and run a basic application
- Learn the roles of main(), runApp(), MaterialApp, and Scaffold
Project Structure (Key Folders)
- lib/ — All Dart source code lives here. main.dart is the entry point.
- android/, ios/, web/, windows/, macos/, linux/ — Platform-specific code.
- pubspec.yaml — Dependencies and assets.
- test/ — Unit and widget tests.
Complete Hello Flutter Application
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hello Flutter',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Hello Flutter'),
centerTitle: true,
),
body: const Center(
child: Text(
'Welcome to Flutter Development',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w500),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
Code Explanation
- runApp() inflates the given widget and attaches it to the screen.
- MaterialApp provides Material Design theming, navigation, and localization.
- Scaffold implements the basic Material visual layout structure (AppBar, body, FAB, drawer, etc.).
Progressive Enhancement
Students next add a Drawer, BottomNavigationBar, and SafeArea, turning the simple app into a professional-looking starter template.
LEVEL 5 — FLUTTER WIDGETS
Level Overview
In previous levels you learned what Flutter is, how to install it, how Dart works, and how to create a basic application with MaterialApp and Scaffold. Now you will master the building blocks of every Flutter interface: widgets.
Everything you see on the screen is a widget. Understanding widgets deeply — how they are configured, how they receive constraints, how they compose, and how Flutter turns them into pixels — is the foundation of professional Flutter development.
Learning Objectives
- Use basic widgets correctly and style them professionally
- Compose complex layouts with Row, Column, Stack, Expanded, Flexible, and related widgets
- Build efficient scrolling lists and grids with ListView, GridView, and Slivers
- Understand the relationship between Widget, Element, and RenderObject trees
- Diagnose common layout and performance problems related to widgets
- Create reusable, well-structured UI components
Prerequisites
- Comfortable with Dart basics (variables, functions, classes, null safety)
- Able to create and run a simple Flutter application
- Understanding of StatelessWidget, build method, and MaterialApp / Scaffold
Module 1 — Basic Widgets
Lesson 5.1 — Text Widget
What Is It?
Text is the fundamental widget that displays a string of text on the screen. It is immutable and highly configurable through style properties.
Why Is It Important?
Almost every screen contains text. Mastering Text and TextStyle is essential for readable, accessible, and brand-consistent interfaces.
How It Works
The Text widget receives a string and an optional TextStyle. During the layout phase Flutter measures the text using the current font metrics and the constraints given by its parent. The text is then painted onto the canvas.
Syntax and Important Properties
Text(
String data, {
Key? key,
TextStyle? style,
TextAlign? textAlign,
TextDirection? textDirection,
bool softWrap = true,
TextOverflow? overflow,
int? maxLines,
...
})
Key properties:
- style → controls font size, weight, color, letter spacing, height, etc.
- textAlign → left, center, right, justify
- overflow → clip, fade, ellipsis, visible
- maxLines → limits the number of lines
Basic Example
Text('Hello Flutter')
Complete Code Example
import 'package:flutter/material.dart';
class TextDemo extends StatelessWidget {
const TextDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Text Widget')),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Primary Heading',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 12),
Text(
'This is a longer paragraph that demonstrates soft wrapping, '
'letter spacing, and line height control for better readability.',
style: TextStyle(
fontSize: 16,
height: 1.5,
letterSpacing: 0.3,
color: Colors.grey[800],
),
),
const SizedBox(height: 24),
const Text(
'This text will be truncated with an ellipsis if it is too long for the available space.',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 16),
),
],
),
),
);
}
}
Code Explanation
- The first Text uses a large bold style for a heading.
- The second Text demonstrates paragraph styling with height (line height) and letterSpacing.
- The third Text shows maxLines and overflow: TextOverflow.ellipsis — a common pattern for list titles.
Real-World Example
In an e-commerce product card the product name uses maxLines: 2 with ellipsis, the price uses a bold accent color, and the description uses a lighter grey with controlled line height.
Common Mistakes
- Forgetting const when the text never changes (causes unnecessary rebuilds).
- Using very small font sizes without considering accessibility.
- Relying only on color to convey meaning (accessibility problem).
Best Practices
- Prefer Theme.of(context).textTheme over hard-coded styles.
- Always provide a sensible maxLines + overflow for dynamic content.
- Use const Text(...) whenever possible.
Practice Exercises
- Create three text styles: heading, body, and caption.
- Display a long sentence that truncates after two lines.
- Center a title both horizontally and vertically using only Text properties and a parent.
Challenge
Build a “Quote of the Day” card that shows a quote in italic, the author in bold, and handles long quotes gracefully.
Mini Project
Create a simple profile header that shows the user’s full name (large), username (grey), and a short bio that truncates after three lines.
Lesson 5.2 — Icon Widget
What Is It?
Icon displays a graphical symbol from a font (Material Icons, Cupertino Icons, or custom icon fonts).
Why Is It Important?
Icons communicate actions and status faster than text and are essential for modern mobile interfaces.
How It Works
Icons are rendered from a font file. Flutter maps an IconData (code point) to a glyph and paints it at the requested size and color.
Syntax
Icon(
IconData icon, {
Key? key,
double? size,
Color? color,
String? semanticLabel,
TextDirection? textDirection,
})
Complete Code Example
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
Icon(Icons.home, size: 32, color: Colors.blue),
Icon(Icons.favorite, size: 32, color: Colors.red),
Icon(Icons.settings, size: 32, color: Colors.grey),
Icon(Icons.notifications_active, size: 32, color: Colors.orange),
],
)
Real-World Example
Bottom navigation bars, action buttons in AppBars, and status indicators (online/offline, battery, Wi-Fi) all rely heavily on the Icon widget.
Common Mistakes
- Using icons without a semanticLabel (hurts accessibility).
- Hard-coding colors instead of using IconTheme or ColorScheme.
Best Practices
- Always supply semanticLabel for important icons.
- Prefer Icons from Material or create a consistent custom icon set.
- Size icons relative to surrounding text (usually 20–28 logical pixels for body actions).
Practice Exercises
- Create a row of four action icons with different colors.
- Add a tooltip to each icon.
- Build a simple status row (Wi-Fi, battery, signal) using icons.
Lesson 5.3 — Image Widget
What Is It?
Image displays an image from assets, network, memory, or file.
Why Is It Important?
Images are central to modern apps — product photos, avatars, banners, illustrations.
How It Works
Flutter loads the image data, decodes it, and paints it according to the BoxFit and alignment rules. Network images are cached by default.
Important Constructors
- Image.asset
- Image.network
- Image.file
- Image.memory
Complete Code Example
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'https://picsum.photos/400/300',
width: double.infinity,
height: 200,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) {
return const Center(child: Icon(Icons.broken_image, size: 48));
},
),
)
Code Explanation
- ClipRRect rounds the corners.
- BoxFit.cover fills the space while preserving aspect ratio.
- loadingBuilder and errorBuilder provide professional loading and error states.
Common Mistakes
- Not handling loading and error states.
- Using very large images without resizing or caching strategy.
- Forgetting to declare assets in pubspec.yaml.
Best Practices
- Always provide loading and error builders for network images.
- Use cached_network_image package for production network images.
- Prefer WebP or properly sized assets.
Practice Exercises
- Display a local asset image.
- Display a network image with loading indicator and error fallback.
- Create a circular avatar using ClipOval + Image.
Lesson 5.4 — Container Widget
What Is It?
Container is a convenience widget that combines painting, positioning, and sizing. It can apply color, decoration, padding, margin, constraints, and transforms.
Why Is It Important?
It is one of the most frequently used widgets for creating cards, buttons, badges, and custom shapes.
How It Works
A Container first applies padding, then constraints, then decoration/color, and finally margin. If no child is provided it tries to be as large as possible (subject to constraints).
Important Properties
- width, height
- padding, margin
- color or decoration
- alignment
- constraints
- transform
Complete Code Example — Product Card Shell
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Wireless Headphones', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('\$129.99', style: TextStyle(fontSize: 16, color: Colors.green)),
],
),
)
Common Mistakes
- Using both color and decoration (they conflict).
- Putting large margins inside tight constraints, causing overflow.
- Overusing Container when a simpler widget (SizedBox, Padding, ColoredBox) would suffice.
Best Practices
- Prefer DecoratedBox, Padding, SizedBox, or ColoredBox when you need only one feature.
- Use BoxDecoration for borders, gradients, and shadows.
- Keep decoration logic consistent across the app (create reusable card styles).
Lesson 5.5 — SizedBox, Padding, Center
SizedBox
Forces a child to have a specific width and/or height, or creates empty space.
const SizedBox(height: 16)
const SizedBox(width: 8)
SizedBox(width: 120, height: 40, child: ElevatedButton(...))
Padding
Adds empty space around a child.
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Text('Content'),
)
Center
Centers its child within itself (both horizontally and vertically).
const Center(child: Text('Centered'))
When to Use Which
- Use SizedBox for fixed gaps or fixed-size children.
- Use Padding when you only need spacing around a widget.
- Use Center when the main goal is centering.
Common Mistake
Nesting multiple Padding widgets instead of combining values into a single EdgeInsets.
Lesson 5.6 — Card, Divider, Placeholder
Card
A Material Design container with elevation and rounded corners. Prefer Card over a plain Container with shadow when you want standard Material styling.
Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Card content'),
),
)
Divider
Draws a thin horizontal (or vertical) line.
const Divider(height: 32, thickness: 1)
const VerticalDivider(width: 24, thickness: 1)
Placeholder
A simple box useful during UI development before real content is ready.
const Placeholder(fallbackHeight: 120)
Module 2 — Layout Widgets
Lesson 5.7 — Row and Column
What Are They?
Row lays children out horizontally. Column lays children out vertically. They are the backbone of most Flutter layouts.
How Constraints Work
A Row or Column receives constraints from its parent and passes tight or loose constraints to its children depending on mainAxisSize and the presence of Expanded/Flexible.
Important Properties
- mainAxisAlignment
- crossAxisAlignment
- mainAxisSize
- children
Complete Example — Profile Header
Row(
children: [
const CircleAvatar(radius: 32, child: Icon(Icons.person)),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Sarah Johnson', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Text('@sarah_j', style: TextStyle(color: Colors.grey)),
],
),
),
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
],
)
Common Mistakes
- Forgetting Expanded or Flexible when a child needs to take remaining space → overflow errors.
- Nesting too many Row/Column without understanding constraint propagation.
Best Practices
- Prefer Expanded when the child should take all remaining space.
- Prefer Flexible when the child can be smaller than the remaining space.
- Keep the widget tree shallow when possible.
Lesson 5.8 — Expanded and Flexible
Expanded forces a child of a Row/Column/Flex to fill the remaining space along the main axis. Flexible allows a child to fill remaining space but does not force it.
Row(
children: [
Container(width: 60, height: 60, color: Colors.red),
Expanded(child: Container(height: 60, color: Colors.blue)),
Flexible(child: Container(width: 80, height: 60, color: Colors.green)),
],
)
Rule of Thumb
- Use Expanded for flexible content areas (lists, text that should grow).
- Use Flexible when you want the child to keep its intrinsic size if possible.
Lesson 5.9 — Stack and Positioned
Stack places children on top of each other. Positioned (only valid inside a Stack) gives explicit top/right/bottom/left values.
Complete Example — Image with Overlay
Stack(
children: [
Image.network('https://picsum.photos/400/250', fit: BoxFit.cover, width: double.infinity, height: 250),
Positioned(
bottom: 16,
left: 16,
right: 16,
child: Text(
'Beautiful Landscape',
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, shadows: [
Shadow(blurRadius: 8, color: Colors.black54),
]),
),
),
Positioned(
top: 12,
right: 12,
child: IconButton(
icon: const Icon(Icons.favorite_border, color: Colors.white),
onPressed: () {},
),
),
],
)
Common Mistakes
- Using Positioned outside a Stack.
- Forgetting that non-positioned children are aligned according to alignment (default top-start).
Lesson 5.10 — Wrap, Align, AspectRatio, FittedBox, ConstrainedBox
- Wrap — like a Row that wraps to the next line when space runs out (chips, tags, filters).
- Align — positions a child within itself using an Alignment value.
- AspectRatio — forces a child to maintain a width/height ratio.
- FittedBox — scales and positions its child within itself.
- ConstrainedBox — imposes additional constraints on its child.
These widgets solve very specific layout problems and are essential for polished UIs.
Module 3 — Scrolling Widgets
Lesson 5.11 — ListView and ListView.builder
ListView is the most common way to display a scrollable list of children. For long or infinite lists always prefer the builder constructor:
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(items[index].title),
subtitle: Text(items[index].subtitle),
onTap: () {},
);
},
)
Why builder?
It lazily builds only the widgets that are visible (plus a small buffer), which is critical for performance with hundreds or thousands of items.
ListView.separated adds a separator between items — ideal for dividers.
Lesson 5.12 — GridView
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: products.length,
itemBuilder: (context, index) {
return ProductCard(product: products[index]);
},
)
Common Mistake
Using a regular GridView with a large list of children instead of GridView.builder.
Lesson 5.13 — SingleChildScrollView and Nested Scrolling
Use SingleChildScrollView when you have a single child that may overflow (forms, long columns). Avoid nesting scrollable widgets that scroll in the same direction without special handling (NestedScrollView or careful use of PrimaryScrollController).
Lesson 5.14 — CustomScrollView and Slivers
Slivers are the low-level protocol for scrollable areas. CustomScrollView lets you compose multiple slivers:
- SliverAppBar (collapsing header)
- SliverList
- SliverGrid
- SliverToBoxAdapter
- SliverFillRemaining
Example — Collapsing Header + List
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('My App'),
background: Image.network('...', fit: BoxFit.cover),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item $index')),
childCount: 50,
),
),
],
)
This pattern is used in many professional apps (profile screens, product details, news readers).
Module 4 — Flutter Rendering Architecture
Widget → Element → RenderObject
Widget Tree — Immutable configuration objects. You create them in the build method. They describe what the UI should look like.
Element Tree — Mutable objects that hold the current widget configuration and manage the lifecycle. Elements decide whether an existing element can be updated with a new widget or must be replaced.
RenderObject Tree — The objects that perform layout and painting. Each RenderObject knows its size, position, and how to paint itself.
Phases
- Build — Widgets are created or updated.
- Layout — Constraints flow down, sizes flow up.
- Paint — RenderObjects paint into layers.
- Compositing — Layers are combined and sent to the GPU.
Understanding this pipeline helps you diagnose why a widget is not appearing, why layout is wrong, or why performance is poor.
Practical Tip
When you call setState, Flutter marks the corresponding element as dirty. Only the dirty subtrees are rebuilt. This is why keeping state as low as possible and using const widgets matters.
Level 5 Practice Project — Product Listing Screen
Build a complete product listing screen that includes:
- A custom AppBar
- A horizontal category list (ListView with horizontal scroll)
- A responsive product grid (GridView.builder)
- Each product card uses Container/Card, Image, Text, Icon, and proper padding
- Pull-to-refresh simulation
- Empty and loading states
This project consolidates everything taught in Level 5.
Level 5 Review Questions
- What is the difference between Expanded and Flexible?
- Why should you prefer ListView.builder over ListView for long lists?
- What happens if you put a Positioned widget outside a Stack?
- Explain the relationship between Widget, Element, and RenderObject in one paragraph.
- When would you choose CustomScrollView over a simple ListView?
LEVEL 6 — UI AND RESPONSIVE DESIGN
Level Overview
In Level 5 you learned the individual widgets that form the building blocks of every screen. In this level you will learn how to style those widgets consistently, create professional visual systems, and make interfaces that adapt gracefully to different screen sizes, orientations, and platforms.
Learning Objectives
- Create and apply a complete Material 3 theme (light and dark)
- Use ColorScheme, TextTheme, and component themes correctly
- Build consistent buttons, cards, dialogs, bottom sheets, and snack bars
- Control spacing, shapes, and elevation systematically
- Make layouts respond to screen size, orientation, and platform
- Use MediaQuery, LayoutBuilder, and OrientationBuilder effectively
- Design interfaces that work well on phones, tablets, foldables, desktop, and web
Prerequisites
- Solid understanding of basic and layout widgets (Level 5)
- Ability to create StatelessWidget and StatefulWidget
- Familiarity with Scaffold, AppBar, and basic navigation
Module 1 — Material Design and Material 3
Lesson 6.1 — What Is Material Design and Material 3?
What Is It?
Material Design is Google’s design system. Material 3 (also called Material You) is the latest version. It emphasizes dynamic color, larger shapes, improved accessibility, and personalization.
Why Is It Important?
Using Material 3 gives your application a modern, consistent look that users already understand. It also provides built-in support for light/dark themes, dynamic color, and accessible contrast.
How It Works in Flutter
Flutter implements Material 3 through ThemeData and ColorScheme. When you set useMaterial3: true, many widgets automatically adopt the new visual language.
Basic Theme Setup
MaterialApp(
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
home: const HomePage(),
)
Code Explanation
- ColorScheme.fromSeed generates a full harmonious palette from a single seed color.
- theme and darkTheme define the two modes.
- themeMode: ThemeMode.system follows the device setting.
Common Mistakes
- Setting useMaterial3: true but still using old Material 2 color properties.
- Hard-coding colors instead of reading from Theme.of(context).colorScheme.
Best Practices
- Always generate the ColorScheme from a seed.
- Prefer semantic colors (primary, onPrimary, surface, onSurface, error, etc.) over raw color values.
- Keep light and dark themes in sync by using the same seed.
Practice Exercise
Create a MaterialApp that switches between light and dark themes using a switch in the AppBar.
Lesson 6.2 — ColorScheme in Depth
Key Properties You Will Use Daily
- primary / onPrimary
- secondary / onSecondary
- surface / onSurface
- background / onBackground (now largely replaced by surface)
- error / onError
- outline, shadow, scrim, etc.
How to Access Colors
final colorScheme = Theme.of(context).colorScheme;
Container(
color: colorScheme.primaryContainer,
child: Text(
'Hello',
style: TextStyle(color: colorScheme.onPrimaryContainer),
),
)
Best Practice
Never hard-code Colors.blue or Color(0xFF...) in widgets when a semantic color exists. Always pull from the ColorScheme.
Lesson 6.3 — Typography and TextTheme
Material 3 Text Roles
displayLarge → displaySmall, headlineLarge → headlineSmall, titleLarge → titleSmall, bodyLarge → bodySmall, labelLarge → labelSmall
Complete Example
Text(
'Welcome back',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
)
Text(
'Here is your dashboard summary for today.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
Custom Fonts
- Add the font files to an assets/fonts folder.
- Declare them in pubspec.yaml.
- Apply them in ThemeData:
theme: ThemeData(
useMaterial3: true,
fontFamily: 'Inter',
textTheme: const TextTheme(
headlineMedium: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600),
bodyLarge: TextStyle(fontFamily: 'Inter'),
),
)
Common Mistake
Overriding too many text styles with one-off TextStyle instead of extending the theme.
Best Practice
Create a small set of semantic text styles and reuse them. Use copyWith only for minor adjustments.
Module 2 — Common UI Components
Lesson 6.4 — Buttons
Flutter provides several Material 3 button types: ElevatedButton, FilledButton, FilledButton.tonal, OutlinedButton, TextButton, IconButton, FloatingActionButton.
Professional Button Example
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('Create New'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
)
Best Practices
- Prefer FilledButton for the primary action on a screen.
- Use OutlinedButton or TextButton for secondary actions.
- Keep button height consistent (usually 40–48 logical pixels).
- Always provide an onPressed of null when the button should appear disabled.
Lesson 6.5 — Cards, Dialogs, Bottom Sheets, and SnackBars
Card
Card(
elevation: 0,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Revenue', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text('\$24,500', style: Theme.of(context).textTheme.headlineSmall),
],
),
),
)
Dialog
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirm Delete'),
content: const Text('This action cannot be undone.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
],
),
);
Bottom Sheet
showModalBottomSheet(
context: context,
showDragHandle: true,
builder: (context) => Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Options', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 16),
ListTile(leading: const Icon(Icons.share), title: const Text('Share'), onTap: () {}),
ListTile(leading: const Icon(Icons.link), title: const Text('Copy Link'), onTap: () {}),
],
),
),
);
SnackBar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Item saved successfully'),
behavior: SnackBarBehavior.floating,
action: SnackBarAction(label: 'Undo', onPressed: () {}),
),
);
Common Mistakes
- Using the root ScaffoldMessenger incorrectly when you have nested Scaffolds.
- Making bottom sheets too tall without making them scrollable.
Lesson 6.6 — Spacing, Shapes, and Elevation
Spacing System — Professional applications use a consistent spacing scale (usually multiples of 4 or 8): 4, 8, 12, 16, 24, 32, 48, 64.
class AppSpacing {
static const double xs = 4;
static const double sm = 8;
static const double md = 16;
static const double lg = 24;
static const double xl = 32;
}
Shapes — Material 3 encourages larger corner radii (12–28).
class AppShapes {
static final small = RoundedRectangleBorder(borderRadius: BorderRadius.circular(8));
static final medium = RoundedRectangleBorder(borderRadius: BorderRadius.circular(12));
static final large = RoundedRectangleBorder(borderRadius: BorderRadius.circular(16));
}
Module 3 — Responsive and Adaptive Design
Lesson 6.7 — MediaQuery
MediaQuery gives you information about the current screen: size, orientation, padding (notches, system UI), text scale factor, brightness, etc.
final size = MediaQuery.sizeOf(context);
final padding = MediaQuery.paddingOf(context);
final orientation = MediaQuery.orientationOf(context);
final textScaler = MediaQuery.textScalerOf(context);
Example — Avoiding the Notch
Padding(
padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top),
child: ...,
)
Or simply wrap content with SafeArea.
Lesson 6.8 — LayoutBuilder and Breakpoints
LayoutBuilder gives you the constraints of the parent, which is more reliable than MediaQuery when the widget is not full-screen.
class Breakpoints {
static const double mobile = 600;
static const double tablet = 900;
static const double desktop = 1200;
}
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < Breakpoints.mobile) {
return const MobileLayout();
} else if (constraints.maxWidth < Breakpoints.tablet) {
return const TabletLayout();
} else {
return const DesktopLayout();
}
},
);
}
Real-World Pattern
- Mobile: bottom navigation + single column
- Tablet: navigation rail + two columns
- Desktop: navigation rail or side drawer + multi-column content
Lesson 6.9 — OrientationBuilder
OrientationBuilder(
builder: (context, orientation) {
if (orientation == Orientation.portrait) {
return const PortraitLayout();
} else {
return const LandscapeLayout();
}
},
)
Lesson 6.10 — Adaptive Navigation and Responsive Grids
Navigation Patterns
- Phone → NavigationBar (bottom)
- Tablet / Desktop → NavigationRail or NavigationDrawer
Responsive Grid Example
GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 300,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 0.75,
),
itemBuilder: ...,
)
Level 6 Mini Project — Responsive Dashboard
Build a complete dashboard that includes:
- A proper Material 3 theme (light + dark) with a brand seed color.
- An AppBar with theme toggle.
- A responsive body (mobile: vertical list + bottom navigation; tablet/desktop: NavigationRail + multi-column grid).
- At least four summary cards using consistent spacing, typography, and ColorScheme colors.
- A floating action button that shows a modal bottom sheet with quick actions.
- A SnackBar confirmation when an action is performed.
- Safe handling of notches and system UI with SafeArea.
Level 6 Review Questions
- Why should you prefer ColorScheme.fromSeed over manually defining every color?
- What is the difference between MediaQuery.sizeOf(context) and the constraints given by LayoutBuilder?
- When would you choose a NavigationRail instead of a NavigationBar?
- Explain why hard-coding colors and text styles makes dark mode difficult.
- What is the recommended way to handle the status bar and notch area?
LEVEL 7 — NAVIGATION AND ROUTING
Level Overview
Real applications contain many screens. This level teaches you how Flutter handles navigation — from the classic imperative Navigator API to modern declarative routing.
Learning Objectives
- Navigate between screens using Navigator
- Define and use named routes
- Pass arguments to a screen and return results
- Implement nested navigation
- Build bottom navigation with state preservation using IndexedStack
- Protect routes that require the user to be logged in
- Understand the difference between imperative and declarative routing
- Structure a complete multi-screen application with a clean navigation flow
Prerequisites
- Comfortable creating StatelessWidget and StatefulWidget
- Understanding of Scaffold, AppBar, and basic Material widgets
- Basic knowledge of async/await (for returning results)
Module 1 — Core Navigation Concepts
Lesson 7.1 — Navigator and Basic Navigation
What Is It?
The Navigator is a Flutter widget that manages a stack of routes (screens). When you push a new route, it is added on top of the stack. When you pop, the top route is removed and the previous screen becomes visible again.
Basic Syntax
// Push a new screen
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SecondScreen(),
),
);
// Pop the current screen
Navigator.of(context).pop();
Complete Example — Two Screens
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: FilledButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DetailsScreen(),
),
);
},
child: const Text('Go to Details'),
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
const DetailsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details')),
body: Center(
child: FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Go Back'),
),
),
);
}
}
Code Explanation
- MaterialPageRoute creates a platform-adaptive transition (slide on iOS, fade/slide on Android).
- The builder function returns the widget that should be shown for that route.
- pop() removes the current route and returns to the previous one.
Common Mistakes
- Calling Navigator.pop(context) when there is nothing to pop (can throw).
- Using BuildContext after an async gap without checking mounted.
Best Practices
- Prefer Navigator.of(context).push over the shorter Navigator.push(context, ...) for clarity.
- Always check if (context.mounted) before using the context after an await.
Lesson 7.2 — Passing Arguments to a Screen
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ProductDetailsScreen(productId: 42),
),
);
class ProductDetailsScreen extends StatelessWidget {
const ProductDetailsScreen({super.key, required this.productId});
final int productId;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product $productId')),
body: Center(child: Text('Showing product #$productId')),
);
}
}
Lesson 7.3 — Returning Data from a Screen
// From the first screen
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
if (result != null && context.mounted) {
print('User selected: $result');
}
// Inside SelectionScreen
Navigator.of(context).pop('Selected Value');
Common Mistake
Forgetting to type the generic (push<String>) and then struggling with dynamic types.
Module 2 — Named Routes
Lesson 7.4 — Named Routes
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/details': (context) => const DetailsScreen(),
'/profile': (context) => const ProfileScreen(),
'/settings': (context) => const SettingsScreen(),
},
)
Navigator.of(context).pushNamed('/details');
Navigator.of(context).pushNamed('/profile');
Navigator.of(context).pop();
Passing Arguments with Named Routes
Navigator.of(context).pushNamed(
'/details',
arguments: 42,
);
// Inside DetailsScreen
final productId = ModalRoute.of(context)!.settings.arguments as int;
When Named Routes Become Limiting
Named routes work well for simple applications. When you need strongly typed arguments, route guards, deep linking, or nested navigation, most professional teams move to a declarative solution such as go_router.
Module 3 — Bottom Navigation and State Preservation
Lesson 7.5 — BottomNavigationBar + IndexedStack
The Problem
If you simply swap the body of a Scaffold when the user taps a bottom navigation item, the state of each tab is lost (scroll position, form data, etc.).
The Professional Solution
Keep all tab screens alive and only change which one is visible using IndexedStack.
class MainShell extends StatefulWidget {
const MainShell({super.key});
@override
State<MainShell> createState() => _MainShellState();
}
class _MainShellState extends State<MainShell> {
int _currentIndex = 0;
final List<Widget> _pages = const [
HomeTab(),
SearchTab(),
OrdersTab(),
ProfileTab(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() => _currentIndex = index);
},
destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.receipt_long_outlined), selectedIcon: Icon(Icons.receipt_long), label: 'Orders'),
NavigationDestination(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
Why IndexedStack?
All children stay in the widget tree. Only the visibility changes. Scroll positions and internal state are preserved.
Common Mistake
Rebuilding the entire page list on every tab change instead of keeping the list constant.
Module 4 — Nested Navigation and Protected Routes
Lesson 7.6 — Nested Navigation
Each tab can have its own navigation stack. This is common in apps where the Home tab can push detail screens while the Profile tab has its own stack. You achieve this by giving each tab its own Navigator (or by using a declarative router that supports nested routes).
Lesson 7.7 — Protected Routes (Authentication Guard)
Concept
Some screens should only be accessible when the user is logged in.
void openProfile(BuildContext context, bool isLoggedIn) {
if (isLoggedIn) {
Navigator.of(context).pushNamed('/profile');
} else {
Navigator.of(context).pushNamed('/login');
}
}
Better Approach
Centralize the decision in your routing layer so individual buttons do not need to know about authentication logic.
Module 5 — Modern Declarative Routing (go_router Concepts)
Why Declarative Routing?
Imperative navigation (push, pop) becomes hard to maintain in large applications. Declarative routing describes the entire navigation state as a function of application state. Deep linking, web support, and authentication redirects become much cleaner.
Core Ideas
- Routes are declared in one place.
- Redirects can be performed based on authentication state.
- Path parameters (/product/:id) are first-class.
- Nested navigation is supported naturally.
Level 7 Mini Project — Complete Multi-Screen Application
Build an application with the following screens and flows:
- Splash Screen — Shows for 2 seconds, then redirects to Login or Home depending on authentication state.
- Login Screen — Email + password fields. On success → Home. Link to Registration.
- Registration Screen — Name, email, password. On success → Home.
- Main Shell (after login) — Bottom navigation with four tabs: Home, Search, Orders, Profile. Use IndexedStack so state is preserved.
- Home Tab — List of items. Tapping an item pushes a Detail screen (nested navigation).
- Profile Tab — Shows user info + Logout button. Logout clears the session and returns to Login (clearing the navigation stack).
- Settings Screen — Reachable from Profile.
Requirements
- Use named routes or a clean Navigator structure.
- Pass at least one argument between screens.
- Return a result from one screen.
- Protect the Main Shell so it cannot be opened without “login”.
- Preserve tab state with IndexedStack.
- Handle the back button correctly on Android.
Level 7 Review Questions
- What is the difference between push and pushReplacement?
- Why is IndexedStack preferred over simply swapping widgets for bottom navigation?
- How do you safely return data from a screen that was opened with push?
- What problem do protected routes solve?
- When does the classic Navigator API start to feel limited?
LEVEL 8 — USER INPUT AND FORMS
Level Overview
This level teaches you how to build professional forms in Flutter: controlled text input, validation, focus management, different input types, date and time selection, and how to provide clear feedback while a form is being submitted.
Learning Objectives
- Use TextField and TextFormField correctly
- Manage text with TextEditingController
- Control focus with FocusNode
- Build forms with validation using Form and FormState
- Create custom validators
- Use dropdowns, checkboxes, radio buttons, and switches
- Show date and time pickers
- Handle loading and error states during form submission
- Build a complete, production-style registration and login form
Prerequisites
- Comfortable with StatelessWidget and StatefulWidget
- Understanding of basic layout widgets and theming
- Basic knowledge of navigation (push/pop)
Module 1 — Text Input Fundamentals
Lesson 8.1 — TextField and TextFormField
What Is It?
TextField is a basic material text input. TextFormField is the same input but designed to work inside a Form. It supports validation and integrates with FormState.
Basic Syntax
TextField(
decoration: InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
)
Preferred for Forms — TextFormField
TextFormField(
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
if (!value.contains('@')) {
return 'Enter a valid email';
}
return null;
},
)
Common Mistakes
- Using TextField inside a Form and then trying to validate it (it will not participate in form validation).
- Forgetting to dispose of controllers (memory leak).
Best Practices
- Prefer TextFormField whenever the field belongs to a form.
- Always provide a clear labelText or hintText.
- Use appropriate keyboardType and textInputAction.
Lesson 8.2 — TextEditingController
Complete Example
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
),
],
);
}
}
Important Rules
- Create the controller in State.
- Always call dispose() on every controller.
- Never create a controller inside the build method.
Lesson 8.3 — FocusNode and Keyboard Handling
Example — Moving Focus
final _emailFocus = FocusNode();
final _passwordFocus = FocusNode();
TextFormField(
focusNode: _emailFocus,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
_passwordFocus.requestFocus();
},
)
Dismissing the Keyboard
GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(...),
)
Best Practice
Dispose of every FocusNode you create, just like controllers.
Module 2 — Forms and Validation
Lesson 8.4 — Form and FormState
Complete Pattern
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
// Simulate network request
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Registration successful')),
);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Full Name'),
validator: (value) =>
value == null || value.trim().isEmpty ? 'Name is required' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) return 'Email is required';
if (!value.contains('@')) return 'Enter a valid email';
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value == null || value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
},
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _isLoading ? null : _submit,
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Create Account'),
),
),
],
),
);
}
}
Code Explanation
- The GlobalKey<FormState> gives you access to validate() and save().
- validate() runs every field’s validator.
- The button is disabled and shows a loading indicator while the request is in progress.
- mounted is checked after the async gap.
Common Mistakes
- Creating the GlobalKey inside build.
- Forgetting to disable the button while loading (user can submit multiple times).
- Not checking mounted after an await.
Lesson 8.5 — Custom Validators and Reusability
String? requiredValidator(String? value) {
if (value == null || value.trim().isEmpty) return 'This field is required';
return null;
}
String? emailValidator(String? value) {
if (value == null || value.isEmpty) return 'Email is required';
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) return 'Enter a valid email address';
return null;
}
String? passwordValidator(String? value) {
if (value == null || value.length < 8) {
return 'Password must be at least 8 characters';
}
return null;
}
Module 3 — Other Common Form Controls
Lesson 8.6 — Dropdown, Checkbox, Radio, Switch
DropdownButtonFormField
String? _selectedRole;
DropdownButtonFormField<String>(
value: _selectedRole,
decoration: const InputDecoration(labelText: 'Role'),
items: const [
DropdownMenuItem(value: 'user', child: Text('User')),
DropdownMenuItem(value: 'editor', child: Text('Editor')),
DropdownMenuItem(value: 'admin', child: Text('Admin')),
],
onChanged: (value) => setState(() => _selectedRole = value),
validator: (value) => value == null ? 'Please select a role' : null,
)
Checkbox
bool _agreeToTerms = false;
CheckboxListTile(
title: const Text('I agree to the Terms of Service'),
value: _agreeToTerms,
onChanged: (value) => setState(() => _agreeToTerms = value ?? false),
controlAffinity: ListTileControlAffinity.leading,
)
Switch
SwitchListTile(
title: const Text('Enable notifications'),
value: _notificationsEnabled,
onChanged: (value) => setState(() => _notificationsEnabled = value),
)
Lesson 8.7 — Date and Time Pickers
Date Picker
DateTime? _selectedDate;
Future<void> _pickDate() async {
final now = DateTime.now();
final picked = await showDatePicker(
context: context,
initialDate: _selectedDate ?? now,
firstDate: DateTime(1900),
lastDate: now,
);
if (picked != null) {
setState(() => _selectedDate = picked);
}
}
Time Picker
TimeOfDay? _selectedTime;
Future<void> _pickTime() async {
final picked = await showTimePicker(
context: context,
initialTime: _selectedTime ?? TimeOfDay.now(),
);
if (picked != null) {
setState(() => _selectedTime = picked);
}
}
Level 8 Mini Project — Professional Registration & Login System
1. Login Screen
- Email and password fields with validation
- “Forgot password?” text button
- Loading state on the login button
- Link to the Registration screen
- Error message area (for invalid credentials)
2. Registration Screen
- Full name, email, password, confirm password
- Password visibility toggle
- Terms & conditions checkbox (must be checked)
- Strong validation (email format, password length, passwords match)
- Loading state
- On success: navigate to Home and remove the auth screens from the stack
Extra Requirements
- Use a single Form per screen with a GlobalKey
- Dispose of all controllers and focus nodes
- Disable the submit button while loading
- Show a SnackBar on success or failure
- Keyboard should move correctly between fields (textInputAction + FocusNode)
- Tap outside a field should dismiss the keyboard
Level 8 Review Questions
- Why should you always dispose of TextEditingController and FocusNode?
- What is the difference between TextField and TextFormField?
- How does FormState.validate() decide whether a form is valid?
- Why is it important to disable the submit button while a request is in progress?
- How can you move focus from one field to the next when the user presses the “Next” key?
LEVEL 9 — STATE MANAGEMENT
Level Overview
This level teaches state management from first principles. You will start with the built-in tools (StatelessWidget, StatefulWidget, setState), learn when they become insufficient, and then master the most widely used professional solutions: Provider, Riverpod, and BLoC/Cubit.
Learning Objectives
- Understand the difference between ephemeral (local) state and application state
- Use setState correctly and know its limitations
- Lift state up when needed
- Implement state management with Provider + ChangeNotifier
- Build reactive applications with Riverpod (Providers, Notifier, AsyncNotifier)
- Structure business logic with Cubit and BLoC
- Compare the main approaches and select the appropriate one
Prerequisites
- Solid understanding of StatelessWidget and StatefulWidget
- Comfortable with forms, navigation, and basic async code
Module 1 — Built-in State Management
Lesson 9.1 — StatelessWidget vs StatefulWidget
StatelessWidget — A widget that depends only on its configuration (constructor parameters) and the inherited widgets around it. It has no internal mutable state.
StatefulWidget — A widget that can change over time. It is split into two classes: the widget itself (immutable) and a State object that holds the mutable data and the setState method.
When to use each
- Use StatelessWidget whenever possible.
- Use StatefulWidget only when the widget needs to update itself in response to user interaction or internal events.
Lesson 9.2 — setState and Widget Lifecycle
How setState Works
Calling setState marks the State object as dirty. Flutter schedules a rebuild of that widget and its descendants.
Basic Example
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Text('Count: $_counter', style: Theme.of(context).textTheme.headlineMedium),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
}
Important Lifecycle Methods
- initState — called once when the State is created. Ideal for controllers, listeners, and initial data loading.
- didChangeDependencies — called when inherited widgets change.
- dispose — called when the State is removed. Always cancel timers, dispose controllers, and remove listeners here.
Common Mistakes
- Calling setState after the widget has been disposed.
- Putting heavy work inside setState.
- Using setState for data that many distant widgets need (this leads to prop drilling).
Best Practice
Keep state as low in the tree as possible. Only lift it up when multiple siblings need the same data.
Module 2 — Provider and ChangeNotifier
Lesson 9.3 — Provider Fundamentals
Core Pieces
- ChangeNotifier — a class that holds state and notifies listeners when something changes.
- ChangeNotifierProvider — makes the notifier available to the widget tree.
- Consumer / context.watch / context.read — ways to listen to or read the state.
Complete Example — Simple Counter with Provider
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// In main.dart
ChangeNotifierProvider(
create: (_) => CounterNotifier(),
child: const MyApp(),
)
// In a widget
final counter = context.watch<CounterNotifier>();
Text('Count: ${counter.count}')
// To call a method without rebuilding
context.read<CounterNotifier>().increment();
Common Mistakes
- Using context.watch inside event handlers (causes unnecessary rebuilds or errors).
- Forgetting to call notifyListeners().
- Creating the provider too low in the tree.
Module 3 — Riverpod (Recommended Modern Approach)
Lesson 9.4 — Why Riverpod?
- No BuildContext required to read providers
- Compile-safe
- Better support for async state
- Easier testing
- Support for multiple providers of the same type
Lesson 9.5 — Core Riverpod Providers
- Provider — for immutable values or services
- StateProvider — for simple mutable state
- FutureProvider — for one-time async work
- StreamProvider — for streams
- Notifier / AsyncNotifier — for complex logic (preferred in modern code)
Basic Setup
final counterProvider = StateProvider<int>((ref) => 0);
class CounterView extends ConsumerWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
// To update
ref.read(counterProvider.notifier).state++;
Modern Notifier Example
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
Async Example (Loading Data)
final productsProvider = FutureProvider<List<Product>>((ref) async {
return await ref.watch(productRepositoryProvider).fetchProducts();
});
Best Practices with Riverpod
- Prefer Notifier / AsyncNotifier over StateProvider for anything beyond trivial state.
- Keep business logic inside notifiers, not inside widgets.
- Use ref.watch only inside build methods. Use ref.read inside callbacks.
Module 4 — BLoC and Cubit
Lesson 9.6 — Cubit (Simplified BLoC)
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
BlocProvider(
create: (_) => CounterCubit(),
child: ...,
)
// Listening
BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('$count'),
)
Lesson 9.7 — Full BLoC (Events + States)
Use full BLoC when you want a clear separation between events (user intentions) and states (UI representation), or when the logic is complex.
Comparison Table
Recommendation
- Small apps or prototypes → setState + Provider
- Most professional Flutter apps today → Riverpod
- Teams that already use BLoC or need very strict event-driven architecture → Cubit/BLoC
Level 9 Mini Project
Build a simple shopping cart using Riverpod:
- Product list (FutureProvider)
- Cart state (Notifier)
- Ability to add/remove items
- Total price calculation
- Clear cart action
- UI that reacts to loading, error, and data states
LEVEL 10 — WORKING WITH APIs
Level Overview
This level teaches you how to work with APIs professionally in Flutter: HTTP requests, JSON, clean Dart models, loading and error states, authentication, pagination, and search.
Learning Objectives
- Explain what an API is and how client-server communication works
- Perform GET, POST, PUT, PATCH, and DELETE requests
- Work with query parameters, path parameters, headers, and request bodies
- Parse JSON into strongly typed Dart models
- Use the http package effectively
- Handle network errors, timeouts, and HTTP status codes
- Implement loading, empty, and error states in the UI
- Add authentication headers (Bearer tokens)
- Implement pagination and infinite scrolling
- Structure API code using services and repositories
Prerequisites
- Solid understanding of Dart (especially classes, async/await, and null safety)
- Comfortable with StatefulWidget / state management basics (Level 9)
- Ability to build forms and display lists
Module 1 — API Fundamentals
Lesson 10.1 — What Is an API?
What Is It?
API stands for Application Programming Interface. In the context of mobile development it usually means a web API: a set of URLs (endpoints) that a server exposes so that clients (your Flutter app) can request or send data.
Why Is It Important?
Without APIs, every application would need its own database and business logic running only on the device. APIs allow a single backend to serve many clients and keep data centralized and secure.
Detailed Explanation
When your Flutter app needs data, it does not open a database connection directly. Instead it sends an HTTP request to a server, which processes it and returns a response (almost always JSON). Your app then converts that response into Dart objects and displays them.
Common Mistakes
- Thinking the Flutter app can directly access a remote database (it should not).
- Hard-coding API responses instead of learning to consume real endpoints.
Best Practices
- Treat the API as a contract. Your app should depend on the documented shape of the data, not on implementation details of the server.
- Always assume the network can fail.
Review Questions
- What does API stand for?
- Why do mobile apps use APIs instead of connecting directly to a database?
Lesson 10.2 — Client/Server Architecture
How It Works
- The client creates an HTTP request.
- The request travels over the network to the server.
- The server authenticates the request (if required), executes business logic, and often queries a database.
- The server sends an HTTP response back (status code + body).
- The client inspects the status code and parses the body.
Common Mistakes
- Assuming the server is always available and fast.
- Performing heavy work on the main isolate while waiting for a response.
Lesson 10.3 — HTTP Fundamentals
Important Parts of an HTTP Request
- Method (GET, POST, PUT, PATCH, DELETE, etc.)
- URL (endpoint)
- Headers (metadata such as content type and authentication)
- Body (data sent with POST/PUT/PATCH)
Important Parts of an HTTP Response
- Status code (200, 201, 400, 401, 404, 500, …)
- Headers
- Body (usually JSON)
GET /products?category=electronics HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Accept: application/json
Lesson 10.4 — REST and RESTful APIs
Core Principles
- Resources are identified by URLs (/users, /products/42)
- Standard methods are used (GET to read, POST to create, etc.)
- Communication is stateless
- Responses are usually JSON
Example Resource Design
- GET /products → list of products
- GET /products/15 → single product
- POST /products → create a new product
- PUT /products/15 → replace product 15
- PATCH /products/15 → partially update product 15
- DELETE /products/15 → delete product 15
Module 2 — Making HTTP Requests in Flutter
Lesson 10.5 — The http Package
dependencies:
http: ^1.2.0
Basic GET Request
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> fetchProducts() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
print(data);
} else {
throw Exception('Failed to load data: ${response.statusCode}');
}
}
Common Mistakes
- Forgetting to check the status code.
- Calling an API without handling the case when the device has no internet.
Lesson 10.6 — GET Requests in Practice
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
);
}
}
Future<List<Post>> fetchPosts() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('Failed to load posts');
}
}
Lesson 10.7 — POST Requests
Future<Post> createPost(String title, String body) async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode({
'title': title,
'body': body,
'userId': 1,
}),
);
if (response.statusCode == 201) {
return Post.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create post');
}
}
Lesson 10.8 — PUT, PATCH, and DELETE
// DELETE example
Future<void> deletePost(int id) async {
final response = await http.delete(
Uri.parse('https://jsonplaceholder.typicode.com/posts/$id'),
);
if (response.statusCode != 200 && response.statusCode != 204) {
throw Exception('Failed to delete post');
}
}
Lesson 10.9 — Headers, Query Parameters, and Path Parameters
final uri = Uri.https('api.example.com', '/products', {
'category': 'phones',
'page': '1',
'limit': '20',
});
Common headers: Content-Type: application/json, Authorization: Bearer <token>, Accept: application/json.
Lesson 10.10 — HTTP Status Codes
- 2xx → Success (200 OK, 201 Created, 204 No Content)
- 4xx → Client error (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found)
- 5xx → Server error (500 Internal Server Error, 503 Service Unavailable)
Module 3 — JSON and Models
Lesson 10.11 — JSON Serialization and Deserialization
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
When to Move to Code Generation
When you have many models or nested objects, manual fromJson becomes tedious and error-prone. Then introduce json_serializable or freezed.
Lesson 10.12 — Error Handling and Loading States
enum DataState { initial, loading, success, error }
class PostsState {
final DataState state;
final List<Post> posts;
final String? errorMessage;
PostsState({
this.state = DataState.initial,
this.posts = const [],
this.errorMessage,
});
}
Network Timeouts
final response = await http
.get(uri)
.timeout(const Duration(seconds: 10));
Lesson 10.13 — Authentication with Bearer Tokens
Future<List<Post>> fetchProtectedPosts(String token) async {
final response = await http.get(
Uri.parse('https://api.example.com/posts'),
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
},
);
}
Lesson 10.14 — Pagination and Infinite Scrolling
The API returns a page of results plus metadata (page, totalPages, nextPage). In Flutter you keep the current page number, append new items to the existing list, and load the next page when the user scrolls near the bottom (ScrollController).
Level 10 Complete Project — News / Posts Application
Build a complete application that:
- Fetches a list of posts from a public API.
- Shows a loading indicator while fetching.
- Displays the posts in a ListView.
- Handles errors with a user-friendly message and Retry button.
- Supports pull-to-refresh.
- Allows tapping a post to open a detail screen.
- Implements search (client-side filtering is acceptable for the first version).
- Uses clean separation: Model → API Service → Repository → UI.
Suggested Folder Structure
lib/
├── models/
│ └── post.dart
├── services/
│ └── api_service.dart
├── repositories/
│ └── post_repository.dart
├── providers/
│ └── post_provider.dart
├── screens/
│ ├── post_list_screen.dart
│ └── post_detail_screen.dart
└── main.dart
Level 10 Review Questions
- What is the difference between PUT and PATCH?
- Why should you always check response.statusCode before parsing JSON?
- What is the purpose of a fromJson factory constructor?
- How do you send a Bearer token with a request?
- What are the three main UI states you should handle when loading data from an API?
LEVEL 11 — LOCAL STORAGE AND DATABASES
Level Overview
This level teaches the main local persistence options available in Flutter, from simple key-value storage to full relational databases.
Learning Objectives
- Store and retrieve simple values with SharedPreferences
- Securely store sensitive data with flutter_secure_storage
- Design tables and perform CRUD operations with SQLite
- Use the sqflite package
- Work with Drift for type-safe database access
- Implement database migrations
- Build an offline-first Notes application with search and categories
- Decide which storage solution fits different kinds of data
Prerequisites
- Comfortable with Dart classes, async/await, and models
- Experience with state management (Level 9)
- Ability to work with lists and forms
Module 1 — Simple Local Storage
Lesson 11.1 — Local Storage Concepts
Main Categories
- Key-value storage (SharedPreferences, secure storage)
- Relational databases (SQLite / Drift)
- NoSQL / document-style solutions (less common in pure Flutter)
- File storage (for images, PDFs, etc.)
Best Practice
Choose the simplest tool that meets your needs. Do not use a full database for a single boolean flag.
Lesson 11.2 — SharedPreferences
When to Use It
Theme mode, language preference, onboarding completed flag, last selected tab, simple non-sensitive settings.
dependencies:
shared_preferences: ^2.2.0
import 'package:shared_preferences/shared_preferences.dart';
Future<void> saveThemeMode(bool isDark) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isDarkMode', isDark);
}
Future<bool> loadThemeMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('isDarkMode') ?? false;
}
Common Mistakes
- Storing large objects or sensitive data (tokens, passwords).
- Calling getInstance() on every single read instead of caching the instance when appropriate.
Best Practices
- Create a small wrapper class (e.g., SettingsService) so the rest of the app does not depend directly on SharedPreferences.
- Use clear, consistent key names.
Lesson 11.3 — flutter_secure_storage
When to Use It
Authentication tokens, refresh tokens, API keys, any sensitive user data.
dependencies:
flutter_secure_storage: ^9.0.0
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
const storage = FlutterSecureStorage();
Future<void> saveToken(String token) async {
await storage.write(key: 'access_token', value: token);
}
Future<String?> readToken() async {
return await storage.read(key: 'access_token');
}
Future<void> deleteToken() async {
await storage.delete(key: 'access_token');
}
Module 2 — SQLite and sqflite
Lesson 11.4 — Relational Databases and SQLite
Core Concepts
- Table → collection of rows
- Column → field with a type
- Primary key → unique identifier (usually id INTEGER PRIMARY KEY AUTOINCREMENT)
- Foreign key → reference to another table
- Query → SQL statement that reads or modifies data
Lesson 11.5 — Using sqflite
dependencies:
sqflite: ^2.3.0
path: ^1.8.0
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
Future<Database> openNotesDatabase() async {
final databasePath = await getDatabasesPath();
final path = join(databasePath, 'notes.db');
return openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE notes(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
category TEXT,
created_at TEXT NOT NULL,
is_deleted INTEGER DEFAULT 0
)
''');
},
);
}
Lesson 11.6 — CRUD Operations with sqflite
// Create
Future<int> insertNote(Database db, Map<String, dynamic> note) async {
return await db.insert('notes', note);
}
// Read
Future<List<Map<String, dynamic>>> getNotes(Database db) async {
return await db.query(
'notes',
where: 'is_deleted = ?',
whereArgs: [0],
orderBy: 'created_at DESC',
);
}
// Update
Future<int> updateNote(Database db, int id, Map<String, dynamic> values) async {
return await db.update(
'notes',
values,
where: 'id = ?',
whereArgs: [id],
);
}
// Delete (Soft Delete)
Future<int> softDeleteNote(Database db, int id) async {
return await db.update(
'notes',
{'is_deleted': 1},
where: 'id = ?',
whereArgs: [id],
);
}
Best Practice
Prefer soft deletes when users may want to restore data. Provide a permanent delete option separately.
Module 3 — Modern Approach with Drift
Lesson 11.7 — Introduction to Drift
Drift (formerly Moor) is a reactive, type-safe persistence library built on top of SQLite. You define tables in Dart and Drift generates the corresponding code.
class Notes extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 200)();
TextColumn get content => text()();
TextColumn get category => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
}
Lesson 11.8 — Database Migrations
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await db.execute('ALTER TABLE notes ADD COLUMN color TEXT');
}
},
Always test migrations carefully. Never destroy user data.
Lesson 11.9 — Offline-First Principles
Core Ideas
- Read from local storage first (instant UI).
- Synchronize with the server when the network is available.
- Queue write operations performed while offline.
- Resolve conflicts (last-write-wins or more sophisticated strategies).
Level 11 Complete Project — Offline Notes Application
Build a fully functional offline Notes app with: create, view, edit, soft-delete, restore (Trash), search, filter by category, persistence, and complete offline functionality.
Folder Structure Suggestion
lib/
├── models/
│ └── note.dart
├── database/
│ └── notes_database.dart
├── repositories/
│ └── note_repository.dart
├── providers/
│ └── note_provider.dart
├── screens/
│ ├── notes_list_screen.dart
│ ├── note_editor_screen.dart
│ └── trash_screen.dart
└── main.dart
Level 11 Review Questions
- When should you use SharedPreferences instead of a database?
- Why is flutter_secure_storage preferred for tokens?
- What is a soft delete and why is it useful?
- What problem do database migrations solve?
- Explain the basic idea of an offline-first approach.
LEVEL 12 — FIREBASE WITH FLUTTER
Level Overview
Firebase is a Backend-as-a-Service (BaaS) platform from Google. This level teaches you how to integrate Firebase into a Flutter application properly: project setup, authentication, Cloud Firestore, Storage, and basic cloud messaging.
Learning Objectives
- Create a Firebase project and connect it to a Flutter application
- Implement email/password and Google authentication
- Perform CRUD operations with Cloud Firestore
- Listen to real-time updates from Firestore
- Upload and download files with Firebase Storage
- Understand the basics of Firebase Cloud Messaging
- Write simple Firestore security rules
- Structure a Flutter app that uses Firebase cleanly
Prerequisites
- Comfortable with Dart classes, async/await, and models
- Understanding of state management (Level 9)
- Experience with forms and navigation
Module 1 — Firebase Fundamentals and Setup
Lesson 12.1 — What Is Firebase?
What Is It?
Firebase is a platform developed by Google that provides a suite of cloud services: Firebase Authentication, Cloud Firestore (NoSQL database), Firebase Storage, Firebase Cloud Messaging, and Firebase Analytics/Crashlytics.
Best Practices
- Always configure proper security rules before releasing an app.
- Keep Firebase-related code behind repositories so you can replace the backend later if needed.
Lesson 12.2 — Creating a Firebase Project and Connecting Flutter
Steps (High-Level)
- Go to the Firebase Console and create a new project.
- Register an Android app (package name must match your Flutter app).
- Register an iOS app (bundle identifier must match).
- Download the configuration files (google-services.json, GoogleService-Info.plist).
- Place them in the correct locations in your Flutter project.
- Install the FlutterFire CLI and run the configuration command.
- Add the required FlutterFire packages to pubspec.yaml.
dependencies:
firebase_core: ^3.0.0
firebase_auth: ^5.0.0
cloud_firestore: ^5.0.0
firebase_storage: ^12.0.0
google_sign_in: ^6.2.0 # for Google Sign-In
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
Common Mistakes
- Forgetting to call Firebase.initializeApp() before using any Firebase service.
- Mismatched package name / bundle ID between Firebase and the Flutter project.
Module 2 — Firebase Authentication
Lesson 12.3 — Email and Password Authentication
// Register
final credential = await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: email,
password: password,
);
// Sign in
final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password,
);
// Sign out
await FirebaseAuth.instance.signOut();
// Current user
final user = FirebaseAuth.instance.currentUser;
Listening to Auth State
StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return const HomeScreen();
}
return const LoginScreen();
},
)
Code Explanation
authStateChanges() emits the current user whenever the authentication state changes (login, logout, token refresh). This is the recommended way to protect routes and update the UI.
Best Practices
- Validate email and password on the client before calling Firebase.
- Show clear loading states during authentication calls.
- Never store passwords yourself; let Firebase handle them.
Lesson 12.4 — Google Authentication
final GoogleSignIn googleSignIn = GoogleSignIn();
Future<UserCredential?> signInWithGoogle() async {
final GoogleSignInAccount? googleUser = await googleSignIn.signIn();
if (googleUser == null) return null; // user cancelled
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
return await FirebaseAuth.instance.signInWithCredential(credential);
}
Lesson 12.5 — Password Reset
await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
Module 3 — Cloud Firestore
Lesson 12.6 — Firestore Fundamentals
Core Concepts
- Collection → similar to a table (e.g., users, posts)
- Document → similar to a row (has an ID and fields)
- Field → a key-value pair inside a document
- Subcollection → a collection nested inside a document
await FirebaseFirestore.instance.collection('notes').add({
'title': title,
'content': content,
'userId': currentUserId,
'createdAt': FieldValue.serverTimestamp(),
});
final doc = await FirebaseFirestore.instance.collection('notes').doc(noteId).get();
if (doc.exists) {
final data = doc.data()!;
}
final querySnapshot = await FirebaseFirestore.instance
.collection('notes')
.where('userId', isEqualTo: currentUserId)
.orderBy('createdAt', descending: true)
.get();
Lesson 12.7 — Real-Time Listeners
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('notes')
.where('userId', isEqualTo: currentUserId)
.orderBy('createdAt', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
final notes = snapshot.data!.docs;
return ListView.builder(
itemCount: notes.length,
itemBuilder: (context, index) {
final data = notes[index].data() as Map<String, dynamic>;
return ListTile(
title: Text(data['title'] ?? ''),
subtitle: Text(data['content'] ?? ''),
);
},
);
},
)
Lesson 12.8 — Firestore Security Rules (Introduction)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /notes/{noteId} {
allow read, write: if request.auth != null && request.auth.uid == resource.data.userId;
}
}
}
Best Practice
Never leave rules open (allow read, write: if true) in production.
Module 4 — Firebase Storage and Messaging
Lesson 12.9 — Firebase Storage
final storageRef = FirebaseStorage.instance
.ref()
.child('user_uploads')
.child(userId)
.child('${DateTime.now().millisecondsSinceEpoch}.jpg');
final uploadTask = await storageRef.putFile(file);
final downloadUrl = await uploadTask.ref.getDownloadURL();
Lesson 12.10 — Cloud Messaging Basics
- Add the firebase_messaging package.
- Request permission (especially on iOS).
- Obtain the device FCM token.
- Send the token to your backend or store it in Firestore.
- Handle foreground and background messages.
Level 12 Complete Project — Firebase Notes / Social Mini-App
Build an application that includes email/password registration and login, auth state listening, real-time note CRUD linked to the current user, optional image upload, proper loading/error states, and logout.
Level 12 Review Questions
- Why must you call Firebase.initializeApp() before using any Firebase service?
- What is the advantage of using authStateChanges() instead of checking currentUser only once?
- What is the difference between .get() and .snapshots() in Firestore?
- Why are security rules critical when using Firestore?
- When would you choose Firebase over a custom REST API?
LEVEL 13 — AUTHENTICATION AND SECURITY
Level Overview
This level teaches the core concepts of authentication and authorization, token-based systems, secure storage, biometric authentication, and practical defensive coding techniques.
Learning Objectives
- Distinguish between authentication and authorization
- Explain how access tokens and refresh tokens work
- Store sensitive data securely on the device
- Implement biometric authentication
- Apply proper input validation and sanitization
- Recognize common mobile security vulnerabilities
- Design a secure authentication architecture
- Protect API communication with HTTPS and tokens
Module 1 — Core Authentication Concepts
Lesson 13.1 — Authentication vs Authorization
Authentication answers “Who are you?” — verifying identity. Authorization answers “What are you allowed to do?” — determining permissions.
Common Mistake
Checking only if (currentUser != null) before allowing sensitive operations.
Best Practice
Always verify both identity and permissions.
Lesson 13.2 — Sessions, Access Tokens, and Refresh Tokens
Access Token — A short-lived credential sent with every API request. Typically valid for 15 minutes to 1 hour.
Refresh Token — A longer-lived credential used only to obtain a new access token.
Typical Flow
- User logs in → server returns access token + refresh token.
- Client stores both tokens securely.
- Client sends the access token in the Authorization header on every request.
- When the access token expires, the client uses the refresh token to get a new access token.
- If the refresh token is invalid, the user must log in again.
Lesson 13.3 — JWT (JSON Web Tokens)
Structure: header.payload.signature. The payload often contains User ID, expiration time (exp), issued-at time (iat), roles or permissions.
Important Security Notes
- Never trust the payload without verifying the signature.
- Do not store sensitive information inside a JWT.
- Always check the expiration claim.
Module 2 — Secure Storage and Biometrics
Lesson 13.4 — Secure Token Storage
Why Not SharedPreferences?
SharedPreferences data is not encrypted by default on all platforms and can be extracted from a rooted/jailbroken device or from backups.
final storage = FlutterSecureStorage();
// Save
await storage.write(key: 'access_token', value: accessToken);
await storage.write(key: 'refresh_token', value: refreshToken);
// Read
final accessToken = await storage.read(key: 'access_token');
// Delete on logout
await storage.deleteAll();
Lesson 13.5 — Biometric Authentication
final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics;
final isDeviceSupported = await localAuth.isDeviceSupported();
if (canCheck || isDeviceSupported) {
final didAuthenticate = await localAuth.authenticate(
localizedReason: 'Please authenticate to continue',
options: const AuthenticationOptions(
biometricOnly: false,
stickyAuth: true,
),
);
}
Module 3 — Secure Communication and Input Handling
Lesson 13.6 — HTTPS and API Security
- Always use https:// URLs
- Send tokens only in headers, never in query parameters
- Validate server responses
- Consider certificate pinning for high-security apps
Lesson 13.7 — Input Validation and Sanitization
Any data coming from the user (forms, query parameters, deep links) must be treated as untrusted. Validate on the client for user experience, and always re-validate and sanitize on the server.
Module 4 — Common Mobile Vulnerabilities
Lesson 13.8 — Frequent Security Problems and Defenses
- Insecure Data Storage — Use secure storage; minimize sensitive data on device.
- Insufficient Transport Layer Protection — Enforce HTTPS everywhere.
- Weak Server-Side Validation — Never rely solely on client validation.
- Broken Authentication — Use proven authentication systems and follow token best practices.
- Insecure Direct Object References — Always verify on the server that the authenticated user owns or has permission for the requested resource.
- Excessive Permissions — Request only the permissions required for the current feature.
Level 13 Practical Project — Secure Authentication Flow
Build or extend an authentication system with strong client-side validation, tokens stored in secure storage, automatic session restoration, proper logout, optional biometric unlock, protected routes, and handling of expired sessions.
Level 13 Review Questions
- What is the difference between authentication and authorization?
- Why should access tokens be short-lived?
- Why is SharedPreferences not suitable for storing refresh tokens?
- What is the main risk of trusting only client-side input validation?
- Name three common mobile security vulnerabilities and one defense for each.
LEVEL 14 — ADVANCED FLUTTER
Level Overview
This level covers deeper control over the framework: widget lifecycle, keys, custom painting, animations, gestures, and isolates.
Learning Objectives
- Explain the widget lifecycle and use initState, didUpdateWidget, and dispose correctly
- Understand the rules of BuildContext
- Use different types of keys effectively
- Share data down the tree with InheritedWidget
- Draw custom graphics with CustomPainter
- Create both implicit and explicit animations
- Implement Hero animations and custom page transitions
- Handle complex gestures
- Run expensive work in isolates
Module 1 — Widget Lifecycle and BuildContext
Lesson 14.1 — Widget Lifecycle
Key Methods
- initState — called once when the State is inserted into the tree.
- didChangeDependencies — called when an inherited widget changes.
- didUpdateWidget — called when the parent rebuilds the widget with new configuration.
- build — called whenever the widget needs to render.
- deactivate / dispose — called when the State is removed.
Example — Correct Controller Management
class TimerScreen extends StatefulWidget {
const TimerScreen({super.key});
@override
State<TimerScreen> createState() => _TimerScreenState();
}
class _TimerScreenState extends State<TimerScreen> {
late Timer _timer;
int _seconds = 0;
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted) {
setState(() => _seconds++);
}
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text('Seconds: $_seconds');
}
}
Best Practice
Always pair every resource you create (controllers, timers, stream subscriptions) with cleanup in dispose. Check mounted before calling setState after an async gap.
Lesson 14.2 — BuildContext Rules
Important Rules
- Do not use a context across async gaps without checking mounted.
- Do not use the context of a widget that is not yet fully mounted.
- The context of a parent is different from the context of a child.
onPressed: () async {
await someAsyncOperation();
if (!context.mounted) return;
Navigator.of(context).pop();
}
Module 2 — Keys
Lesson 14.3 — Why Keys Exist
Common Types
- ValueKey — based on a primitive value (id, string, number)
- ObjectKey — based on an object identity
- UniqueKey — forces a new identity every time
- GlobalKey — provides a global handle to a widget’s state or context
ListView(
children: items.map((item) {
return ListTile(
key: ValueKey(item.id),
title: Text(item.title),
);
}).toList(),
)
Common Mistake
Using GlobalKey for everything. GlobalKeys are expensive and should be used sparingly.
Module 3 — InheritedWidget and Custom Widgets
Lesson 14.4 — InheritedWidget
class UserInherited extends InheritedWidget {
const UserInherited({
super.key,
required this.userName,
required super.child,
});
final String userName;
static UserInherited of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<UserInherited>();
assert(result != null, 'No UserInherited found in context');
return result!;
}
@override
bool updateShouldNotify(UserInherited oldWidget) {
return userName != oldWidget.userName;
}
}
Lesson 14.5 — Building Reusable Custom Widgets
Extract repeated UI patterns into well-named, configurable widgets. Keep them focused and prefer composition over deep inheritance.
Module 4 — Custom Painting
Lesson 14.6 — CustomPainter and Canvas
class CirclePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
40,
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// Usage
CustomPaint(
size: const Size(200, 200),
painter: CirclePainter(),
)
Module 5 — Gestures
Lesson 14.7 — GestureDetector and Advanced Gestures
GestureDetector(
onTap: () {},
onDoubleTap: () {},
onLongPress: () {},
onPanUpdate: (details) {
// details.delta gives movement
},
onScaleUpdate: (details) {
// details.scale for pinch zoom
},
child: ...,
)
Module 6 — Animations
Lesson 14.8 — Implicit Animations
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: isExpanded ? 300 : 100,
height: 100,
color: isExpanded ? Colors.blue : Colors.grey,
)
Lesson 14.9 — Explicit Animations with AnimationController
class FadeInDemo extends StatefulWidget {
const FadeInDemo({super.key});
@override
State<FadeInDemo> createState() => _FadeInDemoState();
}
class _FadeInDemoState extends State<FadeInDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeIn),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animation,
child: const FlutterLogo(size: 120),
);
}
}
Lesson 14.10 — Hero Animations and Page Transitions
// First screen
Hero(
tag: 'product-image-${product.id}',
child: Image.network(product.imageUrl),
)
// Second screen
Hero(
tag: 'product-image-${product.id}',
child: Image.network(product.imageUrl),
)
Module 7 — Isolates and Background Work
Lesson 14.11 — Isolates
Future<List<Product>> parseProducts(String jsonString) async {
return await compute(_parse, jsonString);
}
List<Product> _parse(String jsonString) {
final list = jsonDecode(jsonString) as List;
return list.map((e) => Product.fromJson(e)).toList();
}
Level 14 Mini Project — Animated Product Card Gallery
Build a screen with a grid of product cards, Hero animation to a detail screen, an explicit animation on the detail screen, a custom-painted badge, a swipe-to-dismiss gesture, and a heavy operation performed in an isolate.
Level 14 Review Questions
- Why must you cancel timers and dispose controllers in dispose?
- When should you use a ValueKey versus a GlobalKey?
- What is the main advantage of implicit animations over explicit ones?
- Why do we need isolates in a Flutter application?
- What problem does the Hero widget solve?
LEVEL 15 — FLUTTER ARCHITECTURE
Level Overview
This level teaches professional software architecture applied to Flutter: MVC, MVVM, Clean Architecture, SOLID principles, the repository pattern, dependency injection, and feature-first project structure.
Module 1 — Why Architecture Matters
Lesson 15.1 — The Cost of Poor Structure
Without architecture, codebases suffer from widgets that are hundreds of lines long, duplicated logic, difficult testing, fear of change, and hard onboarding.
Module 2 — Common Architectural Patterns
Lesson 15.2 — MVC and MVVM
MVC — Model (data and business rules), View (the UI), Controller (coordinates between Model and View).
MVVM — Model (data), View (pure UI), ViewModel (holds UI state and exposes methods/commands). MVVM maps naturally to Flutter using Riverpod Notifiers, Provider + ChangeNotifier, or similar.
Lesson 15.3 — Clean Architecture
Typical Layers in a Flutter App
- Presentation (UI) — Widgets, pages, state management. Knows nothing about data sources.
- Domain — Business rules, entities, and use cases. Pure Dart. No Flutter imports.
- Data — Repositories (implementations), data sources (API, database, cache), DTOs and mappers.
Dependency Rule: Presentation → Domain ← Data. The Domain layer sits in the middle and defines interfaces (abstract repositories). The Data layer implements those interfaces.
Module 3 — Key Building Blocks
Lesson 15.4 — The Repository Pattern
abstract class AuthRepository {
Future<User> login(String email, String password);
Future<void> logout();
Stream<User?> authStateChanges();
Future<User?> getCurrentUser();
}
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remote;
final AuthLocalDataSource local;
AuthRepositoryImpl({required this.remote, required this.local});
@override
Future<User> login(String email, String password) async {
final user = await remote.login(email, password);
await local.cacheUser(user);
return user;
}
}
Lesson 15.5 — Dependency Injection
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepositoryImpl(
remote: ref.watch(authRemoteDataSourceProvider),
local: ref.watch(authLocalDataSourceProvider),
);
});
Lesson 15.6 — SOLID Principles in Flutter
- Single Responsibility — a class should have one reason to change.
- Open/Closed — open for extension, closed for modification.
- Liskov Substitution — subtypes must be substitutable for their base types.
- Interface Segregation — prefer small, focused interfaces.
- Dependency Inversion — depend on abstractions, not concretions.
Module 4 — Project Structure
Lesson 15.7 — Feature-First Organization
lib/
├── core/
│ ├── error/
│ ├── network/
│ ├── theme/
│ ├── utils/
│ └── widgets/
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ ├── models/
│ │ │ └── repositories/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ ├── repositories/
│ │ │ └── usecases/
│ │ └── presentation/
│ │ ├── providers/
│ │ ├── screens/
│ │ └── widgets/
│ ├── home/
│ ├── profile/
│ └── settings/
├── shared/
└── main.dart
Lesson 15.8 — Layered Structure Inside a Feature
- presentation may import domain
- data may import domain
- domain must not import presentation or data
- presentation should not import data directly
Level 15 Practical Project — Refactor a Feature into Clean Architecture
Take a simple feature and refactor it: define entities and repository interfaces in the domain layer, implement the repository in the data layer, create use cases, expose state through a Notifier/Cubit/ViewModel, and wire everything with dependency injection.
Level 15 Review Questions
- What is the main dependency rule in Clean Architecture?
- Why should the domain layer contain no Flutter imports?
- What problem does the repository pattern solve?
- Why is feature-first organization often preferred over purely technical layering?
- Give one concrete benefit of dependency injection for testing.
LEVEL 16 — TESTING
Level Overview
This level teaches the three main categories of testing in Flutter: unit tests, widget tests, and integration tests.
Module 1 — Testing Fundamentals
Lesson 16.1 — Why Testing Matters
The Testing Pyramid
- Many fast unit tests (business logic, pure functions, repositories)
- Fewer widget tests (UI components and screens)
- A small number of integration / end-to-end tests (critical user flows)
Common Mistake
Writing only end-to-end tests. They are slow, brittle, and expensive to maintain.
Lesson 16.2 — Test Organization
test/
├── unit/
│ ├── models/
│ ├── repositories/
│ └── notifiers/
├── widget/
│ ├── screens/
│ └── widgets/
└── integration/
Module 2 — Unit Testing
Lesson 16.3 — Writing Your First Unit Tests
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/post.dart';
void main() {
group('Post', () {
test('fromJson creates a valid Post', () {
final json = {
'id': 1,
'title': 'Test Title',
'body': 'Test Body',
};
final post = Post.fromJson(json);
expect(post.id, 1);
expect(post.title, 'Test Title');
expect(post.body, 'Test Body');
});
});
}
Lesson 16.4 — Testing Repositories with Mocks
dev_dependencies:
mocktail: ^1.0.0
import 'package:mocktail/mocktail.dart';
import 'package:flutter_test/flutter_test.dart';
class MockAuthRemoteDataSource extends Mock implements AuthRemoteDataSource {}
void main() {
late AuthRepositoryImpl repository;
late MockAuthRemoteDataSource mockRemote;
setUp(() {
mockRemote = MockAuthRemoteDataSource();
repository = AuthRepositoryImpl(remote: mockRemote);
});
test('login returns user when remote succeeds', () async {
final user = User(id: '1', email: 'test@example.com');
when(() => mockRemote.login(any(), any())).thenAnswer((_) async => user);
final result = await repository.login('test@example.com', 'password');
expect(result, user);
verify(() => mockRemote.login('test@example.com', 'password')).called(1);
});
test('login throws when remote fails', () async {
when(() => mockRemote.login(any(), any()))
.thenThrow(Exception('Network error'));
expect(
() => repository.login('test@example.com', 'password'),
throwsException,
);
});
}
Lesson 16.5 — Testing State Management (Notifier / Cubit)
test('increment increases the count', () {
final container = ProviderContainer();
addTearDown(container.dispose);
final notifier = container.read(counterProvider.notifier);
expect(container.read(counterProvider), 0);
notifier.increment();
expect(container.read(counterProvider), 1);
});
blocTest<CounterCubit, int>(
'emits [1] when increment is called',
build: () => CounterCubit(),
act: (cubit) => cubit.increment(),
expect: () => [1],
);
Module 3 — Widget Testing
Lesson 16.6 — Widget Tests Fundamentals
testWidgets('Counter increments when button is tapped', (tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterPage()),
);
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
pump vs pumpAndSettle
- pump advances the frame by a small duration.
- pumpAndSettle repeatedly pumps until all animations and microtasks are finished.
Lesson 16.7 — Testing Screens with Dependencies
await tester.pumpWidget(
ProviderScope(
overrides: [
postRepositoryProvider.overrideWithValue(mockRepository),
],
child: const MaterialApp(home: PostListScreen()),
),
);
Module 4 — Integration Testing and Coverage
Lesson 16.8 — Integration Tests
Integration tests run on a real device or emulator and exercise complete user flows using the integration_test package.
Lesson 16.9 — Test Coverage
flutter test --coverage
Level 16 Practical Project — Test a Complete Feature
Choose a feature you have already built and add unit tests for models, repository (mocked), notifier/cubit (success, loading, error), and at least two widget tests for the main screen.
Level 16 Review Questions
- What is the difference between a unit test and a widget test?
- Why do we mock dependencies in unit tests?
- When should you use pump versus pumpAndSettle?
- Why is it important to test error states as well as success states?
- Where should the majority of your tests sit in the testing pyramid?
LEVEL 17 — PERFORMANCE OPTIMIZATION
Level Overview
This level teaches you how to measure performance, identify bottlenecks, and apply practical optimizations.
Module 1 — Understanding Performance in Flutter
Lesson 17.1 — How Flutter Renders Frames
The Pipeline: Build → Layout → Paint → Compositing. A frame should normally complete in under 16 ms (for 60 fps).
What Causes Jank
- Excessive rebuilds of large parts of the tree
- Expensive build methods
- Large synchronous work on the main isolate
- Inefficient list construction
- Decoding large images on the UI thread
Lesson 17.2 — The Performance Overlay and DevTools
MaterialApp(
showPerformanceOverlay: true,
...
)
Flutter DevTools key panels: Performance (timeline, frame chart), CPU Profiler, Memory, Widget rebuild stats, Network.
Module 2 — Controlling Rebuilds
Lesson 17.3 — Why Rebuilds Matter
Techniques
- Keep state as low as possible in the tree.
- Split large widgets into smaller ones.
- Use const constructors wherever possible.
- Use selectors / select in Riverpod or Consumer carefully.
- Avoid creating new objects inside build when used as dependencies.
Lesson 17.4 — The Power of const
// Good
const Text('Hello')
const SizedBox(height: 16)
const Icon(Icons.star)
// Also good
const ProductCard(product: someProduct) // if ProductCard has a const constructor
Lesson 17.5 — Minimizing Rebuild Scope
// Only rebuilds when the count changes, not when other state changes
final count = ref.watch(counterProvider.select((state) => state.count));
Module 3 — List and Grid Performance
Lesson 17.6 — ListView.builder and Lazy Building
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ItemTile(item: items[index]);
},
)
Additional Optimizations
- Provide itemExtent when all items have the same height.
- Use ListView.separated when you need dividers.
- Avoid heavy work inside itemBuilder.
Lesson 17.7 — GridView Performance
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: products.length,
itemBuilder: (context, index) => ProductCard(product: products[index]),
)
Module 4 — Images and Memory
Lesson 17.8 — Image Optimization
Best Practices
- Resize images on the server or at rest when possible.
- Use cacheWidth and cacheHeight with Image.network / Image.asset.
- Prefer a caching package (such as cached_network_image).
- Provide placeholder and error widgets.
Image.network(
imageUrl,
width: 120,
height: 120,
fit: BoxFit.cover,
cacheWidth: 240, // 2x for high-density screens
cacheHeight: 240,
)
Module 5 — Heavy Work and Isolates
Lesson 17.9 — Moving Work Off the Main Isolate
final products = await compute(parseProducts, largeJsonString);
Module 6 — Common Anti-Patterns and Fixes
Lesson 17.10 — Bad Code → Problem → Optimized Code
- Rebuilding a large screen for a small change → Extract the changing part into a separate widget or use a precise selector.
- Creating a new list of widgets on every build → Use ListView.builder / GridView.builder.
- Non-const widgets that never change → Add const constructors.
- Loading full-size images for thumbnails → Use cacheWidth/cacheHeight or server-side resizing.
- Heavy synchronous work on the UI thread → Move to an isolate with compute.
- Listening to an entire large state object → Use select (Riverpod) or equivalent.
Level 17 Practical Project — Optimize a Slow Screen
Measure a baseline with DevTools, identify unnecessary rebuilds, convert lists to builder versions, add const, optimize image loading, move heavy parsing to an isolate, and document the improvement.
Level 17 Review Questions
- Why does const help performance?
- What is the main advantage of ListView.builder over a plain ListView?
- When should you consider using an isolate?
- What tool do you use to inspect long frames and rebuilds?
- Why is it dangerous to load a full-resolution photo into a small thumbnail widget?
LEVEL 18 — PACKAGES AND PLUGINS
Level Overview
This level teaches how the pub ecosystem works, how to manage dependencies, evaluate package quality, and create your own reusable packages and plugins.
Module 1 — The Package Ecosystem
Lesson 18.1 — What Are Packages and Where Do They Come From?
Types of Packages
- Pure Dart packages (work everywhere Dart runs)
- Flutter packages (depend on Flutter APIs)
- Plugins (Flutter packages that include native Android/iOS/web/desktop code)
Lesson 18.2 — pubspec.yaml in Depth
name: my_app
description: A professional Flutter application
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
flutter_riverpod: ^2.5.0
shared_preferences: ^2.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
mocktail: ^1.0.0
build_runner: ^2.4.0
flutter:
uses-material-design: true
assets:
- assets/images/
fonts:
- family: Inter
fonts:
- asset: assets/fonts/Inter-Regular.ttf
Module 2 — Versioning and Dependency Management
Lesson 18.3 — Semantic Versioning
Format: MAJOR.MINOR.PATCH (breaking / new features / bug fixes).
http: ^1.2.0 # >=1.2.0 <2.0.0
http: >=1.2.0 <1.4.0
http: 1.2.1 # exact version (rarely recommended)
Lesson 18.4 — Resolving Dependency Conflicts
flutter pub get
flutter pub outdated
flutter pub deps
Use dependency_overrides only as a temporary last resort.
Lesson 18.5 — Evaluating Package Quality
Before adding a package, examine: popularity, pub points, maintenance, documentation, null safety, platform support, license, and security.
Module 3 — Creating Your Own Packages
Lesson 18.6 — Creating a Reusable Dart / Flutter Package
flutter create --template=package my_utils
my_utils/
├── lib/
│ └── my_utils.dart # main export file
├── test/
├── pubspec.yaml
└── README.md
// lib/my_utils.dart
library my_utils;
export 'src/string_extensions.dart';
export 'src/date_helpers.dart';
export 'src/validators.dart';
Lesson 18.7 — Local Path Dependencies
dependencies:
my_utils:
path: ../my_utils
Lesson 18.8 — Introduction to Plugins and Platform Channels
A plugin is a Flutter package that includes native code and communicates with Dart through platform channels: Dart ↔ MethodChannel / EventChannel ↔ Native code.
Level 18 Practical Project — Create a Small Utility Package
Create a reusable package called app_validators containing common validation functions with tests and a clear README.
Level 18 Review Questions
- What is the difference between dependencies and dev_dependencies?
- What does the caret (^) symbol mean in a version constraint?
- Why should you be careful with dependency_overrides?
- Name at least four things you should check before adding a package to a production project.
- When does it make sense to create your own package instead of copying code between projects?
LEVEL 19 — DEVICE FEATURES
Level Overview
This level teaches how to integrate common device features: permissions, camera, location, maps, notifications, biometrics, and file access.
Module 1 — Permissions Foundation
Lesson 19.1 — Runtime Permissions
Best Practices
- Request permissions just before they are needed (not at app start).
- Explain why you need the permission before the system dialog appears.
- Provide a clear fallback when permission is denied.
- Guide the user to app settings when the permission has been permanently denied.
Common Package: permission_handler.
Module 2 — Camera and Images
Lesson 19.2 — Camera and Gallery
Common Package: image_picker.
final ImagePicker picker = ImagePicker();
// From camera
final XFile? photo = await picker.pickImage(
source: ImageSource.camera,
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 85,
);
// From gallery
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
);
Platform Configuration
- Android: camera and storage permissions in AndroidManifest.xml.
- iOS: NSCameraUsageDescription and NSPhotoLibraryUsageDescription in Info.plist.
Module 3 — Location and Maps
Lesson 19.3 — GPS and Current Location
Common Package: geolocator.
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.whileInUse ||
permission == LocationPermission.always) {
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
print('${position.latitude}, ${position.longitude}');
}
Lesson 19.4 — Displaying Maps
Common Package: google_maps_flutter (requires API keys and platform setup).
Module 4 — Notifications
Lesson 19.5 — Local Notifications
Common Package: flutter_local_notifications.
Key Topics to Master: initialization, requesting notification permission, scheduling, handling taps, notification channels on Android.
Module 5 — Biometrics and Secure Access
Lesson 19.6 — Biometric Authentication
Common Package: local_auth. Always provide a fallback (PIN / password) when biometrics fail or are unavailable.
Module 6 — Files and Storage
Lesson 19.7 — File Picking and Device Storage
Common Packages: file_picker, path_provider, open_filex.
Level 19 Practical Project — Feature-Rich “Add Memory” Flow
Combine camera/gallery permission handling, location, notes, local save, and a confirmation notification into one coherent flow.
Level 19 Review Questions
- Why should permissions be requested just-in-time instead of at app startup?
- What is the difference between ImageSource.camera and ImageSource.gallery?
- Name three location-permission states you must handle.
- Why is it important to declare usage descriptions in Info.plist on iOS?
- When would you choose local notifications over push notifications?
LEVEL 20 — FLUTTER WEB AND DESKTOP
Level Overview
Flutter officially supports web, Windows, macOS, and Linux from a single codebase. This level teaches you how to adapt Flutter applications for web and desktop environments.
Module 1 — Platform Overview
Lesson 20.1 — Flutter’s Multi-Platform Support
Supported platforms: Android, iOS, Web (Chrome, Firefox, Safari, Edge), Windows, macOS, Linux. Web can use HTML or CanvasKit (WebAssembly + Skia) renderers.
Lesson 20.2 — Enabling and Running on Each Platform
# Web
flutter create . --platforms=web
flutter run -d chrome
flutter build web
# Windows
flutter create . --platforms=windows
flutter run -d windows
flutter build windows
# macOS
flutter create . --platforms=macos
flutter run -d macos
flutter build macos
# Linux
flutter create . --platforms=linux
flutter run -d linux
flutter build linux
Module 2 — Platform Detection and Conditional Code
Lesson 20.3 — Detecting the Current Platform
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io' show Platform;
if (kIsWeb) {
// Web-specific code
}
if (!kIsWeb && Platform.isWindows) {
// Windows-only code
}
Best Practice
Create small abstraction layers instead of scattering if (Platform.is...) checks throughout the UI.
Module 3 — Responsive Desktop and Web Layouts
Lesson 20.4 — Designing for Large Screens
if (constraints.maxWidth >= 1200) {
return DesktopLayout();
} else if (constraints.maxWidth >= 800) {
return TabletLayout();
} else {
return MobileLayout();
}
Lesson 20.5 — Navigation Patterns by Platform
Module 4 — Input Methods
Lesson 20.6 — Keyboard and Mouse
Support common shortcuts, use Shortcuts/Actions/Focus widgets, ensure hover states, right-click context menus where expected, and natural mouse wheel scrolling.
Module 5 — Platform-Specific Considerations
Lesson 20.7 — Flutter Web Specifics
Limitations: larger initial download, some plugins lack web support, limited SEO, browser API restrictions, restricted file system access.
Lesson 20.8 — Desktop Specifics (Windows, macOS, Linux)
Consider window management, native look and feel, file system integrations, and distribution (MSIX, notarized .app, AppImage/Snap/Flatpak).
Level 20 Practical Project — Multi-Platform Notes or Dashboard
Adapt an existing app for web and desktop with responsive navigation, keyboard shortcuts, and guarded dart:io usage.
Level 20 Review Questions
- Why must you guard dart:io usage with kIsWeb?
- What navigation pattern is generally preferred on desktop instead of a bottom navigation bar?
- Name two limitations you should consider when targeting Flutter web.
- Why are keyboard shortcuts important on desktop and web?
- What is the benefit of using a breakpoint-based layout system across mobile, tablet, and desktop?
LEVEL 21 — APP DEPLOYMENT
Level Overview
This level teaches you how to deploy Flutter applications to Android (Google Play), iOS (App Store), and the web.
Module 1 — Versioning and Release Preparation
Lesson 21.1 — Versioning
version: 1.2.3+45
1.2.3 is the version name; 45 is the version code / build number (must increase with every store upload).
Lesson 21.2 — Debug vs Release Builds
flutter build appbundle # Android (preferred)
flutter build apk --release # Android APK
flutter build ipa # iOS (on macOS)
flutter build web # Web
Module 2 — Android Deployment
Lesson 21.3 — App Signing and Keystore
keytool -genkey -v -keystore upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
Best Practices
- Keep secure backups of the keystore and passwords.
- Prefer Google Play App Signing.
- Never share the keystore in chat or commit it to source control.
Lesson 21.4 — APK vs Android App Bundle
Android App Bundle (AAB) is the required format for new apps on Google Play.
flutter build appbundle
Lesson 21.5 — Google Play Console
Main Steps: create a developer account, create the app, complete the store listing, set category/contact/privacy policy, complete content rating, upload the App Bundle on a release track, roll out.
Important Assets: high-resolution icon, feature graphic, phone/tablet screenshots, privacy policy.
Module 3 — iOS Deployment
Lesson 21.6 — Certificates, Identifiers, and Provisioning Profiles
Core concepts: Apple Developer Account, App ID / Bundle Identifier, Certificate, Provisioning Profile.
High-Level Flow: register Bundle ID, create/let Xcode manage certificates, open ios/Runner.xcworkspace, select team, enable automatic signing, set deployment target and version/build numbers.
Lesson 21.7 — TestFlight and App Store
Upload a build through Xcode or Transporter, invite testers via TestFlight, then create the App Store Connect record, fill metadata/screenshots, upload the build, and submit for review.
Common Rejection Reasons: incomplete metadata, missing privacy policy/labels, crashes on launch, placeholder content, guideline violations.
Module 4 — Web Deployment
Lesson 21.8 — Building and Hosting Flutter Web
flutter build web
Output is in build/web/ — static files hostable anywhere (Firebase Hosting, Netlify, Vercel, GitHub Pages, S3 + CloudFront, etc.).
Level 21 Practical Checklist — First Production Release
Android: version updated, keystore backed up, signing configured, appbundle builds, store listing complete, privacy policy published, content rating done, testing track verified, production release prepared.
iOS: developer account active, Bundle ID registered, signing works, release build tested on device, App Store Connect record created, screenshots/metadata ready, privacy labels complete, TestFlight tested, submitted for review.
Web: build succeeds, hosted on HTTPS, base href/routing verified, performance checked, custom domain configured.
Level 21 Review Questions
- What is the difference between version name and version code?
- Why is an Android App Bundle preferred over an APK for Google Play?
- What is the main risk of losing your Android upload keystore?
- What is TestFlight used for?
- Name three items that are commonly required in a store listing.
LEVEL 22 — CI/CD
Level Overview
This level teaches the practical side of CI/CD for Flutter projects: Git habits, branching, pull requests, code review, and GitHub Actions workflows.
Module 1 — Professional Git Workflow
Lesson 22.1 — Git Fundamentals for Teams
git status
git add .
git commit -m "Add login validation and error handling"
git push origin feature/login-validation
git pull origin main
Lesson 22.2 — Branching Strategy
- main — always stable and deployable
- develop (optional) — integration branch
- feature/*, bugfix/*, release/*, hotfix/*
Lesson 22.3 — Pull Requests and Code Review
A good pull request has a clear title/description, screenshots for UI changes, testing notes, and a linked issue. Review for correctness, readability, architecture consistency, and test coverage.
Module 2 — Continuous Integration with GitHub Actions
Lesson 22.4 — What Is CI/CD?
CI — Automatically building and testing on every push/PR. CD — Automatically creating release artifacts and deploying them.
Lesson 22.5 — GitHub Actions Core Concepts
Workflow (YAML in .github/workflows/), Trigger (on:), Job, Step, Runner, Secret.
Lesson 22.6 — Basic Flutter CI Workflow
name: Flutter CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0' # use a stable version
channel: 'stable'
- name: Install dependencies
run: flutter pub get
- name: Analyze code
run: flutter analyze
- name: Run tests
run: flutter test
Lesson 22.7 — Adding Builds to CI
build-android:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
- run: flutter pub get
- run: flutter build appbundle
Module 3 — Secrets and Configuration
Lesson 22.8 — Managing Secrets
Store API keys, keystore passwords, service account JSON, certificates, and tokens in the repository’s Settings → Secrets and variables → Actions, referenced as ${{ secrets.MY_SECRET_NAME }}. Never print secrets in logs or commit them.
Lesson 22.9 — Environment Variables and Flavors
Pass environment variables into the CI workflow and combine with Flutter flavors or --dart-define for dev/staging/production builds.
Level 22 Practical Project — Add CI to Your App
Create .github/workflows/flutter-ci.yml, run on push/PR to main, install Flutter, run flutter pub get, flutter analyze, flutter test, and optionally build web/Android artifacts.
Level 22 Review Questions
- What is the main purpose of Continuous Integration?
- Why should main remain stable and deployable?
- Where do you store sensitive values such as keystore passwords when using GitHub Actions?
- What does a failing CI check tell you about a pull request?
- Why is it better to run tests automatically on every pull request instead of only testing manually?
LEVEL 23 — AI + FLUTTER DEVELOPMENT
Level Overview
This level covers how you can use AI to become a more productive Flutter developer, and how to build Flutter applications that integrate large language models (LLMs) and other AI services.
Module 1 — AI as a Development Assistant
Lesson 23.1 — AI-Assisted Coding and Debugging
What AI Can Help With: generating boilerplate, explaining errors, suggesting refactorings, writing test cases, drafting documentation.
Best Practices: never paste secrets into AI tools, prefer small focused requests, always review and test generated code.
Lesson 23.2 — Prompting for Better Code Results
Good prompts include a specific goal, relevant constraints, existing code context, and expected behavior. Example: “Create a Flutter login form using TextFormField, Form, and validation. Use Riverpod for state. Show loading and error states. Target Flutter 3.24 and Material 3.”
Module 2 — Integrating AI into Flutter Applications
Lesson 23.3 — Calling LLM APIs
Typical Flow: user enters a prompt → app sends prompt (plus history) to the API → API returns a response → app displays and stores it.
Security Note: Never embed secret API keys directly in the client when the key can be extracted. Prefer a backend proxy for production apps.
Lesson 23.4 — Prompt Engineering for Applications
System prompt defines role/tone/boundaries; user prompt is the actual request; conversation history maintains context (mind token limits).
Lesson 23.5 — Streaming Responses
Streaming displays tokens as they arrive for a more responsive experience. Show a typing indicator, allow the user to stop generation, and handle connection interruptions cleanly.
Module 3 — Building a Chat Interface
Lesson 23.6 — Core Chat UI Elements
Message list, text input field, send button, loading/streaming indicator, optional clear/copy/regenerate actions.
Lesson 23.7 — Message Model and History
enum MessageRole { user, assistant, system }
class ChatMessage {
final MessageRole role;
final String content;
final DateTime timestamp;
ChatMessage({
required this.role,
required this.content,
DateTime? timestamp,
}) : timestamp = timestamp ?? DateTime.now();
}
Module 4 — Security and Production Concerns
Lesson 23.8 — Protecting API Keys and Data
Use a backend proxy that holds the real API key, apply authentication, rate-limit requests, monitor usage/costs, and avoid sending unnecessary sensitive data to AI APIs.
Level 23 Practical Project — AI Chat Application
Build a complete AI chat application with a clean chat UI, conversation history, LLM API integration (directly or via proxy), loading/error states, and optional streaming.
Level 23 Review Questions
- Why should secret API keys not be stored directly in a Flutter client for production apps?
- What is the benefit of streaming responses in a chat interface?
- What information should a good coding prompt include?
- Why is conversation history important when calling an LLM?
- Name two security or privacy concerns when adding generative AI features to a mobile app.
LEVEL 24 — PROFESSIONAL PROJECTS
Level Overview
This level guides you through progressive real-world projects, from beginner fundamentals to a production-style capstone.
Module 1 — Beginner Projects
Project 24.1 — Calculator: basic arithmetic, clear/delete, responsive buttons, running total display. Focus: layout, gesture handling, simple state.
Project 24.2 — To-Do List: add/edit/complete/delete tasks, persist locally, filter, empty state. Focus: lists, forms, local storage, state management.
Project 24.3 — Quiz App: multiple questions, score tracking, results screen, restart. Focus: navigation, state across screens.
Project 24.4 — Expense Tracker (Basic): add expenses, list, total calculation, local persistence. Focus: forms, models, lists, aggregation.
Module 2 — Intermediate Projects
Project 24.5 — Weather App: fetch by city/location, loading/error/empty states. Focus: API integration, error handling.
Project 24.6 — News App: article list, detail screen, pull-to-refresh, search/filter, pagination. Focus: REST APIs, repository pattern.
Project 24.7 — Recipe App: browse, search/filter, detail with ingredients/steps, local favorites. Focus: combined remote + local data.
Project 24.8 — Notes App (Enhanced): full CRUD, categories, search, soft delete/trash, offline-first. Focus: local database, offline UX.
Project 24.9 — E-Commerce Product Browser: product list/detail, categories, local cart, simple checkout. Focus: complex state, clean models.
Module 3 — Advanced Projects
Project 24.10 — Chat Application: auth, conversation list, real-time messages, sending states. Focus: streams, complex UI state.
Project 24.11 — Social Media Style Feed: auth, feed, create post (text + image), like/comment, profile. Focus: Firestore + Storage, pagination.
Project 24.12 — Food Delivery Style App: menu browsing, cart, order placement, order history, status flow. Focus: multi-feature architecture.
Project 24.13 — Booking Application: browse services, date/time selection, booking form/history, auth. Focus: forms, validation, domain modeling.
Project 24.14 — Learning Management System (Simplified): courses, lessons, progress tracking, quiz, auth. Focus: structured content, progress persistence.
Module 4 — Professional Capstone Project
Project 24.15 — Production-Style Capstone
Suggested Product Ideas: personal finance tracker, task/project management app, content app, marketplace-style app, health or habit tracker.
Functional Requirements: authentication (email/password + one social provider), user profile, core CRUD + business logic, search/filtering, pagination, image upload, local caching/offline support, push or local notifications, settings.
Technical Requirements: clean architecture, repository pattern, professional state management, proper error/loading/empty states, form validation, secure token/session handling, unit tests, widget tests, responsive UI, performance-conscious lists/images.
Architecture Expectations: feature-first structure, domain entities/repository interfaces, data layer (remote + local), presentation layer, dependency injection.
Security: secure token storage, HTTPS only, input validation, protected routes.
Testing: unit tests for repositories/core logic, widget tests for critical UI, clear instructions to run tests.
Deployment Readiness: versioning, release build documentation, store listing assets or web hosting plan, privacy policy draft.
Implementation Plan
- Define requirements and user flows.
- Design data models and architecture.
- Set up project structure and dependency injection.
- Implement authentication.
- Build core feature vertical slices.
- Add offline/caching support.
- Add search, filtering, and pagination.
- Implement profile and settings.
- Write tests for critical paths.
- Polish UI, error handling, and performance.
- Prepare release builds and deployment documentation.
Level 24 Review Questions
- Why is it better to build progressive projects instead of jumping straight to a large app?
- What makes a project “portfolio quality”?
- Why should the capstone include both authentication and offline support?
- What is the value of defining architecture before writing most of the UI code?
- Name three non-UI elements that reviewers often look for in a professional Flutter project.
LEVEL 25 — FLUTTER BEST PRACTICES
Level Overview
This level consolidates the most important Flutter best practices, each explained with reasoning, common mistakes, and good vs bad examples.
Module 1 — Clean Code and Naming
Lesson 25.1 — Clean Code Principles
Bad Example
Widget build(BuildContext c) {
return x ? Container(child: Text(a), color: b ? Colors.red : Colors.blue) : Container();
}
Good Example
Widget build(BuildContext context) {
if (!isVisible) return const SizedBox.shrink();
return ColoredBox(
color: isError ? Colors.red : Colors.blue,
child: Text(title),
);
}
Lesson 25.2 — Naming Conventions
- Files and directories: snake_case
- Classes, enums, typedefs: PascalCase
- Variables, functions, parameters: camelCase
- Constants: camelCase or SCREAMING_SNAKE_CASE
- Private members: leading underscore (_controller)
Best Practice: If you need a comment to explain what a variable is for, the name is probably not good enough.
Module 2 — Widgets and UI Structure
Lesson 25.3 — Reusable Widgets
Extract a widget when: the same UI pattern appears more than once, a build method becomes hard to read, or a part of the UI has its own state.
Lesson 25.4 — Keep Build Methods Readable
Extract sections such as _Header(), _ProductInfo(), _ActionButtons() even if only used once.
Module 3 — State Management and Architecture
Lesson 25.5 — Choose State Management Deliberately
Local ephemeral state → setState is fine. Shared or complex state → Riverpod, BLoC, or equivalent. Avoid mixing too many different approaches randomly.
Lesson 25.6 — Architecture Discipline
UI should not talk directly to raw HTTP clients or database APIs; business rules belong outside widgets; repositories hide data sources.
Module 4 — Errors, Security, and Resilience
Lesson 25.7 — Error Handling
Never leave empty catch blocks. Map low-level exceptions into user-friendly messages. Show retry options. Log unexpected errors.
Lesson 25.8 — Security Basics
Use HTTPS everywhere, store tokens in secure storage, validate input, don’t embed secrets in the client, request only needed permissions.
Module 5 — Accessibility, Responsiveness, and Performance
Lesson 25.9 — Accessibility
Provide semantic labels, ensure sufficient contrast, support system text scaling, make interactive targets large enough.
Lesson 25.10 — Responsive Design
Design for different screen sizes from the beginning using breakpoints, flexible grids, and adaptive navigation.
Lesson 25.11 — Performance Habits
Use const whenever possible, prefer builder constructors, avoid heavy work in build, resize/cache images, measure before optimizing.
Module 6 — Collaboration and Maintainability
Lesson 25.12 — Documentation
Document why a non-obvious decision was made, public APIs, setup instructions, and environment configuration.
Lesson 25.13 — Git and Code Review Habits
Small, focused commits; clear commit messages; no secrets in history. Review for correctness, readability, architecture, and tests.
Level 25 Practical Exercise — Best-Practice Audit
Audit an existing project against naming, widget size, state-management consistency, architecture boundaries, error/empty states, security basics, accessibility/responsiveness, performance hotspots, documentation, and test coverage. Apply at least five concrete improvements.
Level 25 Review Questions
- Why is consistency in state management more important than always using the newest library?
- When should you extract a new widget?
- What are the four UI states that most data-driven screens should handle?
- Why should tokens not be stored in SharedPreferences?
- Name three habits that improve maintainability for future teammates.
LEVEL 26 — COMMON FLUTTER MISTAKES
Level Overview
A practical troubleshooting reference. Each entry follows: the problem, why it happens, incorrect code, correct code, the solution, and how to prevent it.
Module 1 — BuildContext and Lifecycle Mistakes
Mistake 26.1 — Using BuildContext Across an Async Gap
Incorrect
onPressed: () async {
await Future.delayed(const Duration(seconds: 2));
Navigator.of(context).pop(); // unsafe if widget is gone
}
Correct
onPressed: () async {
await Future.delayed(const Duration(seconds: 2));
if (!context.mounted) return;
Navigator.of(context).pop();
}
Prevention: Treat every await as a potential point where the widget may disappear.
Mistake 26.2 — Calling setState After Dispose
Correct
Timer.periodic(const Duration(seconds: 1), (_) {
if (!mounted) return;
setState(() => _seconds++);
});
Prevention: Pair every resource created in initState with cleanup in dispose.
Module 2 — Async and Null-Safety Mistakes
Mistake 26.3 — Forgetting to Handle Async Errors
Correct
try {
final products = await api.fetchProducts();
// update state with data
} catch (e) {
// update state with error message
}
Mistake 26.4 — Misusing the Null Assertion Operator (!)
Correct
final name = user?.name ?? 'Guest';
// or
if (user == null) return;
final name = user.name;
Prevention: Treat ! as a code smell that needs justification.
Module 3 — Memory and Resource Leaks
Mistake 26.5 — Missing dispose() for Controllers
Correct
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Module 4 — Performance-Related Mistakes
Mistake 26.6 — Using ListView with a Large Children List
Correct
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemTile(item: items[index]),
)
Mistake 26.7 — Unnecessary Rebuilds of Large Subtrees
Solution: Move state closer to where it is used, extract widgets, use const constructors, use selectors.
Module 5 — Architecture and State Mistakes
Mistake 26.8 — Putting Business Logic and API Calls Directly in Widgets
Solution: Move logic into notifiers/cubits/use cases and data access into repositories.
Mistake 26.9 — Overusing Global / God State
Solution: Prefer feature-scoped state and clear ownership boundaries.
Module 6 — Networking and Package Mistakes
Mistake 26.10 — Ignoring HTTP Status Codes
Solution: Check status codes and handle 4xx/5xx explicitly.
Mistake 26.11 — Dependency Conflicts and Careless Overrides
Solution: Inspect flutter pub deps and flutter pub outdated, upgrade carefully, treat overrides as temporary.
Module 7 — Build and Release Mistakes
Mistake 26.12 — Android Release Signing Problems
Solution: Verify key.properties, Gradle signing config, and version codes. Keep secure backups of the keystore.
Mistake 26.13 — iOS Code Signing and Provisioning Errors
Solution: Align Bundle ID, team, and signing settings. Prefer automatic signing and renew certificates on time.
Mistake 26.14 — Testing Only in Debug Mode
Solution: Regularly test release builds on real devices.
Level 26 Practical Exercise — Debugging Drill
Take an existing project or intentionally introduce 5 of the mistakes above, reproduce the symptoms, fix each issue, and write a short root cause → fix → prevention note for each.
Level 26 Review Questions
- Why is using BuildContext after an await potentially unsafe?
- What is the most common cause of setState() called after dispose()?
- Why is ListView.builder preferred for long lists?
- What is the danger of the null assertion operator (!)?
- Why should release builds be tested before store submission?
LEVEL 27 — FLUTTER DEVELOPER ROADMAP
Level Overview
This level defines a practical path from absolute beginner to senior Flutter developer across seven stages.
Stage 1 — Absolute Beginner
Goal: Become comfortable with the basic tools and building UIs with code.
Recommended Projects: Hello World variations, static profile card, basic counter app.
Assessment: You can run an app without help and understand what main(), runApp(), and MaterialApp do.
Stage 2 — Dart Beginner
Goal: Write correct, confident Dart code without Flutter UI complexity.
Recommended Projects: Dart-only console exercises, simple data models (User, Product, Note).
Assessment: You can create models with fromJson manually and use async/await without confusion.
Stage 3 — Flutter Beginner
Goal: Build multi-screen UIs with standard widgets and basic navigation.
Recommended Projects: To-Do list, quiz app, simple notes app, basic expense tracker.
Assessment: You can implement basic navigation and form validation, and persist simple data locally.
Stage 4 — Intermediate Flutter Developer
Goal: Build data-driven applications that consume APIs, manage state professionally, and handle real UI states.
Recommended Projects: Weather app, news/product listing app, notes app with local database, auth + profile + home flow.
Assessment: You can implement loading/error/empty states consistently and use a real state-management solution beyond setState.
Stage 5 — Advanced Flutter Developer
Goal: Build scalable features with solid architecture, tests, performance awareness, and advanced UI.
Recommended Projects: Chat or social-style app, e-commerce flow with cart and auth, offline-first app, feature-rich capstone.
Assessment: You can explain and apply architecture boundaries, write meaningful automated tests, and diagnose performance issues.
Stage 6 — Professional Flutter Developer
Goal: Deliver production-ready applications and work effectively in a team environment.
Recommended Projects: Full production-style capstone, real or simulated client project, team codebase contributions.
Assessment: You have shipped at least one real release build and your code is understandable to other developers.
Stage 7 — Senior Flutter Developer
Goal: Lead technical decisions, raise team quality, and design systems that remain healthy over time.
Recommended Work: Leading a non-trivial application or major module, establishing architecture guidelines, mentoring, driving quality improvements.
Assessment: Others trust your technical judgment and you leave codebases healthier than you found them.
How to Use This Roadmap
- Identify your current stage based on what you can build without tutorials.
- Focus on the skills and projects of that stage until the assessment criteria feel honest.
- Move forward when you can consistently perform the practical abilities of the stage.
- Revisit earlier stages whenever weaknesses appear (this is normal and healthy).
Important Reality Check: Completing a course or copying projects does not automatically move you to the next stage. Ability to build, debug, and explain independently is the real measure.
Level 27 Review Questions
- What is the main difference between an intermediate and an advanced Flutter developer in this roadmap?
- Why is shipping a release build an important professional milestone?
- How should you decide whether you are ready for the next stage?
- Why is architecture more important at advanced stages than at beginner stages?
- What distinguishes a senior developer from a strong individual contributor?
LEVEL 28 — EXERCISES AND ASSESSMENTS
Level Overview
This level provides concrete exercises and assessments covering the major areas of the course, including multiple-choice, short-answer, coding exercises, debugging problems, and practical assignments.
Section 28.1 — Multiple-Choice Questions
- What is the main purpose of a StatefulWidget? A. To create widgets that never change B. To hold mutable state that can update the UI C. To replace MaterialApp D. To manage navigation only
- Which widget should you prefer for long dynamic lists? A. Column B. ListView C. ListView.builder D. SingleChildScrollView
- What does context.mounted help prevent? A. Null safety errors B. Using a BuildContext after the widget has been disposed C. Package conflicts D. Slow animations
- Which storage option is most appropriate for refresh tokens? A. SharedPreferences B. Plain text file C. flutter_secure_storage D. Global variables
- In Clean Architecture applied to Flutter, which layer should contain pure business rules with no Flutter imports? A. Presentation B. Data C. Domain D. UI widgets
- What is the primary benefit of const widgets? A. They enable hot reload B. They can reduce unnecessary rebuilds C. They replace state management D. They are required for API calls
- Which HTTP status code range usually indicates a client error? A. 1xx B. 2xx C. 4xx D. 5xx
- What is the purpose of a repository in a Flutter application? A. To draw custom graphics B. To act as a single source of truth for data access C. To replace themes D. To manage only animations
- Which command produces the preferred Google Play upload format? A. flutter build apk B. flutter build appbundle C. flutter build ios D. flutter build web
- Why should heavy JSON parsing of large data often be moved to an isolate? A. Isolates are required for all API calls B. To keep the UI thread responsive C. Because JSON cannot be parsed on the main isolate D. To reduce package size
Answer Key (28.1): 1-B, 2-C, 3-B, 4-C, 5-C, 6-B, 7-C, 8-B, 9-B, 10-B
Section 28.2 — Short-Answer Questions
Answer in 2–6 sentences.
- Explain the difference between authentication and authorization.
- Why is ListView.builder generally preferred over ListView(children: …) for large data sets?
- What problem does the repository pattern solve?
- Why is it unsafe to store access tokens in SharedPreferences?
- What is the difference between implicit and explicit animations in Flutter?
- Why should you check context.mounted after an await before using BuildContext?
- Name three UI states that a data-driven screen should normally handle.
- What is the role of dispose() in a StatefulWidget?
Sample Guidance: Strong answers are precise, use correct terminology, and show practical understanding rather than repeating definitions word-for-word.
Section 28.3 — Coding Exercises
Exercise 1 — Model + Parsing: Create a Product class with id, title, price, and optional imageUrl. Implement fromJson/toJson and a unit test.
Exercise 2 — Form Validation: Build a login form with email/password validation, a loading indicator on submit, and a disabled button while loading.
Exercise 3 — Repository Interface: Define an abstract NotesRepository with getNotes/createNote/updateNote/deleteNote, then a fake in-memory implementation.
Exercise 4 — Responsive Layout: Create a screen with bottom navigation on narrow screens and NavigationRail on wider screens.
Exercise 5 — Async State Handling: Implement a data loader exposing loading/success/error states for a list fetched from a fake repository.
Section 28.4 — Debugging Exercises
Debug 1
onPressed: () async {
final data = await api.fetchData();
setState(() => items = data);
}
Debug 2
ListView(
children: products.map((p) => ProductTile(product: p)).toList(),
)
Debug 3
final name = user!.name.toUpperCase();
Debug 4: A screen uses a TextEditingController but never disposes of it — show the correct lifecycle handling.
Expected Focus Areas: Mounted checks, builder lists, null safety, and disposal.
Section 28.5 — Practical Assignments
Assignment A — API Feature: Fetch a list, show loading/error/success, open a detail screen, use a repository and state management.
Assignment B — Offline Notes Slice: Create, list, edit, soft delete, and search using local persistence.
Assignment C — Auth Flow: Login, registration, session restoration, protected home screen, logout, secure token storage.
Assignment D — Best-Practice Refactor: Improve an older project in at least five areas: naming, widget extraction, state boundaries, error handling, const usage, repository extraction, or tests.
Section 28.6 — Advanced Challenges
- Add pagination or infinite scrolling to an API list without breaking loading and error handling.
- Implement a theme switch (light/dark) that persists across app restarts.
- Write unit tests for a repository that depends on both a remote data source and a local cache. Mock the dependencies.
- Profile a janky screen with DevTools and apply at least two measurable optimizations.
- Design a feature-first folder structure for an app with Auth, Products, Cart, and Profile. Explain what belongs in each layer.
Section 28.7 — Self-Assessment Checklist
Rate yourself honestly (Needs work / Adequate / Strong) on: Dart null safety and async; layout and responsive UI; forms and validation; state management beyond setState; API integration and error handling; local storage / offline support; authentication flows; architecture and repositories; unit and widget testing; performance basics; Git + CI basics; release and deployment awareness.
Use weak areas as a focused review plan before Level 29.
LEVEL 29 — FINAL PROFESSIONAL CERTIFICATION TEST
Level Overview
This final examination evaluates whether you can apply the knowledge and skills from the entire course at a professional level.
Examination Structure
- Part 1 — Multiple-Choice Questions (50)
- Part 2 — Technical Questions (20)
- Part 3 — Debugging Problems (10)
- Part 4 — Coding Challenges (5)
- Part 5 — Professional Capstone Project + Rubric
Recommended Time Guidance: Part 1: 60–75 min · Part 2: 60–90 min · Part 3: 60–90 min · Part 4: 3–6 hours · Part 5: multi-day project work.
PART 1 — Multiple-Choice Questions (50)
- What is the primary role of the Flutter Engine? A. Define Material widgets B. Handle low-level rendering, text layout, and platform channels C. Manage pubspec dependencies D. Replace the Dart runtime
- Which statement about StatelessWidget is correct? A. It can call setState B. It is immutable and depends only on configuration and inherited data C. It must always have a controller D. It cannot be used in production apps
- What does Hot Reload preserve that Hot Restart does not? A. App state in most cases B. Native platform code C. pubspec.lock D. AndroidManifest permissions
- Which is the safest way to store authentication tokens on device? A. SharedPreferences B. Global singleton variables C. flutter_secure_storage D. Hard-coded constants
- What is the main advantage of ListView.builder? A. It supports only static children B. It lazily builds children that are visible C. It replaces GridView D. It disables scrolling
- In null safety, what does String? mean? A. The variable can never be null B. The variable may be null C. The variable is a list of strings D. The variable is deprecated
- Which lifecycle method is best for creating controllers and one-time setup? A. build B. dispose C. initState D. deactivate
- What should you do before using BuildContext after an await? A. Call setState B. Check context.mounted C. Force a rebuild D. Disable null safety
- Which HTTP method is typically used to create a new resource? A. GET B. POST C. DELETE D. HEAD
- What does a repository typically hide from the UI layer? A. Theme colors B. Data source details (API, database, cache) C. Animation controllers D. File names of widgets
- Which state-management solution is built around providers and compile-time safety? A. setState only B. Riverpod C. Raw InheritedWidget only D. CSS
- What is the purpose of dispose()? A. To create new widgets B. To clean up controllers, timers, and subscriptions C. To compile the app D. To publish packages
- Which widget is most appropriate for grouping form fields and validating them together? A. Container B. Form C. Stack D. Spacer
- What does ColorScheme.fromSeed help you generate? A. Database tables B. A harmonious set of theme colors C. API endpoints D. Test coverage reports
- Which platform file commonly stores iOS permission usage descriptions? A. AndroidManifest.xml B. Info.plist C. pubspec.yaml D. build.gradle
- What is the preferred upload format for new apps on Google Play? A. APK only B. Android App Bundle (AAB) C. IPA D. ZIP of Dart sources
- What is a common cause of setState() called after dispose()? A. Using const widgets B. Async callbacks completing after the widget is removed C. Too many fonts D. Missing MaterialApp
- Which of the following is a domain-layer responsibility in Clean Architecture? A. Drawing pixels B. Business rules and entities C. HTTP header formatting only D. Store screenshots
- What is the main purpose of unit tests? A. To replace design B. To verify individual classes/functions in isolation C. To deploy the app D. To create animations
- Why move heavy JSON parsing of large payloads to an isolate? A. Isolates are required by the compiler B. To avoid blocking the UI thread C. Because JSON is illegal on the main isolate D. To reduce image size
- Which widget creates a shared-element transition between routes? A. AnimatedContainer B. Hero C. Placeholder D. Divider
- What does semantic versioning ^1.4.2 generally allow? A. Any version including 2.0.0 B. Compatible versions >=1.4.2 <2.0.0 C. Only version 1.4.2 exactly D. Only pre-release versions
- Which practice helps prevent unnecessary rebuilds? A. Using const widgets and narrowing state scope B. Calling setState as often as possible C. Putting all state in one global object D. Avoiding keys forever
- What is the role of Firestore security rules? A. Style text fields B. Control who can read/write data C. Generate screenshots D. Replace versioning
- Which of the following is an example of authorization rather than authentication? A. Logging in with email and password B. Verifying a fingerprint C. Checking whether a logged-in user can delete another user’s post D. Resetting a password
- What is the safest default assumption about network requests? A. They always succeed B. They can fail, time out, or return errors C. They never need loading states D. They replace local storage completely
- Which tool is most useful for diagnosing jank and rebuild problems? A. Flutter DevTools B. Only print statements C. Only pubspec.yaml D. Only the App Store
- What is a soft delete? A. Permanently removing a row immediately B. Marking a record as deleted while retaining recovery possibility C. Deleting the keystore D. Removing a package
- Which navigation pattern is generally better for desktop widths than a bottom navigation bar? A. NavigationRail or side menu B. Only PopupMenuButton C. Only SnackBar D. Only CircularProgressIndicator
- What should a professional API-driven screen usually handle? A. Only success data B. Loading, success, empty, and error states C. Only dark mode D. Only animations
- Why is feature-first folder structure often preferred in larger apps? A. It hides all code in one file B. Related code for a business capability lives together C. It eliminates the need for tests D. It disables hot reload
- What is the main risk of embedding a secret API key directly in a Flutter client? A. Faster builds B. The key can be extracted from the app C. Better animations D. Automatic store approval
- Which package is commonly used for secure on-device key-value storage? A. shared_preferences B. flutter_secure_storage C. path_provider only D. url_launcher
- What does flutter analyze primarily help with? A. Store screenshots B. Static analysis of code issues C. Generating icons D. Increasing download size
- When is a GlobalKey most appropriate? A. For every widget in a list B. When you need a global handle to a widget’s state or context C. To replace all ValueKeys D. To style text
- What is the purpose of verify(…) when using mocks in tests? A. To style widgets B. To confirm a method was called as expected C. To build release APKs D. To create color schemes
- Which of the following is a common production release requirement? A. Privacy policy when collecting user data B. Only debug builds C. No versioning D. Hard-coded passwords
- What is the main purpose of CI (Continuous Integration)? A. Automatically design UI B. Automatically run checks/tests on code changes C. Replace developers D. Remove the need for architecture
- Which animation approach automatically animates property changes with less boilerplate? A. Explicit AnimationController only B. Implicit animations such as AnimatedContainer C. Isolates D. MethodChannels
- What should you do when permission is permanently denied? A. Crash the app B. Guide the user to app settings and provide a fallback C. Silently ignore the feature forever without explanation D. Store tokens in plain text
- In a Clean Architecture Flutter app, which direction should dependencies point? A. Domain depends on Presentation B. Domain depends on Data and Presentation C. Presentation and Data depend inward on Domain abstractions D. Everything depends on widgets
- What is the primary purpose of keys in lists that can reorder? A. To improve color contrast B. To help Flutter preserve and match widget identity correctly C. To generate API keys D. To sign Android apps
- Which of the following is a good commit message characteristic? A. Vague and unrelated B. Clear description of why the change was made C. Contains secrets D. One word only always
- What is the purpose of pagination or infinite scrolling? A. Load all possible records at once B. Load data in manageable pages for performance and UX C. Disable search D. Replace authentication
- Which of the following is most appropriate for a one-time simple boolean preference? A. Full SQLite schema B. SharedPreferences C. Cloud Firestore only D. Custom C++ engine
- What is a frequent cause of Android release install/update problems? A. Using const widgets B. Not incrementing version code or signing incorrectly C. Using Material 3 D. Writing tests
- Why write widget tests? A. To verify UI behavior and interactions in a fast, automated way B. To replace all unit tests C. To generate keystores D. To create app icons
- What is the recommended response when an API returns 401 Unauthorized? A. Ignore it B. Handle re-authentication / logout flow appropriately C. Parse it as success JSON D. Disable the device camera
- Which of the following improves accessibility? A. Meaningful semantic labels and readable contrast B. Tiny unlabeled icons only C. Hard-coded text sizes that ignore system scaling D. Removing focus order
- What best describes a portfolio-quality Flutter project? A. Only screenshots with no code structure B. A complete app with architecture, error handling, tests, and clear README C. A single main.dart with everything mixed together D. An app that works only in debug mode
Answer Key (Part 1)
1-B, 2-B, 3-A, 4-C, 5-B, 6-B, 7-C, 8-B, 9-B, 10-B, 11-B, 12-B, 13-B, 14-B, 15-B, 16-B, 17-B, 18-B, 19-B, 20-B, 21-B, 22-B, 23-A, 24-B, 25-C, 26-B, 27-A, 28-B, 29-A, 30-B, 31-B, 32-B, 33-B, 34-B, 35-B, 36-B, 37-A, 38-B, 39-B, 40-B, 41-C, 42-B, 43-B, 44-B, 45-B, 46-B, 47-A, 48-B, 49-A, 50-B
PART 2 — Technical Questions (20)
Answer clearly and completely.
- Explain the relationship between Widget, Element, and RenderObject.
- When would you choose Riverpod over plain setState?
- Describe the difference between access tokens and refresh tokens.
- How does ListView.builder improve performance compared with a plain ListView?
- What problem do database migrations solve?
- Explain the dependency rule in Clean Architecture.
- Why is it important to dispose controllers and cancel subscriptions?
- What are the main differences between unit tests and widget tests?
- How would you design loading, empty, and error states for a product list screen?
- Why should client-side validation never be the only validation?
- Explain when to use const constructors and why they matter.
- What is the purpose of a repository interface in the domain layer?
- How do you safely handle navigation after an asynchronous login call?
- Describe a basic offline-first strategy for a notes app.
- What are common reasons an iOS build fails code signing?
- How can AI tools help a Flutter developer without replacing engineering judgment?
- Explain the difference between implicit and explicit animations with examples.
- Why is feature-first organization useful in medium-to-large Flutter codebases?
- What checks should a CI pipeline run for a typical Flutter project?
- List five production readiness items before submitting to an app store.
PART 3 — Debugging Problems (10)
For each problem: identify the issue, explain the cause, and provide a corrected approach.
- setState() called after dispose() after a network call.
- Using Navigator.of(context) after an await without mounted checks.
- A long list is built with ListView(children: products.map(…).toList()) and stutters.
- Tokens are saved in SharedPreferences and appear extractable.
- A form validates only on the client and the server still receives invalid data.
- A TextEditingController is created in State but never disposed.
- A package conflict appears after adding a new dependency; dependency_overrides was added permanently.
- A release build behaves differently from debug and crashes on startup.
- An API success path is assumed whenever any response is received, ignoring status codes.
- A large screen rebuilds completely when only one small text value changes.
PART 4 — Coding Challenges (5)
- Intermediate: Build a validated registration form with name, email, password, and confirm password. Show loading and error states.
- Intermediate: Implement a repository + notifier/cubit that fetches a list of items, exposes loading/error/data states, and supports retry.
- Advanced: Create a notes feature with local persistence, soft delete, restore, and search.
- Advanced: Implement an authenticated flow with secure session restoration and protected home screen.
- Advanced: Optimize a janky product grid: convert to builder, add const where possible, and optimize image loading. Explain the improvements.
PART 5 — Professional Capstone Project
Project Brief: Design and build a production-style Flutter application of your choice (task/project manager, personal finance tracker, content reader with offline support, marketplace listing app, habit tracker).
Required Capabilities: authentication; core CRUD feature(s) with meaningful business logic; API and/or Firebase integration; local storage / offline support; search and filtering; proper loading/empty/error states; secure session handling; clean architecture; state management beyond setState; responsive UI; unit tests + at least two widget tests; README with setup/architecture/run instructions; release-build awareness.
Architecture Requirements: feature-first structure, repository pattern, dependency injection, clear domain/data/presentation boundaries.
Security Requirements: secure token storage, HTTPS only, input validation, protected routes.
Testing Requirements: unit tests for repositories/notifiers/core logic, widget tests for key screens, instructions to run tests.
Performance Requirements: efficient lists/grids, thoughtful image handling, no obvious jank on core flows.
Deployment Requirements: versioning configured, release build instructions, store or web deployment plan, privacy policy note if user data is collected.
Capstone Grading Rubric (100 points)
Passing Guidance
- 90–100: Excellent professional level
- 75–89: Strong / solid professional baseline
- 60–74: Needs improvement in key areas
- Below 60: Not yet at certification standard
Final Certification Statement
Successful completion of this examination demonstrates that you can design, develop, test, optimize, secure, and prepare for deployment a professional Flutter application using modern Dart and Flutter practices.
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |





