DOCODIVE
Mobile Apps Free Learning Path Advanced

Mobile App Development — Advanced Guide

Master production Flutter: architecture, auth, Firebase, offline sync, performance, testing, security, and publishing.

10–12 weeks 40 lessons Production-ready
01

Production Flutter Architecture

30 min

Moving from tutorial apps to production apps requires a deliberate architecture. This lesson introduces the principles that keep large Flutter apps maintainable.

What you'll learn

  • Understand why architecture matters
  • Learn layered architecture principles
  • Plan a production-ready folder structure

Concept

Production apps handle real users, real data, and real failures. Good architecture separates concerns — data, logic, and UI live in different layers. This makes the app testable, maintainable, and easier for multiple developers to work on simultaneously.

How it works

A common Flutter production structure separates the app into features, each with its own data, domain, and presentation layers. Shared code — networking, storage, utilities — lives in a core folder used by every feature.

Example

lib/ core/ # shared services, network, storage features/ auth/ data/ # repositories, models domain/ # business logic, entities presentation/ # screens, widgets, state home/ ...

Understand it

Feature-first means each feature is self-contained. Core is shared. This scales from 5 screens to 100 without becoming a mess.

⚠️ Common Mistake Putting all files flat in lib/ and relying on file names to organize code — it works for demos, not production.
💡 Pro Tip Start with a simple feature-folder structure even for small projects; it is much harder to retro-fit later.
✅ Key takeaway: Production architecture = feature folders + core services + clear layers.
Flutter app architecture guide

Practice

Design the folder structure for a shopping app with auth, products, cart, and checkout features.

Each feature gets its own folder with data/domain/presentation sublayers.
✓ lib/core + lib/features/auth, products, cart, checkout — each with data, domain, presentation.
01 / 40 Next
02

Clean Architecture

35 min

Clean Architecture is a well-known approach that makes code independent of frameworks and easy to test by separating it into concentric layers.

What you'll learn

  • Understand Clean Architecture layers
  • Separate entities, use cases, and UI
  • Apply dependency rules

Concept

Clean Architecture divides code into Entities (business objects), Use Cases (business rules), and Interface Adapters (repositories, controllers, presenters). The key rule: dependencies point inward — outer layers depend on inner layers, never the reverse.

How it works

Business logic lives in the innermost layers and knows nothing about Flutter, databases, or APIs. Outer layers (UI, frameworks) depend on inner layers through interfaces, so you can swap implementations without touching business rules.

Example

Domain layer (pure Dart): class GetUser { final UserRepository repository; GetUser(this.repository); Future<User> call(String id) => repository.getUser(id); } // UI depends on GetUser via an interface, not on HTTP or SQLite directly.

Understand it

Because business logic is pure Dart and has no Flutter imports, you can unit-test it without emulators or mock frameworks.

⚠️ Common Mistake Over-engineering a tiny app with every Clean Architecture layer — it adds boilerplate without benefit for small projects.
💡 Pro Tip Apply Clean Architecture selectively: full layers for complex features, simpler patterns for simple ones.
✅ Key takeaway: Clean Architecture = dependencies point inward, business logic stays framework-free.
The Clean Architecture (Uncle Bob)

Practice

Identify which layer a 'Login with email' use case belongs to and what it should NOT import.

It is a business rule — what does it know about?
✓ Domain/use-case layer — it should not import Flutter widgets, HTTP clients, or database code.
Previous 02 / 40 Next
03

Repository Pattern

30 min

The repository pattern is the standard way to separate data access from business logic. Your code asks a repository for data without caring where it comes from.

What you'll learn

  • Understand the repository abstraction
  • Hide data sources behind a repository
  • Swap data sources easily

Concept

A repository is a class that provides data to the rest of the app. It decides whether to fetch from the network, read from a local database, or use cached data. Business logic depends on the repository interface, not on specific data sources.

How it works

Define an abstract repository with methods like getUser(id) or getProducts(). Implement it with a class that uses HTTP, SQLite, or both. The rest of the app only knows the abstract type, so switching data sources is transparent.

Example

abstract class UserRepository { Future<User> getUser(String id); } class ApiUserRepository implements UserRepository { final http.Client client; ApiUserRepository(this.client); @override Future<User> getUser(String id) async { // HTTP call and JSON parsing } }

Understand it

The abstract class is the contract. Concrete implementations handle the 'how'. Callers only see the contract, making tests and future changes much easier.

⚠️ Common Mistake Calling HTTP or database code directly from widgets instead of going through a repository.
💡 Pro Tip Always program to the interface (abstract repository), never to a concrete implementation.
✅ Key takeaway: Repository = the single door through which all data flows.
Flutter repository pattern

Practice

Define an abstract ProductRepository with a method to fetch products.

Use an abstract class and a Future return type.
✓ abstract class ProductRepository { Future<List<Product>> getProducts(); }
Previous 03 / 40 Next
04

Dependency Injection

30 min

Dependency injection (DI) supplies objects their dependencies instead of creating them internally. It makes code testable and decoupled.

What you'll learn

  • Understand dependency injection
  • Use constructor injection
  • Wire dependencies with Riverpod/Provider

Concept

Instead of a class creating its own dependencies (new ApiClient()), those dependencies are passed in through the constructor or provided from outside. This lets you swap real implementations with fakes in tests and centralizes object creation.

How it works

Flutter apps commonly use Provider or Riverpod for DI. You declare providers for services and repositories, then widgets or use cases read them. At the composition root (main), you decide which concrete implementations to provide.

Example

class UserService { final UserRepository repository; UserService(this.repository); // injected, not created here Future<User> fetch(String id) => repository.getUser(id); }

Understand it

Constructor injection makes dependencies explicit — you can see exactly what a class needs just by looking at its constructor.

⚠️ Common Mistake Creating dependencies with 'new' inside classes, which hardcodes implementations and breaks testability.
💡 Pro Tip Prefer constructor injection — it is the simplest and most explicit form of DI.
✅ Key takeaway: Dependency injection = pass dependencies in, don't create them inside.
Flutter dependency injection

Practice

Refactor a class that creates its own ApiClient to accept it via constructor instead.

Move the dependency to a constructor parameter.
✓ class Service { final ApiClient client; Service(this.client); }
Previous 04 / 40 Next
05

Advanced State Management

35 min

Beyond simple counters, real apps have complex, interconnected state. This lesson covers advanced patterns for managing it predictably.

What you'll learn

  • Understand complex state challenges
  • Choose advanced state tools
  • Structure state by domain

Concept

Complex state includes async data, multiple data sources, shared state across features, and derived state. Tools like Riverpod, Bloc, and Provider handle these with patterns like state notifiers, selectors, and event streams. The goal is predictable state changes and minimal rebuilds.

How it works

State is held in dedicated state classes. UI watches only the slices it needs using selectors. Events or methods update state and notify only relevant listeners, avoiding whole-app rebuilds.

Example

Riverpod's StateNotifier: class CartNotifier extends StateNotifier<CartState> { CartNotifier() : super(CartState.empty()); void addItem(Item i) { state = state.copyWith(items: [...state.items, i]); } }

Understand it

Immutable state + copyWith makes changes predictable and easy to trace. Selectors let widgets watch only the part of state they care about.

⚠️ Common Mistake One giant state object for the whole app — every tiny change rebuilds everything.
💡 Pro Tip Split state by domain (auth, cart, products) and use selectors to watch only what a widget needs.
✅ Key takeaway: Advanced state = immutable state + selectors + domain-separated notifiers.
State management options

Practice

Split a single app state class into separate domain-specific state classes.

Think auth, cart, and products as separate concerns.
✓ AuthState, CartState, ProductsState each with their own notifier and provider.
Previous 05 / 40 Next
06

Complex Async State

30 min

Real async operations have loading, data, and error states — often all at once. This lesson shows how to model complex async state cleanly.

What you'll learn

  • Model loading/data/error together
  • Handle concurrent async operations
  • Avoid stale data bugs

Concept

A naive approach keeps separate booleans for loading and error, which quickly becomes inconsistent. Better is a sealed class or state object that represents exactly one state at a time: Loading, Success(data), or Error(message). This makes impossible states unrepresentable.

How it works

Define a sealed class hierarchy (Dart 3 sealed classes work well). Async operations return one of these states. UI switches on the state and renders the correct view for each case.

Example

sealed class AsyncState<T> {} class Loading<T> extends AsyncState<T> {} class Success<T> extends AsyncState<T> { final T data; Success(this.data); } class Error<T> extends AsyncState<T> { final String message; Error(this.message); }

Understand it

Because the state can only be one of Loading, Success, or Error, the compiler ensures you handle every case — no more 'loading but also error' bugs.

⚠️ Common Mistake Using separate isLoading and error variables that can both be true at the same time.
💡 Pro Tip Use Dart sealed classes with exhaustive switch — the compiler catches missing cases.
✅ Key takeaway: Sealed async state classes make invalid states impossible.
Dart sealed classes

Practice

Write a sealed class hierarchy for a Future's loading, success, and error states.

Use sealed + subclasses.
✓ See example — sealed AsyncState<T> with Loading, Success, and Error subclasses.
Previous 06 / 40 Next
07

Authentication Concepts

30 min

Authentication verifies who a user is. This lesson covers the core concepts — sessions, tokens, and credentials — before implementing anything.

What you'll learn

  • Understand authentication vs authorization
  • Learn session and token concepts
  • Know common auth flows

Concept

Authentication confirms identity (who are you?); authorization controls access (what can you do?). Modern apps commonly use token-based auth: user logs in with credentials, server returns a token (like JWT), and the app sends that token on subsequent requests. Tokens expire and can be refreshed.

How it works

Login → server validates credentials → returns access token + refresh token. App stores them securely. Each API request includes the access token. When it expires, the app uses the refresh token to get a new access token without re-login.

Example

POST /auth/login {email, password} → 200 {access_token, refresh_token} GET /user/profile Authorization: Bearer <access_token>

Understand it

Tokens are safer than storing passwords because they are short-lived, revocable, and scoped. The refresh token has a longer life but is only used to obtain new access tokens.

⚠️ Common Mistake Storing passwords or long-lived tokens insecurely — always use secure storage for sensitive auth data.
💡 Pro Tip Understand the token lifecycle (login → use → expire → refresh → logout) before coding any auth screen.
✅ Key takeaway: Authentication proves identity; tokens carry that proof securely between app and server.
Token-based authentication

Practice

Explain the difference between an access token and a refresh token.

Which is short-lived? Which is used to renew?
✓ Access token is short-lived and used for API requests; refresh token is longer-lived and used only to obtain new access tokens.
Previous 07 / 40 Next
08

Email & Password Authentication

35 min

The most common auth method is email and password. This lesson implements the login and signup flows in Flutter.

What you'll learn

  • Build login and signup forms
  • Call auth endpoints
  • Store tokens securely

Concept

Email/password auth sends credentials to a backend that verifies them against its user database. The app collects email and password via forms, sends them over HTTPS, and handles the response — success (store token) or failure (show error). Passwords must never be stored in plain text on the client.

How it works

Collect input, validate the form, send a POST to /auth/login with JSON credentials. On success, parse the token response and store tokens in secure storage. On failure, show the server's error message. Signup follows the same pattern with a registration endpoint.

Example

final response = await http.post( Uri.parse('https://api.example.com/auth/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'email': email, 'password': password}), ); if (response.statusCode == 200) { final token = jsonDecode(response.body)['token']; await secureStorage.write('token', token); }

Understand it

The token is the session. Store it securely (not in SharedPreferences, which is unencrypted), and send it on every authenticated request.

⚠️ Common Mistake Storing auth tokens in SharedPreferences — it is not encrypted and can be read by other apps or attackers.
💡 Pro Tip Hash passwords on the SERVER, never on the client. Client-side hashing is not real security.
✅ Key takeaway: Email/password auth = collect credentials → verify on server → store token securely.
Firebase email/password auth

Practice

Write code to send a login request and handle both success and error responses.

Use http.post with try/catch and statusCode checks.
✓ try { final r = await http.post(...); if (r.statusCode == 200) { store token } else { show error } } catch (e) { show network error }
Previous 08 / 40 Next
09

Google / Social Authentication

35 min

Social login lets users sign in with Google, Apple, or other providers instead of creating a new password. It is faster and more secure.

What you'll learn

  • Understand OAuth social login
  • Add Google sign-in
  • Handle social auth tokens

Concept

Social auth uses OAuth — the app redirects to the provider (Google/Apple), the user approves, and the provider returns an identity token. Your server verifies that token and creates or finds the user account. The user never shares a password with your app.

How it works

Use a package like google_sign_in. The user taps 'Sign in with Google', approves consent, and you receive an ID token and user info. Send the ID token to your backend to verify and create a session.

Example

final GoogleSignInAccount? account = await GoogleSignIn().signIn(); final auth = await account?.authentication; final idToken = auth?.idToken; // Send idToken to your backend for verification

Understand it

The identity token is a signed assertion from Google saying 'this user is X'. Your server verifies the signature rather than trusting the client blindly.

⚠️ Common Mistake Trusting user info from the client without verifying the ID token on the server — anyone can send fake data.
💡 Pro Tip Always verify social tokens server-side; never use client-provided identity alone for authorization.
✅ Key takeaway: Social auth = provider verifies identity, your server verifies the token.
google_sign_in package

Practice

Explain why you should verify a Google ID token on the server, not just read user info on the client.

Can a client be trusted?
✓ Because client data can be faked; server-side signature verification proves the token genuinely came from Google.
Previous 09 / 40 Next
10

Token & Session Management

35 min

Getting a token is easy; managing its lifecycle is the hard part. This lesson covers storing, refreshing, and revoking sessions properly.

What you'll learn

  • Store tokens securely
  • Implement token refresh
  • Handle logout and session expiry

Concept

Tokens expire. A robust app stores access and refresh tokens securely, automatically refreshes the access token when it expires, redirects to login on refresh failure, and clears tokens on logout. Session state should be shared app-wide so all screens react to auth changes.

How it works

Wrap authenticated requests in an interceptor or service that checks expiry. If expired, call the refresh endpoint with the refresh token, get a new access token, and retry the original request. If refresh fails, clear tokens and send the user to login.

Example

if (accessTokenExpired && refreshToken != null) { final newTokens = await refresh(refreshToken); store(newTokens); return retry(originalRequest); } // refresh fails → logout + redirect to login

Understand it

The refresh flow is invisible to the user when it works — they stay logged in seamlessly. Only when the refresh token itself expires do they need to re-authenticate.

⚠️ Common Mistake Leaving the refresh logic in every screen instead of centralizing it in an HTTP interceptor or auth service.
💡 Pro Tip Store a session flag (logged in/out) in a state provider so all screens update reactively.
✅ Key takeaway: Token management = store, refresh, retry, and clean up on logout.
Refresh tokens

Practice

Design the flow for what happens when an API call returns 401 Unauthorized.

Try refresh, then retry, then logout.
✓ Catch 401 → try refresh token → retry request → if refresh fails, clear tokens and go to login.
Previous 10 / 40 Next
11

Secure Storage

30 min

Sensitive data like tokens and keys must be stored securely. This lesson covers encrypted storage for Flutter apps.

What you'll learn

  • Understand secure storage vs regular storage
  • Store sensitive data safely
  • Choose the right secure storage package

Concept

SharedPreferences and SQLite store data in plain text — readable by anyone with device access. Secure storage encrypts data using platform keychains (Keychain on iOS, Keystore on Android). Use flutter_secure_storage for tokens, API keys, and other secrets.

How it works

flutter_secure_storage writes encrypted values to the platform's secure enclave. Read and write are async. The OS handles encryption, so even rooted devices have a harder time accessing the data.

Example

final storage = const FlutterSecureStorage(); await storage.write(key: 'auth_token', value: 'secret-token'); final token = await storage.read(key: 'auth_token'); await storage.delete(key: 'auth_token');

Understand it

Secure storage is the right tool for secrets. Never put passwords or tokens in SharedPreferences, logs, or code — all of those leak easily.

⚠️ Common Mistake Using SharedPreferences for tokens because it is 'simpler' — it stores data in plain text.
💡 Pro Tip Treat secure storage like a keychain: small, rare reads/writes, never for large datasets.
✅ Key takeaway: Secure storage = encrypted, platform-backed storage for secrets.
flutter_secure_storage package

Practice

Refactor auth token storage from SharedPreferences to secure storage.

Swap setString/getString for secure storage write/read.
✓ Replace prefs.setString('token', t) with secureStorage.write('token', t) and read similarly.
Previous 11 / 40 Next
12

API Security

35 min

Your app talks to servers — make those conversations secure. This lesson covers HTTPS, tokens, and input validation.

What you'll learn

  • Enforce HTTPS
  • Send tokens safely
  • Validate server responses

Concept

All API traffic must use HTTPS to encrypt data in transit. Auth tokens go in headers, never in URLs. Responses must be validated — never trust data just because it came from your server; it could be malformed or compromised.

How it works

Use HTTPS URLs exclusively. Attach tokens as Authorization headers. Validate response JSON against your models and handle unexpected data gracefully. Add certificate pinning for very high security needs.

Example

final response = await http.get( Uri.parse('https://api.example.com/data'), headers: { 'Authorization': 'Bearer $token', }, ); // validate response before using

Understand it

HTTPS protects against eavesdropping. Headers keep tokens out of logs and URLs. Validation prevents crashes and injection from malformed data.

⚠️ Common Mistake Using http:// in production or putting tokens in query parameters — both leak sensitive data.
💡 Pro Tip Use a centralized API client so every request automatically gets HTTPS and auth headers.
✅ Key takeaway: API security = HTTPS + token headers + response validation.
OWASP API security

Practice

Write a centralized function that adds auth headers to every request.

Wrap http calls with a helper that injects the token.
✓ Future<Response> authedGet(url) => http.get(url, headers: {'Authorization': 'Bearer $token'});
Previous 12 / 40 Next
13

Environment Variables & Secrets

30 min

Hardcoding API keys in source code is a security disaster. This lesson teaches how to manage environment variables and secrets properly.

What you'll learn

  • Separate secrets from code
  • Use environment-specific config
  • Never commit secrets to git

Concept

Secrets — API keys, tokens, endpoints — must never be in source code. Use environment variables or a config file that is excluded from version control. At runtime, load the right config based on the environment (development, staging, production).

How it works

Use a package like flutter_dotenv to load keys from a .env file that is in .gitignore. Reference values through a config object. In CI/CD, provide real secrets via the pipeline's secret store, not the repo.

Example

# .env (in .gitignore) API_KEY=abc123 API_URL=https://api.example.com // Dart await dotenv.load(); final key = dotenv.env['API_KEY']; # .gitignore .env

Understand it

Secrets belong in the environment, not the repository. If a secret is committed, assume it is compromised and rotate it immediately.

⚠️ Common Mistake Committing a .env file to git — once a secret is in git history, removing it is not enough.
💡 Pro Tip Use different API keys for dev and production; never share a production key with a dev environment.
✅ Key takeaway: Secrets in env/config, excluded from git, injected at runtime.
flutter_dotenv package

Practice

List the steps to securely add an API key to a Flutter project.

Think: where does the key live, and how does the app read it?
✓ Add key to .env, add .env to .gitignore, load via dotenv, reference via config, never commit.
Previous 13 / 40 Next
14

Role-Based Access

30 min

Not all users should see everything. Role-based access control (RBAC) restricts features based on the user's role.

What you'll learn

  • Understand roles and permissions
  • Implement UI-level access control
  • Enforce access server-side

Concept

Users have roles — admin, editor, viewer. Each role has permissions. The app shows or hides features based on the user's role, but the real enforcement happens server-side because client-side checks can be bypassed.

How it works

On login, the server returns the user's role. The app stores it and conditionally renders admin-only features. Every protected API endpoint still checks the role server-side, so hiding a button doesn't actually prevent access if someone calls the API directly.

Example

if (user.role == 'admin') { // show admin dashboard button } else { // hide it } // Server always checks role on /admin/* endpoints

Understand it

Client-side checks are UX convenience; server-side checks are the actual security. Never rely only on hiding UI.

⚠️ Common Mistake Thinking a hidden admin button protects the feature — a malicious user can call the API directly.
💡 Pro Tip Use a roles/permissions model in your auth state so UI checks are consistent and centralized.
✅ Key takeaway: RBAC = roles in state for UX + server enforcement for real security.
Firebase custom claims (roles)

Practice

Add a role check that only shows a 'Manage Users' button for admins.

Use an if statement on user.role.
✓ if (user.role == 'admin') ElevatedButton('Manage Users') — plus server check on the endpoint.
Previous 14 / 40 Next
15

Firebase Fundamentals

35 min

Firebase is Google's backend-as-a-service. It gives you auth, database, storage, and more without managing servers.

What you'll learn

  • Understand Firebase services
  • Set up a Firebase project
  • Connect Firebase to Flutter

Concept

Firebase provides ready-made backend services: Authentication, Firestore (NoSQL database), Storage (files), Cloud Functions, and more. Instead of building your own backend, you configure Firebase and use its SDKs from Flutter. It is ideal for MVPs and apps that need a backend fast.

How it works

Create a Firebase project in the console, add your app (Android/iOS), download config files (google-services.json / GoogleService-Info.plist), add them to your Flutter project, then use packages like firebase_core and cloud_firestore.

Example

// Initialize Firebase in main() await Firebase.initializeApp(); // Now use Firebase services final user = await FirebaseAuth.instance.signInAnonymously();

Understand it

Firebase replaces your custom backend for common needs. You still write client code, but the infrastructure is managed for you.

⚠️ Common Mistake Forgetting Firebase.initializeApp() before using any Firebase service — everything fails with cryptic errors.
💡 Pro Tip Use the Firebase CLI (flutterfire configure) to set up config files automatically.
✅ Key takeaway: Firebase = backend-in-a-box: auth, database, storage, all managed.
Firebase Flutter setup

Practice

List the first three steps to connect Firebase to a Flutter app.

Think: project, config, init.
✓ Create Firebase project, add config files to app, call Firebase.initializeApp() in main.
Previous 15 / 40 Next
16

Cloud Firestore

35 min

Firestore is Firebase's NoSQL cloud database — flexible, real-time, and serverless. This lesson covers basic reads and writes.

What you'll learn

  • Understand Firestore data model
  • Read and write documents
  • Listen for real-time updates

Concept

Firestore stores data as collections of documents. Each document is a set of key-value fields. It supports real-time listeners — when data changes on the server, your app updates instantly without polling. Queries filter and sort data from collections.

How it works

Reference a collection with FirebaseFirestore.instance.collection('users'). Add documents, read them, and listen with snapshots(). Query with where() and orderBy() for filtered results.

Example

// Write await FirebaseFirestore.instance.collection('users').add({ 'name': 'Ali', 'age': 25, }); // Read with real-time listener FirebaseFirestore.instance.collection('users').snapshots().listen((snapshot) { for (var doc in snapshot.docs) { print(doc.data()); } });

Understand it

The real-time listener is Firestore's killer feature — no polling, no refresh buttons; data just appears when it changes.

⚠️ Common Mistake Forgetting to cancel stream subscriptions when a widget is disposed, causing memory leaks.
💡 Pro Tip Use StreamBuilder for Firestore snapshots — it handles the stream lifecycle for you.
✅ Key takeaway: Firestore = collections → documents → fields, with real-time listeners.
Cloud Firestore docs

Practice

Write code to add a document and listen to collection changes.

Use collection().add and snapshots().listen.
✓ See example — add for write, snapshots().listen for real-time read.
Previous 16 / 40 Next
17

Firebase Storage

30 min

Firebase Storage stores files — images, videos, PDFs — in the cloud and gives you URLs to access them.

What you'll learn

  • Upload files to Firebase Storage
  • Get download URLs
  • Handle file metadata

Concept

Firebase Storage is object storage for large binary files. You upload from the device, get a download URL, and store that URL in Firestore or use it directly in Image widgets. Security rules control who can read and write files.

How it works

Reference a storage bucket, upload a file from a local path, and get a download URL after upload completes. Use the URL to display the file or store it in a database.

Example

final storage = FirebaseStorage.instance; final ref = storage.ref().child('images/photo.jpg'); await ref.putFile(File(imagePath)); final url = await ref.getDownloadURL(); // Use url in Image.network or store in Firestore

Understand it

Storage handles the binary data, while Firestore handles the metadata (like the URL). This separation keeps the database small and fast.

⚠️ Common Mistake Storing large files directly in Firestore — it has size limits and is not designed for binary data.
💡 Pro Tip Store the download URL in Firestore after uploading, so you can query metadata without hitting storage repeatedly.
✅ Key takeaway: Firebase Storage = files in the cloud; Firestore = their metadata.
Firebase Storage docs

Practice

Write the flow to upload a photo and retrieve its download URL.

Use putFile then getDownloadURL.
✓ await ref.putFile(file); final url = await ref.getDownloadURL();
Previous 17 / 40 Next
18

Push Notifications

35 min

Push notifications re-engage users even when the app is closed. This lesson covers setting up and sending notifications.

What you'll learn

  • Understand push notification flow
  • Set up FCM in Flutter
  • Handle notification taps

Concept

Push notifications flow from your server through a push service (like Firebase Cloud Messaging) to the device. The app registers with FCM, receives a device token, sends that token to your backend, and your backend uses it to send messages. Notifications can be data-only or display messages.

How it works

Configure FCM, initialize it in Flutter, request permission, get the device token, and listen for incoming messages. Handle foreground, background, and terminated states. When a user taps a notification, route them to the relevant screen.

Example

// Request permission and get token final token = await FirebaseMessaging.instance.getToken(); print('Device token: $token'); // Listen for messages FirebaseMessaging.onMessage.listen((message) { print('Received: ${message.notification?.title}'); });

Understand it

The device token uniquely identifies this app installation. Your backend sends a message to that token, and FCM delivers it to the right device.

⚠️ Common Mistake Hardcoding a device token in the backend — tokens rotate and change; always send the current token from the client.
💡 Pro Tip Store the FCM token in Firestore or your backend, associated with the user ID, so you can target specific users.
✅ Key takeaway: Push = device token + FCM + your backend triggering the message.
Firebase Cloud Messaging

Practice

Explain why device tokens should be sent to the backend rather than hardcoded.

Do tokens change?
✓ Tokens rotate and are unique per install; the backend needs the current token to deliver messages.
Previous 18 / 40 Next
19

Deep Links

30 min

Deep links open a specific screen inside your app from a URL — from a browser, another app, or a notification.

What you'll learn

  • Understand deep links
  • Configure deep links in Flutter
  • Route users to specific screens

Concept

A deep link is a URL like myapp://product/123 that opens your app directly on the product screen instead of the home screen. Flutter uses a router (like go_router) and platform configuration to map URLs to screens. Deep links work from browser links, notifications, and share messages.

How it works

Define routes with URL patterns in go_router. Configure Android intent filters and iOS associated domains. When the app opens via a URL, the router parses it and navigates to the matching screen with any parameters.

Example

GoRouter(routes: [ GoRoute( path: '/product/:id', builder: (context, state) { final id = state.pathParameters['id']; return ProductScreen(id: id); }, ), ]); // Opening myapp://product/42 navigates to ProductScreen(id: '42')

Understand it

The router maps URL paths to screens. Parameters in the URL (like :id) become arguments to the screen, enabling precise navigation from outside the app.

⚠️ Common Mistake Configuring deep links only in Flutter code without setting up platform intent filters/associated domains — links open the browser instead.
💡 Pro Tip Use go_router for deep links; it handles path parameters and redirects cleanly.
✅ Key takeaway: Deep links = URLs that open specific screens via your router.
Flutter deep linking

Practice

Define a deep link route for /user/:id that opens a UserScreen.

Use GoRoute with a path parameter.
✓ GoRoute(path: '/user/:id', builder: (c, s) => UserScreen(id: s.pathParameters['id']!))
Previous 19 / 40 Next
20

Background Tasks

35 min

Some work must continue when the app is not in the foreground — syncing, downloads, and timers. This lesson covers background execution.

What you'll learn

  • Understand background execution limits
  • Schedule background work
  • Handle platform restrictions

Concept

Mobile OSes heavily restrict background work to save battery. iOS and Android allow short background execution, background fetch, and push-triggered work. Use packages like workmanager for scheduled tasks and understand that long-running background work is limited by the platform.

How it works

workmanager schedules tasks that run periodically or after a delay. The task is a callback that runs in a background isolate. Be aware: exact timing is not guaranteed, and both OSes may defer work when battery is low.

Example

Workmanager().initialize(callbackDispatcher); await Workmanager().registerPeriodicTask( 'sync-task', 'syncData', frequency: Duration(hours: 1), );

Understand it

Background tasks are best-effort, not guaranteed. Design your app to sync gracefully whenever it gets a chance, rather than relying on exact scheduling.

⚠️ Common Mistake Assuming background tasks run at exact times — they can be delayed significantly by the OS.
💡 Pro Tip Prefer push-triggered background work when you need prompt action; use periodic tasks for best-effort syncing.
✅ Key takeaway: Background work is limited and best-effort — design around uncertainty.
workmanager package

Practice

Explain why background tasks should not rely on exact timing.

What do OSes prioritize?
✓ OSes prioritize battery and may defer work, so exact timing is never guaranteed.
Previous 20 / 40 Next
21

Offline-First Applications

35 min

Offline-first apps work with no internet connection, syncing later when connectivity returns. This is the gold standard for reliability.

What you'll learn

  • Understand offline-first design
  • Cache data locally
  • Sync when online

Concept

An offline-first app treats the local database as the source of truth. Reads always come from local storage, writes are stored locally first, and a background sync pushes changes to the server when connectivity is available. This makes the app instant and reliable even on flaky networks.

How it works

Store data in SQLite or a local cache. On startup, load from local storage for instant UI. Periodically fetch remote changes and merge them. Queue user actions and replay them when online.

Example

Read from SQLite immediately → show UI ↓ Try to sync with server ↓ On success, update local cache ↓ On failure, stay offline and retry later

Understand it

The local database is the source of truth. The server is just a sync target, which means the app never blocks on network calls.

⚠️ Common Mistake Treating the server as the source of truth and caching as an afterthought — this causes blank screens offline.
💡 Pro Tip Use a syncing library or design a clear merge strategy before building offline features.
✅ Key takeaway: Offline-first = local is truth, server is sync target.
Offline data strategies

Practice

Design the data flow for a notes app that works offline and syncs later.

What is the source of truth?
✓ Notes stored in SQLite locally; UI reads SQLite; background sync pushes/pulls changes from server.
Previous 21 / 40 Next
22

Data Synchronization

35 min

Offline apps need a sync strategy to merge local and remote changes without losing data. This lesson covers conflict handling.

What you'll learn

  • Understand sync challenges
  • Handle conflicts
  • Merge local and remote changes

Concept

When both the local device and the server have changed the same record, you have a conflict. Sync strategies include last-write-wins (simplest), version vectors, and operation-based transforms. The right choice depends on how likely conflicts are and how critical data loss is.

How it works

Track timestamps or version numbers on records. During sync, compare local and remote versions. If one is newer, take it. If both changed (conflict), apply a merge rule — for most apps, last-write-wins with timestamps is sufficient.

Example

Record: {id, title, body, updatedAt} Sync: if (local.updatedAt > remote.updatedAt) push(local) else if (remote.updatedAt > local.updatedAt) pull(remote) else no change

Understand it

last-write-wins is simple but can silently drop a user's edit during a conflict. For critical data, use more sophisticated merge strategies.

⚠️ Common Mistake Having no sync strategy and blindly overwriting server data with local data on every sync.
💡 Pro Tip Use updatedAt timestamps for basic last-write-wins; consider version vectors for collaborative apps.
✅ Key takeaway: Sync = compare versions and merge; last-write-wins is the simplest approach.
Offline data sync

Practice

Implement a last-write-wins sync check between two timestamps.

Compare updatedAt and pick the newer.
✓ if (localUpdatedAt.isAfter(remoteUpdatedAt)) push(local) else pull(remote);
Previous 22 / 40 Next
23

Caching Strategies

30 min

Caching stores frequently used data for faster access. A good caching strategy balances freshness with speed.

What you'll learn

  • Understand caching types
  • Implement in-memory caching
  • Use time-based cache invalidation

Concept

Caching keeps data in fast storage (memory or disk) to avoid slow network or database reads. Cache strategies include cache-aside (check cache, fetch on miss), write-through (write to cache and source together), and time-based expiration (TTL). Choose based on how often data changes.

How it works

For cache-aside: check if the cached value exists and is fresh. If yes, return it. If no or stale, fetch from the source, store in the cache, and return. Time-based invalidation marks entries stale after a set duration.

Example

final cached = cache.get('products'); if (cached != null && !cached.isExpired) { return cached.data; // fast path } final fresh = await fetchFromApi(); cache.set('products', fresh, ttl: Duration(minutes: 5)); return fresh;

Understand it

TTL (time to live) is the simplest invalidation — data is fresh for N minutes, then refetched. It is perfect for data that doesn't change second-to-second.

⚠️ Common Mistake Caching everything forever, so users see stale data and can't tell if it's current.
💡 Pro Tip Match TTL to how often data actually changes — user profile can be minutes, a news feed seconds.
✅ Key takeaway: Caching = store, expire (TTL), and refresh for speed without staleness.
Caching strategies

Practice

Write a cache check that returns cached data if not expired, else fetches fresh.

Use an isExpired flag and a TTL.
✓ See example — check cache and expiry, return fresh on miss or expiry.
Previous 23 / 40 Next
24

Performance Optimization

35 min

A smooth, fast app keeps users happy. This lesson covers profiling and the most impactful performance improvements.

What you'll learn

  • Profile Flutter apps
  • Identify performance bottlenecks
  • Apply common optimizations

Concept

Performance issues usually show as jank (dropped frames) — the UI stutters. Use Flutter DevTools and the performance overlay to find which frames are slow. Common causes are unnecessary rebuilds, heavy build methods, and rendering large widgets inefficiently.

How it works

Run the app with the performance overlay. Find frames that exceed 16ms (60fps budget). Use DevTools to inspect the widget tree and isolate slow builds. Apply fixes like const widgets, ListView.builder, and splitting large widgets.

Example

Optimizations: 1. Use const for static widgets 2. Use ListView.builder not ListView(children) 3. Split large build methods into smaller widgets 4. Avoid expensive work in build() 5. Use selectors to limit rebuilds

Understand it

Each frame must complete in ~16ms. Anything that rebuilds more widgets than necessary or does heavy work in build() risks dropping frames.

⚠️ Common Mistake Doing expensive computation or network calls inside build() — it runs on every rebuild.
💡 Pro Tip Profile first, then optimize. Don't guess — DevTools shows exactly what is slow.
✅ Key takeaway: Performance = profile with DevTools, then fix unnecessary rebuilds and heavy builds.
Flutter performance

Practice

List three common causes of jank in Flutter apps.

Think rebuilds, builds, and lists.
✓ Unnecessary rebuilds, expensive build methods, and large lists rendered without builder.
Previous 24 / 40 Next
25

Flutter Rendering & Rebuilds

35 min

Understanding how Flutter renders and rebuilds widgets is the key to writing efficient UIs.

What you'll learn

  • Understand the widget/render tree
  • Control rebuilds
  • Use const and selectors effectively

Concept

Flutter maintains a widget tree, an element tree, and a render tree. When state changes, affected widgets rebuild. A rebuild re-runs the build method but only re-renders what actually changed visually. Const widgets never rebuild, and selectors limit rebuilds to widgets that depend on changed data.

How it works

setState marks a widget dirty, scheduling a rebuild. Flutter diffs the old and new widget trees and updates only the parts that differ. Const widgets are identical each build, so Flutter skips them. Selectors watch specific state slices and rebuild only when that slice changes.

Example

// const widget — never rebuilds const Text('Static title'); // selector — rebuilds only when name changes final name = context.select((User u) => u.name);

Understand it

Rebuild ≠ re-render. A rebuild is cheap if the resulting widget is mostly const or unchanged. Selectors make rebuilds even more targeted.

⚠️ Common Mistake Putting mutable state at the root so every small change rebuilds the entire app.
💡 Pro Tip Move state down to where it's used — a counter should rebuild a counter widget, not the whole screen.
✅ Key takeaway: Rebuilds are cheap when targeted; const and selectors keep them that way.
Widget tree and rebuilding

Practice

Identify which widgets rebuild when a single counter changes in a large screen.

Is the counter state local or global?
✓ Only widgets watching the counter state rebuild if state is properly scoped; root-level state rebuilds everything.
Previous 25 / 40 Next
26

Memory & Resource Management

30 min

Memory leaks and unmanaged resources crash apps over time. This lesson covers finding and fixing them.

What you'll learn

  • Understand memory leaks
  • Dispose controllers and listeners
  • Profile memory usage

Concept

Memory leaks happen when objects are no longer needed but still referenced, so garbage collection can't free them. In Flutter, the most common cause is forgetting to dispose controllers, streams, and listeners. Over time, leaks grow and the app slows or crashes.

How it works

Controllers like TextEditingController, AnimationController, and StreamSubscription must be disposed. Override dispose() in State to release them. DevTools memory view shows memory growth over time, helping spot leaks.

Example

class _MyScreenState extends State<MyScreen> { final _controller = TextEditingController(); StreamSubscription? _sub; @override void dispose() { _controller.dispose(); _sub?.cancel(); super.dispose(); } }

Understand it

Dispose releases native resources and removes listeners. Every controller you create should have a matching dispose call.

⚠️ Common Mistake Creating a controller in build() — it gets recreated every rebuild and never disposed properly.
💡 Pro Tip Create controllers in initState (once) and dispose them in dispose; never create them in build.
✅ Key takeaway: Every controller and listener must be disposed to prevent memory leaks.
Widget lifecycle

Practice

Add proper dispose calls to a screen with a TextEditingController and a StreamSubscription.

Override dispose and release both.
✓ See example — dispose controller and cancel subscription in dispose().
Previous 26 / 40 Next
27

Animations & Motion

35 min

Animations bring apps to life and guide user attention. This lesson covers Flutter's animation system.

What you'll learn

  • Understand implicit vs explicit animations
  • Animate widgets smoothly
  • Build custom animations

Concept

Implicit animations (AnimatedContainer, AnimatedOpacity) animate a property change automatically. Explicit animations use AnimationController for full control over timing and curves. Most everyday animations can be built with implicit widgets; use explicit when you need chained or physics-based motion.

How it works

Implicit animations watch a property and smoothly transition when it changes. Explicit animations need an AnimationController with a duration and a listener that rebuilds on each tick. Curves (like Curves.easeInOut) make motion feel natural.

Example

AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, width: _expanded ? 200 : 100, height: _expanded ? 100 : 50, child: ..., )

Understand it

Implicit animations handle the animation lifecycle for you — change the value, get a smooth transition. Explicit controllers give you frame-by-frame control when needed.

⚠️ Common Mistake Using explicit AnimationController when AnimatedContainer would do — it adds complexity for no benefit.
💡 Pro Tip Start with implicit animations; reach for explicit only when you need custom sequences.
✅ Key takeaway: Implicit = animate property changes; explicit = full control with controllers.
Flutter animations

Practice

Animate a box expanding when tapped using an implicit animation.

Use AnimatedContainer with a width that changes on tap.
✓ AnimatedContainer(duration: 300ms, width: tapped ? 200 : 100, ...) with setState on tap.
Previous 27 / 40 Next
28

Advanced Custom UI

35 min

Sometimes built-in widgets aren't enough. This lesson covers custom painting and advanced widget composition.

What you'll learn

  • Use CustomPainter
  • Build custom shapes and charts
  • Compose complex UIs

Concept

CustomPainter gives you a canvas to draw anything — custom charts, shapes, and effects. It works alongside normal widgets, so you can build custom visual elements and wrap them in standard layout widgets. This is how apps create unique, branded interfaces.

How it works

Extend CustomPainter and override paint(canvas, size) to draw with Canvas API. Override shouldRepaint to control when the painter redraws. Wrap it in CustomPaint widget and use standard layout around it.

Example

class CirclePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = Colors.blue; canvas.drawCircle(size.center(Offset.zero), size.width / 2, paint); } @override bool shouldRepaint(covariant CustomPainter old) => false; } // CustomPaint(painter: CirclePainter())

Understand it

CustomPainter draws directly on a canvas. shouldRepaint returning false means the drawing is static; true means it needs redrawing when properties change.

⚠️ Common Mistake Using CustomPainter for simple layouts that could be built with standard widgets — it is harder to read and maintain.
💡 Pro Tip Reach for CustomPainter only for visuals that standard widgets can't create, like custom charts or complex shapes.
✅ Key takeaway: CustomPainter = canvas drawing for visuals standard widgets can't make.
Custom painting

Practice

Draw a simple diagonal line using CustomPainter.

Use canvas.drawLine in the paint method.
✓ canvas.drawLine(Offset.zero, Offset(size.width, size.height), Paint()..color = Colors.red);
Previous 28 / 40 Next
29

Accessibility

30 min

Accessible apps work for everyone, including users with disabilities. This lesson covers Flutter's accessibility features.

What you'll learn

  • Understand accessibility principles
  • Add semantic labels
  • Support screen readers

Concept

Accessibility ensures users with visual, motor, or cognitive impairments can use your app. Flutter provides semantics — descriptions of widgets for screen readers — and supports large text, high contrast, and focus navigation. Accessible design benefits all users.

How it works

Add semantic labels to images and buttons, use proper contrast ratios, support text scaling, and ensure tappable targets are large enough. Flutter's Semantics widget and tooltips make content readable by screen readers.

Example

Semantics( label: 'Profile picture', child: Image(...), ); // Buttons should have descriptive labels ElevatedButton( onPressed: ..., child: const Text('Save changes'), );

Understand it

Screen readers read semantic labels aloud. Without them, images and icon-only buttons are invisible to visually impaired users.

⚠️ Common Mistake Using icon-only buttons without semantic labels — screen reader users can't tell what they do.
💡 Pro Tip Always add labels to images and icon buttons, and test your app with a screen reader.
✅ Key takeaway: Accessibility = semantics, contrast, scaling, and proper focus for all users.
Flutter accessibility

Practice

Add a semantic label to an icon-only button.

Wrap with Semantics or add a tooltip.
✓ Wrap the IconButton in Semantics(label: 'Search') or add tooltip: 'Search'.
Previous 29 / 40 Next
30

Internationalization & Localization

35 min

Global apps speak many languages. This lesson covers translating your app for different locales.

What you'll learn

  • Understand i18n and l10n
  • Set up localization
  • Support multiple languages

Concept

Internationalization (i18n) is designing the app to support many languages. Localization (l10n) is the actual translation into specific locales. Flutter's gen_l10n tool generates localization classes from ARB files, letting you swap text based on the device's language.

How it works

Create ARB files for each locale (app_en.arb, app_es.arb). Configure l10n in pubspec. Generated classes expose translated strings. Set the app's locale based on the device, and wrap with MaterialApp's localizationsDelegates.

Example

// app_en.arb { "hello": "Hello", "@hello": {"description": "Greeting"} } // app_es.arb { "hello": "Hola" } // In widget Text(AppLocalizations.of(context)!.hello)

Understand it

ARB files hold translations. The code references a key, and the framework picks the right language based on locale.

⚠️ Common Mistake Hardcoding strings everywhere, then realizing you need to support a second language and rewriting everything.
💡 Pro Tip Use ARB/localization from the start, even with one language — adding a second is trivial later.
✅ Key takeaway: i18n = design for languages; l10n = translations via ARB + generated classes.
Flutter internationalization

Practice

Add a Spanish translation for a greeting string.

Create app_es.arb with the translated value.
✓ Add app_es.arb with the same key and the Spanish translation, then configure l10n.
Previous 30 / 40 Next
31

Unit Testing

30 min

Unit tests verify individual functions and classes work correctly in isolation. They are the foundation of reliable software.

What you'll learn

  • Write unit tests in Dart
  • Test pure business logic
  • Use mocks for dependencies

Concept

A unit test exercises a single function or class with known inputs and asserts the output. Pure Dart code (no Flutter, no network) is easy to test. Mock dependencies like repositories to isolate the logic being tested. Run tests with flutter test.

How it works

Use the test package. Write test('description', () { ... expect(actual, expected); }). For code with dependencies, inject mock objects that return fixed values.

Example

import 'package:flutter_test/flutter_test.dart'; int add(int a, int b) => a + b; void main() { test('add returns the sum', () { expect(add(2, 3), 5); }); }

Understand it

expect() compares actual to expected. A test passes if all expectations match; any mismatch fails the test with a clear message.

⚠️ Common Mistake Writing tests that depend on network or database — they become slow and flaky. Mock everything external.
💡 Pro Tip Test pure business logic first — repositories, use cases, and models are the highest-value unit tests.
✅ Key takeaway: Unit tests = pure logic + known inputs + assertions with mocks for dependencies.
Flutter unit testing

Practice

Write a unit test for a function that capitalizes a string.

Use expect with the expected output.
✓ test('capitalize', () { expect(capitalize('hi'), 'Hi'); });
Previous 31 / 40 Next
32

Widget Testing

35 min

Widget tests verify that UI components render and interact correctly without a full device.

What you'll learn

  • Write widget tests
  • Find and interact with widgets
  • Assert UI behavior

Concept

Widget tests pump a widget into a test environment and verify its output. You can tap buttons, enter text, and assert that the expected UI appears. They run faster than full integration tests and catch most UI logic errors.

How it works

Use testWidgets(). Pump the widget, use find.text/find.byType to locate elements, call tester.tap() and tester.enterText() for interactions, and expect() to verify results. pump() advances a frame after changes.

Example

testWidgets('counter increments when tapped', (tester) async { await tester.pumpWidget(const MyApp()); expect(find.text('0'), findsOneWidget); await tester.tap(find.byType(ElevatedButton)); await tester.pump(); expect(find.text('1'), findsOneWidget); });

Understand it

Widget tests render the widget tree in a fake environment. find locates widgets, tap simulates user interaction, pump rebuilds the tree, and expect verifies the result.

⚠️ Common Mistake Forgetting to pump after an interaction — the UI hasn't rebuilt, so assertions see stale state.
💡 Pro Tip Test one behavior per test; small focused tests are easier to debug than one giant test.
✅ Key takeaway: Widget tests = pump UI, interact, pump again, assert the result.
Flutter widget testing

Practice

Write a widget test that verifies a button label is visible.

Use pumpWidget and find.text.
✓ testWidgets('shows button', (t) async { await t.pumpWidget(MyApp()); expect(find.text('Save'), findsOneWidget); });
Previous 32 / 40 Next
33

Integration Testing

35 min

Integration tests run the full app on a real device or emulator, verifying that all parts work together end-to-end.

What you'll learn

  • Understand integration tests
  • Write end-to-end test flows
  • Run tests on devices

Concept

Integration tests exercise the real app from launch through user flows. Unlike unit/widget tests, they run on actual devices and can use real plugins and network. They are slower but catch issues that only appear when everything is connected.

How it works

Use integration_test package. Write testWidgets with real app startup (app.main()), perform multi-step user flows, and assert results. Run with flutter test integration_test or on a device.

Example

import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('full login flow', (tester) async { app.main(); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), '[email protected]'); await tester.tap(find.text('Login')); await tester.pumpAndSettle(); expect(find.text('Home'), findsOneWidget); }); }

Understand it

Integration tests verify real workflows — login, checkout, navigation — using the full app stack with real plugins and backend calls.

⚠️ Common Mistake Relying only on unit tests and discovering integration bugs in production — they don't cover real device behavior.
💡 Pro Tip Write integration tests for your most critical user journeys, not every screen.
✅ Key takeaway: Integration tests = full app + real device + end-to-end user flows.
Flutter integration testing

Practice

Describe when an integration test is more valuable than a unit test.

What do integration tests catch that unit tests miss?
✓ When verifying a full user flow across multiple screens with real plugins and backend.
Previous 33 / 40 Next
34

Debugging Production Issues

30 min

Production bugs are hard because you can't attach a debugger to a user's phone. This lesson covers remote debugging strategies.

What you'll learn

  • Understand production debugging challenges
  • Use logging strategically
  • Reproduce issues locally

Concept

In production, you rely on logs, crash reports, and user feedback instead of a live debugger. Strategic logging captures the state when errors occur. Crash reporting tools collect stack traces. Reproducing the issue locally, even with mocked data, is often the fastest path to a fix.

How it works

Add meaningful logs at key points — request/response, state changes, errors. Use a crash reporting tool like Sentry or Crashlytics to capture crashes with context. When a user reports a bug, reproduce it with the logged data.

Example

try { final data = await api.fetchData(); logger.info('Fetched ${data.length} items'); } catch (e, stack) { logger.error('Fetch failed', e, stack); crashReporter.report(e, stack); }

Understand it

Logs tell you what happened before a crash. Crash reports give the stack trace. Together they pinpoint the cause without needing the user's device.

⚠️ Common Mistake No logging or error tracking in production — when something breaks, you have no idea why.
💡 Pro Tip Log enough context to reproduce the issue: inputs, state, and the exact error message.
✅ Key takeaway: Production debugging = strategic logs + crash reports + reproduction.
Firebase Crashlytics

Practice

List the two most important things to log before an API call and after a crash.

What helps you reproduce and fix?
✓ Log the request inputs/state before the call, and the error + stack trace after a crash.
Previous 34 / 40 Next
35

Crash Reporting & Monitoring

30 min

You can't fix crashes you don't know about. Crash reporting tools capture errors automatically and alert you.

What you'll learn

  • Set up crash reporting
  • Understand crash reports
  • Monitor app health

Concept

Crash reporting tools (Crashlytics, Sentry) automatically collect uncaught errors, stack traces, device info, and user context. You get a dashboard of crashes grouped by type, showing which are most common and most severe. This turns invisible production failures into actionable data.

How it works

Initialize the crash reporter at app startup. Uncaught errors are captured automatically. Add custom context (user ID, screen) to make reports more useful. Monitor the dashboard and fix the highest-impact crashes first.

Example

await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); // Add context FirebaseCrashlytics.instance.setUserIdentifier(userId); // Log a non-fatal error FirebaseCrashlytics.instance.recordFlutterError(error, stack);

Understand it

Crash reports group identical crashes, so you see 'this crash affected 5,000 users' and can prioritize accordingly.

⚠️ Common Mistake Shipping an app with no crash reporting — bugs are invisible until angry users report them.
💡 Pro Tip Add crash reporting before release, not after users start reporting issues.
✅ Key takeaway: Crash reporting = automatic error capture + dashboard + prioritization.
Crashlytics for Flutter

Practice

Set a user identifier in Crashlytics so crashes can be traced to specific users.

Use setUserIdentifier after login.
✓ FirebaseCrashlytics.instance.setUserIdentifier(userId); after authentication.
Previous 35 / 40 Next
36

App Security Best Practices

35 min

Security must be a first-class concern, not an afterthought. This lesson summarizes the most critical mobile security practices.

What you'll learn

  • Protect user data
  • Secure network communication
  • Prevent common vulnerabilities

Concept

Mobile security spans secure storage, encrypted communication, input validation, least-privilege permissions, and protecting secrets. Key principles: never store plaintext secrets, always use HTTPS, validate all input, request only needed permissions, and treat the client as untrusted.

How it works

Apply layers: secure storage for secrets, HTTPS for transit, certificate pinning for high security, server-side validation for all input, and permission minimization. Regular security reviews and dependency updates close known vulnerabilities.

Example

Security checklist: 1. HTTPS everywhere 2. Tokens in secure storage 3. Secrets in env/config, never git 4. Validate server responses 5. Request minimum permissions 6. Keep dependencies updated 7. Server-side auth/role checks

Understand it

Security is layered — no single measure is enough. Each layer covers a different attack vector, and together they make exploitation much harder.

⚠️ Common Mistake Trusting the client for security (hiding buttons, client-side validation) instead of enforcing on the server.
💡 Pro Tip Review your dependencies for known vulnerabilities regularly and update promptly.
✅ Key takeaway: Security = layers: storage, transit, secrets, validation, permissions, server enforcement.
OWASP Mobile Top 10

Practice

List three security practices every production app should follow.

Think storage, transport, and secrets.
✓ Secure storage for tokens, HTTPS for all traffic, secrets in env/config not git.
Previous 36 / 40 Next
37

CI/CD for Flutter

35 min

CI/CD automates building, testing, and deploying your app. It catches errors early and speeds up releases.

What you'll learn

  • Understand CI/CD concepts
  • Set up automated builds and tests
  • Deploy automatically

Concept

CI (Continuous Integration) automatically runs tests and builds whenever code is pushed. CD (Continuous Delivery/Deployment) automates releasing those builds. Tools like GitHub Actions, Codemagic, and GitLab CI can build, test, sign, and publish Flutter apps automatically.

How it works

Configure a pipeline (YAML file) that triggers on git push. The pipeline runs lint, unit tests, widget tests, then builds a release APK/IPA. On tags or main branch merges, it can sign and upload to app stores or internal test tracks.

Example

name: CI on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: subosito/flutter-action@v2 with: flutter-version: '3.x' - run: flutter pub get - run: flutter test - run: flutter build apk

Understand it

The pipeline catches broken code immediately after push, before it can affect users or other developers. This is the backbone of reliable releases.

⚠️ Common Mistake Running tests and builds manually on a laptop — slow, inconsistent, and easy to skip.
💡 Pro Tip Start with a simple pipeline that runs flutter test on every push; add builds and deployment later.
✅ Key takeaway: CI/CD = automated test + build + deploy on every code change.
Flutter continuous delivery

Practice

Write the core steps of a CI pipeline for a Flutter app.

What must happen after code is pushed?
✓ Checkout code, set up Flutter, pub get, lint, run tests, build release.
Previous 37 / 40 Next
38

Android Build, Signing & Release

35 min

Shipping an Android app requires signing a release build with a secure key. This lesson walks through the process.

What you'll learn

  • Build a release APK/AAB
  • Sign with a keystore
  • Prepare for Play Store upload

Concept

Android requires all release apps to be digitally signed. You generate a keystore with a private key, configure Flutter to use it, and build a release App Bundle (.aab) — the format Play Store requires. Protect the keystore and password, because losing them means you can't update the app.

How it works

Generate a keystore with keytool, create key.properties with credentials, reference it in build.gradle, then run flutter build appbundle. The signed .aab is ready to upload to Play Console.

Example

keytool -genkey -v -keystore release-keystore.jks -alias upload -keyalg RSA -keysize 2048 -validity 10000 # key.properties (in .gitignore) storeFile=release-keystore.jks storePassword=... keyAlias=upload keyPassword=... # Build flutter build appbundle

Understand it

The keystore is your app's identity. Lose it, and you cannot publish updates to the same app on Play Store.

⚠️ Common Mistake Committing the keystore or its passwords to git — anyone with them can sign malicious updates as you.
💡 Pro Tip Back up the keystore in a secure offline location and rotate the password regularly.
✅ Key takeaway: Android release = keystore signing + appbundle + Play Console upload.
Flutter Android release

Practice

List the files and credentials that must be kept out of git for Android signing.

What identifies your app?
✓ The keystore file (.jks) and key.properties with passwords — both go in .gitignore/secure storage.
Previous 38 / 40 Next
39

iOS Build, Signing & Release

35 min

Publishing to the App Store requires Apple Developer credentials, certificates, and provisioning profiles. This lesson covers the flow.

What you'll learn

  • Understand Apple signing
  • Configure Xcode signing
  • Build and upload to TestFlight

Concept

iOS apps must be signed with an Apple certificate and provisioning profile. You need an Apple Developer account, create an app identifier, and configure signing in Xcode. Flutter builds an IPA or uses Xcode to archive and upload to App Store Connect for TestFlight and review.

How it works

Create an App ID and distribution certificate in the Apple Developer portal. In Xcode, enable automatic signing with your team. Archive the app, then upload to App Store Connect. TestFlight is used for beta testing before public release.

Example

In Xcode: 1. Add your Apple Developer team 2. Set bundle identifier 3. Enable automatic signing 4. Archive the app 5. Upload to App Store Connect

Understand it

Apple's signing is more complex than Android's — multiple certificates and profiles, all tied to your developer account. Automatic signing in Xcode simplifies most of it.

⚠️ Common Mistake Forgetting to create the App ID or provisioning profile, causing signing errors at archive time.
💡 Pro Tip Use automatic signing in Xcode; it handles most certificate/profile management for you.
✅ Key takeaway: iOS release = Apple account + certificates + Xcode archive + App Store Connect.
Flutter iOS release

Practice

What are the two most important things needed to sign an iOS app?

Think account and config.
✓ Apple Developer account and a valid certificate/provisioning profile.
Previous 39 / 40 Next
40

App Store / Play Store Publishing + Capstone

45 min

The final step is publishing your app to the world. This lesson covers store submission and wraps the entire path with a capstone.

What you'll learn

  • Prepare store listing
  • Submit for review
  • Complete the full capstone app

Concept

Publishing involves creating store listings (title, description, screenshots, icon), setting content ratings, and submitting for review. Both stores have approval processes — Apple's is stricter. The capstone brings everything together: a complete, production-ready app that demonstrates all 90 lessons.

How it works

For Play Store: create listing, upload .aab, set rating, submit. For App Store: create listing, upload via Xcode/TestFlight, set metadata, submit for review. Respond to review feedback if needed. The capstone should be a real app — auth, data, offline sync, tested, and published.

Example

Play Console listing: title, short/full description, screenshots, icon, category, content rating, privacy policy URL. App Store Connect: app name, subtitle, description, keywords, screenshots, build, review notes.

Understand it

The store listing is marketing — clear descriptions, good screenshots, and honest privacy info improve installs and approval chances.

⚠️ Common Mistake Submitting with incomplete privacy policy or misleading screenshots — it causes review rejection.
💡 Pro Tip Publish an internal/beta release first, get feedback, then go public.
✅ Key takeaway: Publishing = store listing + build upload + review + your complete capstone app.
Flutter deployment overview

Practice

List the key items needed in a store listing.

What do users see before downloading?
✓ Title, description, screenshots, icon, category, content rating, and privacy policy.
Previous 40 / 40
You have completed all 40 advanced lessons.

You've finished the complete Flutter path — from zero to production.

Continue to Practice Hub

📱 Scan this QR code with your phone camera to instantly open this page.

Works on iOS, Android, and any modern device. No app installation required.

Account Verified!

Your email has been verified successfully.