Mobile App Development — Intermediate Guide
Level up your Flutter skills: state management, REST APIs, JSON, local storage, media, and real-world architecture.
Responsive UI & Screen Sizes
25 minReal apps run on hundreds of different screen sizes. This lesson teaches you to build layouts that look great on every phone and tablet.
What you'll learn
- Understand screen size and orientation
- Use MediaQuery to adapt layouts
- Build responsive Flutter UIs
Concept
Different devices have different widths, heights, and pixel densities. Flutter gives you MediaQuery to read the screen dimensions at runtime, so you can adjust padding, font sizes, and layouts based on the available space.
How it works
MediaQuery.of(context).size.width gives the screen width. You can check if the width is below a breakpoint and choose a different layout. LayoutBuilder gives the constraints of a specific parent, which is even more useful for reusable widgets.
Example
Understand it
LayoutBuilder checks the space a widget actually has, not the whole screen. This makes the widget reusable anywhere and it will always adapt correctly.
Practice
Build a widget that shows two elements side by side on wide screens and stacked on narrow screens.
Advanced Flutter Layouts
30 minBeyond Rows and Columns, Flutter offers powerful layout widgets like Expanded, Flexible, Stack, and GridView for complex screens.
What you'll learn
- Master Expanded and Flexible
- Layer widgets with Stack
- Create grids with GridView
Concept
Expanded and Flexible control how children share available space inside a Row or Column. Stack layers widgets on top of each other, useful for badges and overlays. GridView arranges items in a scrollable grid.
How it works
Expanded forces a child to fill remaining space. Flexible lets a child take up to a given fraction. Stack's children are positioned with Positioned or alignment. GridView.builder creates a scrollable grid efficiently.
Example
Understand it
The 'flex' value determines the ratio — a flex of 2 gets twice as much space as flex 1.
Practice
Create a screen with a title at the top, a grid of 6 items below, and a bottom button.
Reusable Custom Widgets
30 minDon't repeat the same UI code in every screen. Extract common pieces into reusable custom widgets for cleaner, maintainable code.
What you'll learn
- Identify repeated UI patterns
- Create custom StatelessWidget components
- Pass data via constructors
Concept
When you write the same card, button, or list tile in multiple places, extract it into its own widget class. The widget takes the changing parts as constructor parameters, so each use is slightly different without duplicating code.
How it works
Create a new widget class that accepts parameters like title and onTap. Return a styled Container or Card from its build method. Use the widget everywhere, passing different values each time.
Example
Understand it
The constructor parameters are how you make one widget flexible for many different uses — exactly like Flutter's own widgets.
Practice
Extract a repeated info card from your app into a reusable widget.
Themes & Dark Mode
25 minConsistent colors and typography make an app look professional. Themes let you define them once and apply them everywhere.
What you'll learn
- Define a light theme
- Add dark mode
- Style text and buttons consistently
Concept
Flutter's ThemeData describes the colors, fonts, and component styles for your whole app. You pass it to MaterialApp. Using theme colors like Theme.of(context).colorScheme.primary keeps everything consistent, and dark mode is as simple as providing a darkTheme.
How it works
In MaterialApp, set theme: ThemeData.light() and darkTheme: ThemeData.dark(). Flutter automatically switches based on the system setting. Reference theme colors in widgets instead of hardcoded Color values.
Example
Understand it
When you use Theme.of(context).colorScheme.primary, the widget updates automatically when the theme changes — no manual color updates needed.
Practice
Enable dark mode on your app and confirm all screens switch correctly.
Forms & Form Validation
30 minForms collect user data. This lesson teaches Flutter's Form widget and how to validate input before submitting.
What you'll learn
- Build a form with multiple fields
- Validate input with validators
- Show helpful error messages
Concept
The Form widget wraps TextFormField widgets and manages their validation state. Each field has a validator function that returns an error message if the input is invalid. Calling formKey.currentState.validate() runs all validators at once.
How it works
Create a GlobalKey<FormState>, pass it to the Form, then call validate() on submit. TextFormField's validator gets the current value and returns null if valid, or a string error if not.
Example
Understand it
Each validator returns null for valid input or an error string for invalid. validate() returns true only if every field is valid.
Practice
Add an email and password field, requiring both before submit.
Advanced Navigation
30 minBasic push/pop is fine for two screens, but real apps need tabs, bottom bars, and passing results back. This lesson covers those patterns.
What you'll learn
- Pass data back from a screen
- Use bottom navigation bars
- Handle navigation results
Concept
When a screen opens another, the second screen can return a result using Navigator.pop(context, result). The first screen awaits that result with await Navigator.push(...). Bottom navigation uses a BottomNavigationBar that switches between screens without pushing them onto the stack.
How it works
The first screen does 'final result = await Navigator.push(...)'. The second screen calls 'Navigator.pop(context, data)' to return data. BottomNavigationBar swaps the body based on the selected index.
Example
Understand it
The 'await' pauses the first screen until the second pops, then gives you the returned value. This is how forms return their data to the previous screen.
Practice
Open a picker screen from a main screen and display the picked item when you return.
Named Routes & Navigation Patterns
30 minAs apps grow, hardcoded MaterialPageRoutes become messy. Named routes centralize navigation and make deep links easier.
What you'll learn
- Define named routes
- Navigate with route names
- Use onGenerateRoute for arguments
Concept
Named routes map simple string names like '/home' to builder functions. This centralizes all navigation and lets you push by name instead of constructing widgets inline. You can also pass arguments through onGenerateRoute.
How it works
Define routes in MaterialApp's routes parameter. Navigate with Navigator.pushNamed(context, '/second'). For dynamic arguments, use onGenerateRoute to read settings.arguments.
Example
Understand it
Route names are string keys. Push by name keeps navigation logic in one place, so renaming or changing a screen is simpler.
Practice
Define two named routes and switch between them using pushNamed.
State Management Concepts
30 minAs your app grows, basic setState becomes hard to manage. This lesson explains the broader state management landscape.
What you'll learn
- Understand local vs shared state
- Know the main state management approaches
- Choose the right tool for your app
Concept
Local state affects one widget (a counter). Shared state affects many widgets (logged-in user, cart contents). setState works for local state, but shared state needs a state management solution like Provider, Riverpod, Bloc, or Riverpod to synchronize widgets across the app.
How it works
State management libraries provide a way to store state outside the widget tree and notify listeners when it changes. Widgets subscribe to only the state they need, avoiding rebuilding the whole app on every change.
Example
Understand it
The core problem is 'how do multiple widgets stay in sync when shared data changes?'. Every state library answers this question differently.
Practice
Identify which parts of a simple shop app are local state and which are shared state.
Provider State Management
35 minProvider is Flutter's recommended, beginner-friendly state management solution. This lesson shows how to use it for shared state.
What you'll learn
- Set up Provider
- Read and update shared state
- Notify listeners efficiently
Concept
Provider makes a state object available to widgets below it in the tree. Widgets read it with context.watch or context.read, and when you call notifyListeners(), all watching widgets rebuild with the latest value.
How it works
Wrap your app in a ChangeNotifierProvider. Create a class that extends ChangeNotifier and holds mutable state. In widgets, use context.watch<MyModel>() to rebuild on changes, and context.read<MyModel>() to call methods without rebuilding.
Example
Understand it
notifyListeners() is the key — it signals every listening widget to rebuild. Widgets that use context.watch are the listeners.
Practice
Convert a counter app from setState to Provider.
Riverpod Basics
35 minRiverpod is Provider's more powerful, compile-safe successor. It moves state definition outside the widget tree, fixing several Provider limitations.
What you'll learn
- Understand Riverpod's core concepts
- Create a simple provider
- Use ConsumerWidget to read state
Concept
Riverpod defines state in top-level 'providers' that can be read from anywhere without a BuildContext. Widgets use ConsumerWidget (or Consumer) to watch providers and rebuild when they change. Riverpod is compile-safe, so many errors are caught at compile time instead of runtime.
How it works
Create a StateProvider or ChangeNotifierProvider as a final global variable. In a ConsumerWidget, use ref.watch(myProvider) to get the value and ref.read for methods. Riverpod automatically handles dependency injection and disposal.
Example
Understand it
Unlike Provider, providers are global — no BuildContext required, so there's no chance of 'Provider not found' errors.
Practice
Create a simple Riverpod counter app with an increment button.
Working with REST APIs
30 minMost real apps get their data from a backend over HTTP. This lesson introduces REST APIs and how apps communicate with them.
What you'll learn
- Understand what REST APIs are
- Know HTTP methods and status codes
- Learn request/response basics
Concept
A REST API is a web service that exposes resources through URLs. You use HTTP methods — GET to fetch, POST to create, PUT/PATCH to update, DELETE to remove. Responses come as JSON with status codes (200 success, 404 not found, 500 error).
How it works
Your app sends an HTTP request to a URL like 'https://api.example.com/users'. The server processes it and returns a response with a status code and a body, usually JSON. Your app parses that JSON and updates the UI.
Example
Understand it
Each URL represents a resource. The HTTP method defines the action. Status codes tell you whether the request succeeded and why it failed.
Practice
Identify the correct HTTP method for fetching, creating, and deleting a book in an API.
HTTP Requests in Flutter
30 minTime to actually call an API from Flutter. This lesson shows how to use the http package for real requests.
What you'll learn
- Add the http package
- Make GET and POST requests
- Handle responses and errors
Concept
Flutter has no built-in HTTP client, so you add the 'http' package via pubspec.yaml. The package provides top-level functions like http.get() and http.post() that return a Future<Response>. The response has a statusCode and a body string.
How it works
Add http to dependencies, import 'package:http/http.dart', then call 'await http.get(Uri.parse(url))'. Check response.statusCode, then process response.body (usually JSON).
Example
Understand it
http.get returns a Future, so you must await it. The Response object separates the status code from the body, letting you handle errors properly.
Practice
Make a GET request to a public API and print the status code.
JSON Basics
25 minJSON is the language of APIs. This lesson teaches you to read, write, and understand JSON before parsing it in Dart.
What you'll learn
- Understand JSON structure
- Read nested JSON
- Recognize common JSON pitfalls
Concept
JSON (JavaScript Object Notation) is a lightweight text format for structured data. It uses key-value pairs inside braces, arrays inside brackets, and supports strings, numbers, booleans, and null. Almost every modern API returns JSON.
How it works
A JSON object looks like {'name': 'Ali', 'age': 25}. Arrays look like [1, 2, 3]. Nested structures combine both. Dart's jsonDecode converts a JSON string into Map and List objects.
Example
Understand it
In JSON, keys are always strings in quotes. A JSON object becomes a Dart Map, and a JSON array becomes a Dart List when decoded.
Practice
Write a JSON object representing a book with a title, author, and list of tags.
JSON Parsing & Serialization
35 minNow you take JSON from an API and turn it into Dart objects you can actually use in your app.
What you'll learn
- Decode JSON strings
- Access nested values safely
- Convert Dart objects back to JSON
Concept
Parsing is converting JSON text into Dart objects with jsonDecode. Serialization is the reverse — converting Dart objects back into JSON strings with jsonEncode. For complex responses, manual parsing gives you full control and performance.
How it works
Call jsonDecode(response.body) to get a Map or List. Access values with keys like map['name']. Cast types explicitly since JSON values are dynamic. For nested data, navigate with multiple key accesses or casts.
Example
Understand it
jsonDecode returns dynamic, so casting to String, int, or your own types makes the data safe to use in typed Dart code.
Practice
Parse a JSON string containing a list of three names and print each one.
Models & Data Classes
35 minRaw Maps are hard to work with. Data classes give you typed, named access to your API data and make the code much cleaner.
What you'll learn
- Create simple model classes
- Write fromJson and toJson methods
- Use models instead of raw Maps
Concept
A model class represents one type of data — a User, a Product, a Post. It has typed fields, a constructor, and fromJson/toJson methods to convert between the class and JSON. Using models makes your code readable, safe, and self-documenting.
How it works
Write a class with final fields. The fromJson factory takes a Map and returns an instance. The toJson method converts the instance back to a Map. jsonDecode + fromJson gives you typed objects from any API.
Example
Understand it
fromJson centralizes parsing logic in one place. Everywhere else in your app, you work with a typed User object instead of guessing what keys exist.
Practice
Create a Post model with id, title, and body, including fromJson.
Loading, Error & Empty States
30 minReal apps don't just show data — they show a spinner while loading, an error when something fails, and a message when there's no data.
What you'll learn
- Track loading state
- Handle errors gracefully
- Show empty states
Concept
Every network call goes through three phases: loading, success, or error. Good UX shows a spinner during loading, an error message with retry on failure, and a friendly message when the result is empty.
How it works
Keep an enum or boolean for loading state, and a separate error value. In the build method, check loading first (show spinner), then error (show error UI), then empty data (show empty UI), and finally show the data.
Example
Understand it
By handling states in order, you never show a broken UI — the user always sees something meaningful.
Practice
Add a loading spinner, error message, and empty state to a list screen.
Async Programming & Futures
30 minNetwork calls and file reads take time. Dart's async model uses Futures to handle work that completes later without freezing the app.
What you'll learn
- Understand async vs sync
- Work with Future
- Handle async results
Concept
A Future represents a value that will be available at some point in the future. When you call an async function, it returns immediately with a Future, and the actual work happens later. You use await to pause until the value is ready, keeping the UI responsive.
How it works
An async function returns a Future. Calling it starts the work; you use 'await' inside another async function to get the result. If you don't await, you get the Future itself, not the value.
Example
Understand it
The async/await syntax makes asynchronous code read like synchronous code, but under the hood, execution pauses and resumes when the work completes.
Practice
Write a function that returns a Future with a delay, and await it from main.
Async/Await & FutureBuilder
30 minFutureBuilder is Flutter's way to render UI based on a Future's state — perfect for showing spinners and data without manual state flags.
What you'll learn
- Use FutureBuilder
- Handle Future states cleanly
- Convert async data into UI
Concept
FutureBuilder takes a Future and a builder function. While the Future is pending, it shows one UI; when it completes with data or an error, it shows another. It rebuilds automatically when the Future resolves, removing the need for manual loading flags.
How it works
Pass a Future to FutureBuilder. In the builder, check snapshot.connectionState and snapshot.hasData/hasError to decide what to render.
Example
Understand it
The snapshot carries everything — connection state, data, and error. You don't need separate loading and error variables.
Practice
Use FutureBuilder to show a spinner then a name when a Future completes.
Local Storage Concepts
25 minNot all data lives on a server. Apps store preferences, caches, and offline data locally on the device.
What you'll learn
- Understand local storage options
- Know when to use each option
- Plan offline data strategy
Concept
Local storage keeps data on the device for speed, offline access, and small user preferences. Main options are Shared Preferences (simple key-value pairs), SQLite (structured relational data), and files (large data like images). Choosing the right one depends on what you are storing.
How it works
Shared Preferences is ideal for settings and tiny data. SQLite is for structured, queryable data like tasks or notes. Files store large binary data like downloaded images or documents.
Example
Understand it
Match the storage type to the data structure. Key-value for settings, relational for records, files for media.
Practice
Decide the best storage for: theme preference, a contact list, and a cached image.
Shared Preferences
30 minShared Preferences is the simplest way to store small bits of data like settings, flags, and tokens on the device.
What you'll learn
- Add shared_preferences package
- Store and read simple values
- Use it for app settings
Concept
shared_preferences is a Flutter plugin that stores key-value pairs persistently. It's best for small amounts of simple data — booleans, strings, integers. Data survives app restarts and is perfect for settings like dark mode or 'onboarding seen'.
How it works
Get an instance with SharedPreferences.getInstance(), then call setString, setBool, setInt to write and getString, getBool, getInt to read. Values persist automatically.
Example
Understand it
All read/write methods are async. Use '??' defaults when reading, because a key might not exist yet.
Practice
Store and retrieve a user's display name using Shared Preferences.
SQLite / Local Database
35 minFor structured data that needs querying, SQLite is the standard local database on mobile. This lesson introduces the sqlite3-based approach.
What you'll learn
- Understand SQLite basics
- Know how Flutter integrates SQLite
- Plan a local database schema
Concept
SQLite is a lightweight relational database that stores data in tables with rows and columns. In Flutter, the sqflite plugin provides an easy API for creating tables, inserting rows, and querying data with SQL. It's perfect for offline-first apps and structured records.
How it works
Open a database, create a table with columns, then use SQL commands — INSERT, SELECT, UPDATE, DELETE — through the sqflite API. Queries return rows as Maps.
Example
Understand it
A table is like a spreadsheet: columns define fields, rows are individual records. SQL is the language for reading and writing them.
Practice
Design a simple 'notes' table with id, title, body, and created_at columns.
CRUD Operations
35 minCreate, Read, Update, Delete — the four fundamental operations for any data store. This lesson implements them in Flutter with SQLite.
What you'll learn
- Implement Create
- Implement Read
- Implement Update and Delete
Concept
CRUD is the backbone of data-driven apps. Create inserts new records, Read fetches them, Update modifies existing ones, and Delete removes them. In SQLite, these map to INSERT, SELECT, UPDATE, and DELETE commands.
How it works
Using sqflite, you call db.insert for create, db.query for read, db.update for update, and db.delete for remove. Each operation takes the table name and relevant values or conditions.
Example
Understand it
Create and Read use no where clause (get/insert all). Update and Delete use a where clause to target a specific record by id.
Practice
Write code to add, fetch, update, and delete a record in a tasks table.
Image Loading & Caching
30 minImages make apps feel alive, but loading them inefficiently causes lag. This lesson covers displaying and caching images.
What you'll learn
- Load network images
- Show placeholder while loading
- Cache images for performance
Concept
Image.network loads an image from a URL but doesn't cache by default beyond a basic memory cache. The cached_network_image package adds disk caching, so images load instantly after the first time and work better offline.
How it works
Use Image.network for simple cases. For caching, use CachedNetworkImage with a placeholder and error widget. The package stores images on disk and serves them from cache on subsequent loads.
Example
Understand it
The placeholder shows while downloading, and the errorWidget handles failures. The cached version loads instantly the next time.
Practice
Replace an Image.network with a cached image and add a placeholder.
Image Picker & File Handling
30 minMost apps need to pick images from the gallery or camera. This lesson uses the image_picker plugin for user-selected media.
What you'll learn
- Add image_picker
- Pick images from gallery
- Handle picked files
Concept
image_picker opens the device's gallery or camera and returns a file path (or bytes) for the selected media. You can then display it locally or upload it to a server. Permissions are handled by the plugin on most platforms.
How it works
Call ImagePicker().pickImage(source: ImageSource.gallery). It returns an XFile? (null if the user cancels). Use the file's path with File or Image.file to display it.
Example
Understand it
The result is nullable because the user can cancel. Always check for null before using the file.
Practice
Add a button that opens the gallery and displays the chosen image.
Camera & Device Media
35 minBeyond picking images, apps can use the camera directly for live preview and custom capture experiences.
What you'll learn
- Access the device camera
- Capture photos
- Integrate with media workflows
Concept
The camera plugin gives low-level access to the device camera — preview, capture, and flash control. image_picker uses the camera indirectly through the system UI, but the camera plugin lets you build a custom camera screen inside your app.
How it works
Initialize a controller from available cameras, show a CameraPreview, and call takePicture() to capture. Handle permissions and lifecycle carefully. For most apps, image_picker is simpler; use camera only when you need custom controls.
Example
Understand it
The camera plugin gives you full control but requires more setup. image_picker is the easier path for simple capture.
Practice
List the setup steps needed before using the camera plugin.
Permissions
30 minSensitive device features — camera, location, storage — require user permission. This lesson teaches how to request and handle them.
What you'll learn
- Understand runtime permissions
- Request permissions properly
- Handle denial gracefully
Concept
Modern mobile OSes require apps to ask permission before accessing sensitive features. Android and iOS have different permission models. The permission_handler plugin provides a unified API to check and request permissions across both platforms.
How it works
Check the current status with Permission.camera.status. If not granted, call Permission.camera.request(). The result tells you whether the user granted, denied, or permanently denied — show an appropriate message for each.
Example
Understand it
Permissions are runtime, not install-time. Never assume a permission is granted — always check and handle the denial case.
Practice
Write logic to request camera permission and show a message if denied.
Search, Filter & Sorting
30 minUsers expect to find data quickly. This lesson teaches search, filtering, and sorting of local and API data.
What you'll learn
- Implement search
- Filter collections
- Sort data
Concept
Search filters a list by a query string. Filtering narrows data by criteria like category or status. Sorting orders data by a field like date or name. Combined, they make data exploration fast and intuitive.
How it works
Keep the full data list in memory and a separate filtered list. Use where() to filter, sort() to order, and update state on every query change.
Example
Understand it
where returns a new list with only matching items. sort reorders in place. Both are pure Dart operations — fast and simple.
Practice
Given a list of names, filter to those starting with 'A' and sort alphabetically.
Pagination & Infinite Scrolling
30 minLoading 10,000 items at once is slow. Pagination loads data in pages, and infinite scrolling fetches more as the user reaches the bottom.
What you'll learn
- Understand pagination
- Implement infinite scroll
- Load more data on scroll
Concept
Pagination splits a large dataset into pages — the API returns 20 items at a time with a page number or cursor. Infinite scrolling detects when the user reaches the bottom of a list and loads the next page automatically.
How it works
Use a ScrollController and listen for when the scroll position reaches near the maximum. Trigger a fetch for the next page, append the new items to the existing list, and call setState. Track the current page and whether more data exists.
Example
Understand it
You detect the bottom by comparing current scroll position to maxScrollExtent. Loading ahead by a margin (200px) makes the transition feel seamless.
Practice
Add infinite scroll to a list that fetches 20 more items when the user reaches the bottom.
App Architecture — MVC / MVVM
35 minAs apps grow, unorganized code becomes impossible to maintain. Architecture patterns separate concerns for clean, testable code.
What you'll learn
- Understand MVC pattern
- Understand MVVM pattern
- Choose a scalable structure
Concept
MVC (Model-View-Controller) separates data (Model), UI (View), and logic (Controller). MVVM (Model-View-ViewModel) binds the View to a ViewModel that exposes state and commands. Both keep business logic out of widgets, making code easier to test and maintain.
How it works
In Flutter, the View is your widgets. The Model is your data classes. The Controller/ViewModel holds business logic and exposes data to the View. State management tools like Provider or Riverpod act as the binding layer.
Example
Understand it
Separating concerns means you can change the UI without touching business logic, and test logic without rendering widgets.
Practice
Organize a simple app's files into models, views, and services folders.
Mini Project — Weather / News App
45 minCombine everything: fetch from an API, parse JSON into models, handle states, and display in a responsive list. This is your Intermediate capstone.
What you'll learn
- Integrate API + JSON + state
- Build a real multi-screen app
- Apply loading/error/empty states
Concept
A weather or news app pulls real data from a public API, parses it into typed models, handles loading and errors, and displays the results in a list or detail view. It uses nearly every concept from the Intermediate module.
How it works
Define models, write an API service with http, manage state with Provider/Riverpod, then build screens that watch the state and render lists with FutureBuilder or state management. Add a detail screen for individual items.
Example
Understand it
This is the pattern for nearly every data-driven app: model + service + state + UI. Master it and you can build almost anything.
Practice
Fetch data from a public API and display it in a list with loading and error states.
You have completed all 30 intermediate lessons.
Move on to Advanced for production-ready apps, security, and publishing.
Continue to Advanced