13. Flutter
Estimated time to read: 115 minutes
Flutter is an open-source framework developed by Google to simplify the development of apps for Android and iOS. With Flutter, you can develop and test your app quickly, without worrying about the details of the underlying platforms.
learning path
Here is the suggested learning path of the Flutter team.
13.1 Libraries and Samples
By now, Flutter has a large ecosystem. Don’t reinvent the wheel, but also avoid relying on outdated code.
- package repository for Dart and Flutter -- Check the number of contributors, latest updates, and additional dependencies. Check packages for some recommended libs and useful VS Code extensions.
And samples
- Cookbook
- Samples
- Samples
- Compass App -- shows the recommended architecture explained here and here Architecting Flutter apps.
13.2 Architecture
Flutter is a cross-platform UI toolkit that enables developers to create applications that can run on iOS, Android, web, and desktop while sharing most of their code. The system is built in layers, with the Flutter engine (written in C++) at the bottom handling core functionality like graphics, text layout, and input/output operations. Above the engine sits the Flutter framework (written in Dart), which provides developers with pre-built components called widgets that are used to construct the application's user interface. To make Flutter work on different devices, platform-specific "embedders" serve as intermediaries between Flutter and the operating system, managing essential tasks like rendering surfaces and processing input events.

13.3 Dart
Flutter uses Dart as its programming language. While Dart shares many similarities with C++, there are some key differences you should be aware of.
I recommend starting with my basic Dart material and completing the tasks in the Object-Oriented Programming section to get comfortable with the language. Once you're familiar with the fundamentals, you can move on to the more advanced topics in the following sub sections.
Dart Specialties:
- Everything you can store in a variable is an object, and every object is an instance of a class. This includes numbers, functions, and even null.
- No need for the new keyword to instantiate objects.
- Dart uses single inheritance.
- All classes implicitly define an interface.
- Supports optional parameters.
- Built-in null safety.
- No protected; privacy is indicated by a naming convention (_), with everything else public by default.
- Lists are similar to arrays.
switch
can be used with all types.
13.3.1 Links
- SheLikesCoding -- my beginner friendly material.
- Language Tour
- Dartpad
- Cheatsheet
- Learn the fundamentals
- Dart Course: Learn by doing
13.3.2 Lambdas - Arrow Operator - =>
Lambdas are anonymous functions and are best thought as a mathematical map to, i.e.
(a,b,c) → f(a,b,c)
Here is an example
bool hasEmpty = aListOfStrings.any((s) => s.isEmpty);
13.3.3 Generics
Generics are widely used and make the type of lists or classes and methods in general exchangeable without the need of redefinition. In C++ you have templates. However, there are some differences Comparing Templates and Generics. The following snippets are from Generics.
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}
var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var views = Map<int, View>();
T first<T>(List<T> ts) {
// Do some initial work or error checking, then...
T tmp = ts[0];
// Do some additional checking or processing...
return tmp;
}
13.3.4 General Snippets
// Iterable collections
var listOfInts = [1, 2, 3]; // A simple list of integers
// Using a collection for loop to create a list of strings with interpolated values
var listOfStrings = ['#0', for (var i in listOfInts) '#$i'];
// Filter words longer than 6 characters and count them
var longWords = allWords.where((w) => w.length > 6).toList().length;
var points = <Point>[]; // Empty list of Point objects (List<Point>)
var addresses = <String, Address>{}; // Empty map with String keys and Address values (Map<String, Address>)
var counts = <int>{}; // Empty set of integers (Set<int>)
// const is for hardcoded, compile-time constants
const items = ['Salad', 'Popcorn', 'Toast', 'Lasagne'];
// Check if any item in the list contains the letter 'a'
if (items.any((item) => item.contains('a'))) {
print('At least one item contains "a"');
}
// Check if all items have a length of 5 or more characters
if (items.every((item) => item.length >= 5)) {
print('All items have length >= 5');
}
// Find the first item that has a length greater than 5
var found = items.firstWhere((item) => item.length > 5);
// Filter out even numbers from the list of numbers
var evenNumbers = numbers.where((number) => number.isEven);
// String interpolation
// Prints a message with interpolated variables
print('Hello, $name! You are ${year - birth} years old.');
// Ternary operator (short form of if/else)
var visibility = isPublic ? 'public' : 'private'; // If isPublic is true, visibility will be 'public', else 'private'
// Optional parameters (optional positional parameter and default value for the named parameter)
int sumUpToFive(int a, [int? b, String title = '']) {
// Implementation goes here
}
const Scrollbar({super.key, required Widget? child}); // Constructor for Scrollbar with named parameters
// Named parameters - optional unless marked as required
void enableFlags({bool? bold, bool? hidden}) {
// Enable or disable flags based on parameters
}
// Calling the function with named parameters
enableFlags(bold: true, hidden: false);
// Classes with constructors and methods
class Point {
double x, y;
// Constructor with positional parameters
Point(this.x, this.y);
// Static method to calculate distance between two points
static double distanceBetween(Point a, Point b) {
// Return the distance between points using the Pythagorean theorem
return ((b.x - a.x).abs() + (b.y - a.y).abs());
}
}
class Person {
// A private field, visible only within this class/library
// private by naming convention _
final String _name; //final is for read-only variables that are set just once
// Constructor accepting the name
Person(this._name);
// A public method to greet someone
String greet(String who) => 'Hello, $who. I am $_name.';
}
13.3.5 Mixin, Interfaces, Base Classes, Extension Methods
Mixins, interfaces, base classes, and extension methods are all mechanisms that allow you to add or modify functionality in classes.
- A mixin in Dart is a way to add functionality to classes without using inheritance.
- An interface defines a contract that a class must adhere to. A class that implements an interface must provide implementations for the methods and properties defined by that interface. Unlike other programming languages, in Dart, all classes implicitly define an interface (see Implicit interfaces). However, Dart has some distinct rules regarding interfaces
- If you want to explicitly define an interface, you must use the
interface class
syntax, e.g.,interface class Foo
. - When using the
interface
keyword withoutabstract
, the methods in the interface must have implementations. - To define a pure interface (an interface without any implementation), you need to use the combination of
abstract
andinterface
, i.e.,abstract interface class
.
- If you want to explicitly define an interface, you must use the
-
Extension methods add functionality to existing libraries and types.
extension <extension name>? on <type> { (<member definition>)* }
Try to understand the following examples.
// Mixin for logging user activities
mixin ActivityLogger {
void logActivity(String activity) {
print('Activity Log: $activity');
}
}
// Mixin for sending notifications
mixin Notifier {
void sendNotification(String message) {
print('Sending notification: $message');
}
}
// A basic User class that mixes in both ActivityLogger and Notifier
class User with ActivityLogger, Notifier {
final String name;
User(this.name);
void performAction(String action) {
logActivity('$name performed $action');
sendNotification('$name has completed an action: $action');
}
}
void main() {
var user = User('John Doe');
user.performAction('login');
user.performAction('update profile');
}
/// extension method
extension on String {
bool get isBlank => trim().isEmpty;
}
if (v.isBlank){...}
/// interface
abstract class JournalRepository {
Future<Result<List<Journal>>> getJournals();
Future<Result<void>> delete(int id);
Future<Result<void>> create(Journal journal);
}
interface class Vehicle {
void moveForward(int meters) {
// without the keyword abstract an implementation is required
}
}
/// mixin
mixin LoadingStateMixin {
bool isLoading = false;
void setLoading() {
isLoading = true;
}
}
class _HomeScreenState extends State<HomeScreen> with LoadingStateMixin {
// may use isLoading and setLoading
}
13.3.6 Special Operators
String? notAString = null; // the type String? indicates notAString may be null
// ? execute right hand side only if not null
print(notAString?.length?.isEven);
// ?? operator: uses the lefthandside if not null and the righthandside otherwise
print(nullableString ?? 'alternate');
// and ??= assignment operator, which assigns a value to a variable only if that variable is currently null:
int? a; // a = null
a ??= 3; // a = 3
a ??= 5; // a is still 3
// .. cascade: perform a sequence of operations on the same object, instead of typing querySelector?.text, querySelector?.onclick ...
querySelector('#confirm')
?..text = 'Confirm'
..onClick.listen((e) => window.alert('Confirmed!'))
..scrollIntoView();
Task
- Implement an extension method for String, that makes the first letter of the string a capital letter and returns the given string.
- Use the null aware operator to set a default value for a variable x.
- Define a class (and hence implicitly an interface) for a student.
13.4 Interactive Programming Paradigms
Depending on your learning style, you can either start by building your first Flutter applications following my basic instructions and the codelab and then read the following chapters to understand the bigger picture and concepts, or you can take the opposite approach, starting with the broader concepts and then diving into programming.
13.4.1 Event-Driven Programming
As a C++ programmer, you're likely familiar with writing code that runs sequentially — one line after the other. However, when building applications with graphical user interfaces (GUIs), we often need to handle user input (like clicks, key presses, etc.) and other events asynchronously. This is where event-driven programming comes in.
Event-driven programming is a paradigm where the flow of the program is determined by events, such as user actions or messages from other programs. Instead of executing instructions in a fixed order, your application waits for and reacts to these events.
In GUI development, we typically have an event loop — a mechanism that continuously waits for events (like button clicks or keyboard input) and dispatches them to the appropriate parts of the application. In Dart and Flutter, the event loop is an essential part of how apps run. The event loop is part of the framework that keeps your app responsive. It listens for events like user interactions (taps, gestures, etc.) and system events (like timers or network responses). When an event happens, the event loop triggers the relevant code (called a callback) to handle the event.

In Dart, you work with asynchronous operations (like network requests or timers) using Futures and Streams, which are key concepts for handling non-blocking tasks in the event loop. We will discuss this in more detail in asynchronous programming
13.4.1.1 Simple Example: Button with Event Handler
Imagine creating a simple app with a button. In Flutter, you'd write something like this:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp()); // Runs the MyApp widget and initializes the app
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {//this method renders the UI
return MaterialApp(
// The MaterialApp widget sets up basic material design elements for the app
home: Scaffold(
body: ElevatedButton(
onPressed: () {
// This function is called when the button is pressed
print("Button Pressed!"); // Prints a message to the console
},
child: Text('Press Me'), // Text displayed on the button
),
),
);
}
}
- The
onPressed
is the event handler. It reacts to the button press, and the event loop calls it when the button is pressed. - The event loop is constantly running in the background, awaiting actions like this and triggering the appropriate callback functions.
This is a simple example, but the core idea of event-driven programming in Flutter remains the same: you define what should happen when certain events (like user actions) occur, and the framework takes care of the event loop to trigger those actions.
Widget
In Flutter everything is a widget. Widgets are immutable, meaning they don't change themselves. So when the UI needs to be updated (for example, after a user interaction), Flutter calls the build
method to rebuild the widget and display the updated UI.
13.4.2 State
To fully understand the rest, you will likely need some basic instructions and to complete the codelab.

The state of an app refers to everything that exists in memory while the app is running. State is the data necessary to rebuild the UI at any given moment.
Flutter differs between ephemeral state and app state:
-
Ephemeral state (sometimes referred to as UI state or local state) refers to state that is confined within a single widget. This is where StatefulWidgets come into play, such as for managing the state of a TextField, a checkbox, or a progress indicator.
-
When an app consists of multiple widgets and screens, the question arises: How do you synchronize the state of different widgets and manage the data across the app? This is where app state — in the narrower sense — becomes important, as it involves how data is shared, stored, and managed across different parts of the application.
The following figure illustrates the difference of app state and ephemeral state. And may be used as a decision tree, what is needed in which case.

"A widget declares its user interface by overriding the build()
method, which is a function that converts state to UI:
The build()
method is by design fast to execute and should be free of side effects, allowing it to be called by the framework whenever needed (potentially as often as once per rendered frame)."1
13.4.2.1 Simple Counter with setState
We'll start with the basic counter example using setState
to manage the counter's state. This is the most straightforward way of managing local state in Flutter. In this example, we have a button that, when pressed, increments a counter displayed on the screen. The state of the counter is updated using setState()
, which triggers a rebuild (calls the build
method) of the widgetd (in this case the complete CounterScreen
) to reflect the new counter value.
// Import the Flutter material package for UI elements
import 'package:flutter/material.dart';
// The entry point of the application
void main() {
runApp(MyApp());
}
// MyApp is a stateless widget, which means its state doesn't
// change once it's built, it's like showing a picture
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterScreen(),
);
}
}
// CounterScreen is a stateful widget, which means it can have a
// state that changes over time, e.g. when we change the counter by
// pressing the button
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
// _CounterScreenState manages the state of the CounterScreen widget
class _CounterScreenState extends State<CounterScreen> {
int _counter = 0;
// This function is called when the button is pressed,
// and it increments the counter value
void _incrementCounter() {
// Change the state of the _CounterScreenState instance
// and thus trigger a repainting of the screen,
// i.e. trigger a call of the build method
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
// Scaffold provides a default layout structure, such as app bar and body
return Scaffold(
appBar: AppBar(
title: Text('Counter Example'),
),
body: Center(
// Column widget arranges its children vertically
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Counter: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
),
),
);
}
}
setState()
in the private method_incrementCounter
triggers a rebuild of the widget, reflecting changes in the counter value.
This is fine for a single page application. But it will not work for app state data, i.e. data needed in different widgets or screens of your app. We will discuss this in more detail in chapter .
13.4.3 Reactive Programming vs. Data Binding
Many frameworks like Maui (C#), React and Vue face similar problems.
Reactive Programming is a programming paradigm focused on asynchronous data streams and change propagation. It treats data as streams of events that can be reacted to, emphasizing how changes flow through a system.
Data Binding is a specific technique for automatically synchronizing UI elements with data sources. It can be unidirectional or bidirectional and often serves as an implementation of reactive principles.
Flutter uses a declarative-reactive approach:
- Unidirectional data flow: Data flows top-down through the widget tree
- State management: Changes trigger rebuilds of affected widgets
- Immutable widgets: Widgets are recreated on changes, not modified
- Reactive streams: Uses Streams, Futures, and ValueNotifier for reactive programming
13.4.3.1 Flutter vs MAUI, React, Vue
- Flutter: "UI is a function of state" - rebuilds on changes
- MAUI: "UI and data are coupled" - bidirectional automatic propagation
- React: "Data flows down, events flow up" - explicit unidirectional flow
- Vue 3: "Fine-grained reactivity" - automatic dependency tracking with optional two-way binding
13.5 New Project
For every new project start with the following. I have prepared this for you in your repository under the 'app' directory; you just need to
- run
flutter upgrade
andflutter doctor
to check and upgrade your flutter installation - run
flutter pub get
(in the folder app) and - adapt the steps 7, 8 and 14
- do steps 2 and 11.
Here are the basic steps for a new project
- create a new project, see also codelab first app
- give it a good name
- test rename if you want to rename an existing app.
- run it on all relevant platforms (web, Android, mac, ...), i.e. test the configurations
- choose the namespace, typically your domain backwards with your project, e.g. de.h_da.fbi.hci.fitness -- no - allowed, i.e. replace all occurrences of com.example with your namespace
- adapt VS Code for flutter, e.g. use settings
- create a folder structure similar to package structure of the sample compass_app
- consider copying the folders utils, ui/core of the sample compass_app
- at least create the folders
- data
- domain
- utils
- routing
- ui
- core
- localization
- arb
- localization
- home
- settings
- core
- add internationalization
- add loc extension methods from Simplified Flutter Localization using a BuildContext extension and
nullable-getter: false
tol10n.yaml
.
- add loc extension methods from Simplified Flutter Localization using a BuildContext extension and
- add logging
- define your theme
- Use themes to share colors and font styles
- Material Theme Builder -- with an export to flutter, see figure below.
- flex_color_scheme and the playground
- flex_seed_scheme
- very good flutter styles - generate Flutter theme code directly from the color and text styles in your Figma document
- Advanced Theming Techniques in Flutter: Leveraging Extensions for Dynamic UIs
- Add a NavigationBar for primary destinations. And use Navigator for nested navigation. With Navigator it is possible to pass arguments and return values while navigating.
- add go_router to preserve route states
- Add a test screen (playground) to you primary navigation. Here you can add single widgets or group of widgets and use hot reload to test your design.
- Add an about screen and show the licenses, your app uses licenses
- Add your launcher icon
- Add freezed to easier implement domain models
- Define your App State, see Simple App State or Advanced App State
- Add a settings screen and store your settings using shared_preferences
freezed
If you use freezed with the annotation, make sure to run dart run build_runner build
after each change in the domain model. Or run run dart run build_runner watch
to watch for changes and run the build runner automatically.

Next start to implement the UI of your screens and Figma components.
13.6 User Interface
In this chapter I want to guide you through the process to realize a given Figma prototype into Flutter code.
There are some tools supporting a visual design with drag & drop of UI elements and generating code from this visual design, there are even some plugins for Figma to generate Flutter code. However, the generated code is mostly complicated, difficult to understand and to adapt or extend. In many frameworks the approach of hot reload is used instead.
Case Study
A case study may be found in the article How Flutter facilitates collaboration between designers and developers.
13.6.1 Layout and Widgets
For each screen and component identify rows and cols. Try to work with as less as possible rows and cols.

Spacing Attribute
The new flutter version supports
Row(
spacing: 15.0,
children: [
Now add the widgets according to your design, check the widget catalog. Define custom widgets for each component of your Figma design. Before you start to copy & paste widgets in your code, extract them to a custom widget.
Use the Flutter inspector (devtools or the icon on the right of your debugger) to play with settings of your widgets

See also

Further reading
13.6.1.1 Constraints and Overflowed problems
Make sure, you understand the constraints options and make them work for you.

"The error often occurs when a Column or Row has a child widget that isn’t constrained in its size."2
It might be sufficient to wrap the control with Expanded.
13.6.2 Cupertino - Material
By default, Flutter implements Material Design since it's a Google framework. This means it follows Android's design guidelines out of the box. When you create a new Flutter project, you typically start with MaterialApp
as your root widget, which provides the Material Design structure and styling.
However, Flutter recognizes that iOS users expect a different look and feel - the distinctive Apple design language. This is where the Cupertino package comes in. By using CupertinoApp
instead of MaterialApp
, along with other Cupertino widgets throughout your app, you can create an iOS-native feel. For example, you would use CupertinoButton
instead of MaterialButton
, or CupertinoNavigationBar
instead of AppBar
.
Now, this might raise a question: "What happened to the 'write once, run anywhere' promise of cross-platform development?" This is where the flutter_platform_widgets package provides an elegant solution. This package acts as an abstraction layer that automatically determines which platform the app is running on and serves the appropriate widget - Material for Android or Cupertino for iOS. As a developer, you can write code using these platform-aware widgets, and the package handles the platform-specific implementation details.
Nevertheless, Flutter provides an automatic adaptation for things that are behaviors of the OS environment (such as text editing and scrolling) and that would be 'wrong' if a different behavior took place.
13.6.3 User Input
In Flutter, the Form widget is an essential tool when you are dealing with multiple input fields that require validation, saving, and resetting the form data, see Cookbook - Forms.
- Form: Using a Form provides access to a FormState, which lets you save, reset, and validate each FormField that descends from this Form. You can also provide a GlobalKey to identify a specific form.
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
if (_formKey.currentState!.validate())
- FormField: Each individual form field should be wrapped in a FormField widget with the Form widget as a common ancestor.
- TextFormField: TextFormField wraps a TextField and integrates it with the enclosing Form.
The example below is from Create a Form with a GlobalKey.
class MyCustomForm extends StatefulWidget {
const MyCustomForm({super.key});
@override
MyCustomFormState createState() {
return MyCustomFormState();
}
}
class MyCustomFormState extends State<MyCustomForm> {
// Create a global key that uniquely identifies the Form widget
// and allows validation of the form.
//
// Note: This is a GlobalKey<FormState>,
// not a GlobalKey<MyCustomFormState>.
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
// Build a Form widget using the _formKey created above.
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
// you could also set private members with
// setState here
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: ElevatedButton(
onPressed: () {
// Validate returns true if the form is valid, or false otherwise.
if (_formKey.currentState!.validate()) {
// If the form is valid, display a snackbar. In the real world,
// you'd often call a server or save the information in a database.
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing Data')),
);
}
},
child: const Text('Submit'),
),
),
],
),
);
}
}
The TextEditingController offers precise management, allowing you to retrieve and modify the text, which is useful for pre-populating a text field. If you want to programmatically control the TextField and clear its value, for example, you'll need a TextEditingController.
// Define a custom Form widget.
class MyCustomForm extends StatefulWidget {
const MyCustomForm({super.key});
@override
State<MyCustomForm> createState() => _MyCustomFormState();
}
class _MyCustomFormState extends State<MyCustomForm> {
// Create a text controller and use it to retrieve the current value
// of the TextField.
final myController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void dispose() {
// Clean up the controller when the widget is disposed.
myController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Retrieve Text Input')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: myController,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
// Retrieve the text the that user has entered by using the
// TextEditingController.
content: Text(myController.text),
);
},
);
}
},
child: const Text('Submit'),
),
),
],
),
),
),
);
}
}
13.6.4 Adaptive


In Flutter there is no easy way to create an adaptive design, i.e. a design that is good for large screens and small screens, for touch and mouse. Basically, you need to program for each device individually and use if-statements to apply one or the other. The following attributes are relevant for your UI
- desktop vs. web vs. mobile
- use adaptive widgets for iOS and material, e.g. Radio.adaptive, for more see Adaptive UI Widgets
- width/height
- screen density or device pixel ratio
- text size, the user might increase the font size for better reading
- left to right or right to left text due to different localization
import 'package:flutter/foundation.dart';
...
if(kIsWeb || Platform.isMacOS || Platform.isLinux || Platform.isWindows) {...}
final size = MediaQuery.sizeOf(context);
if (size.width > 1024) {...}
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
if (devicePixelRatio >= 2) {...}
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final direction = Directionality.of(context);
Tip
13.6.5 Animation
Flutter makes it easy to enhance your user experience with animations. You can find many great resources and videos on the Introduction to Animations page.
13.6.6 Accessible
Accessibility isn't merely a matter of compliance, but a fundamental principle of inclusive design, ensuring that your applications are usable and enjoyable for everyone, regardless of their abilities. By embracing accessibility, you enhance the user experience for all and, crucially, empower individuals with disabilities to navigate and interact with the digital world more effectively.
13.6.6.1 Core Aspects of Accessible App Design and Implementation
- Color and Contrast: Ensuring sufficient color contrast is crucial for visual accessibility. For individuals with visual impairments, including color blindness, poor contrast can make content unreadable. You must meticulously check the contrast ratios of your text against its background, as well as interactive elements. Tools and guidelines exist to help you meet the necessary contrast standards, ensuring readability for all users.
- Font Size and Layout with Large Fonts: Users should be able to adjust font sizes. Your application's layout must adapt gracefully to larger font settings without breaking or becoming unreadable. Test your UIs rigorously with significantly increased font sizes (e.g., 200% text scale, as mentioned below) to ensure a seamless experience for users who require larger text.
- Semantics for Meaningful Interactions: For users relying on screen readers or other assistive technologies, the semantic meaning of UI elements is paramount. Flutter's
Semantics
widget is your primary tool for providing this crucial context. - Target Area / Hit Size: Small interactive elements are a major barrier for users with motor skill impairments or those who have difficulty with precise touch. Ensure that all interactive elements, such as buttons and tappable areas, have a sufficiently large target area or "hit size (min 48)." While the visual representation of an icon might be small, its underlying tappable region should be generous to accommodate a wider range of motor abilities and improve usability for everyone.
- Visual Hierarchy and Organization for Screen Readers: The overall visual hierarchy of your application needs to be reflected in its accessibility tree for screen readers. Organize your content logically, ensuring that the navigation order for screen readers makes sense and flows naturally. Group related elements and use appropriate headings so that a user who cannot see the screen can easily understand the structure and flow of your application.
13.6.6.2 Inspecting and Enhancing Accessibility
Thorough inspection and testing are crucial for identifying and rectifying accessibility issues. - Accessibility Scanner and Similar Tools: Leverage dedicated tools like the Accessibility Scanner - You can download its APK to audit your application on Android devices. - 200% Text Scale Testing: During your inspection, specifically test your app with a 200% text scale. - Development-Time Accessibility Checks: Integrate accessibility checks directly into your development workflow. The accessibility_tools` package.
13.6.6.3 Standards and Guidelines
Your efforts in accessible app development are guided by established international and regional standards
- WCAG Criteria: Familiarize yourself with Die WCAG-Kriterien. These widely recognized guidelines provide a comprehensive framework for making web content, and by extension, mobile applications, accessible to people with disabilities. Understanding WCAG will equip you with a robust set of principles to guide your design and development decisions.
- EU Accessibility Directive: Be aware of regional legal requirements, such as the [EU-Barrierefreiheitsrichtlinie] (EU Accessibility Directive) (https://onlinebarrierefrei.de/gesetzliche-anforderungen-eu-richtlinie/
). This directive mandates accessibility for a broad range of products and services. Specifically, new products and services must meet the requirements of this directive from June 28, 2025. This applies to various items including computer operating systems, smartphones, tablets, and online shops. It's important to note that there are exceptions for micro-enterprises and in cases where implementation would pose a disproportionate burden.
The Wonderous app (https://github.com/gskinnerTeam/flutter-wonderous-app
) is an open-source Flutter project that demonstrates best practices in various aspects of app development, including accessibility.
podcast
13.7 State Management
In the State section, we introduced the concept of state. For a single-page application with a simple structure, using setState
can be sufficient. However, when we need to share data between multiple widgets or screens, a more sophisticated state management solution is required.
13.7.1 InheritedWidget
InheritedWidget offers a straightforward method for accessing data from a shared ancestor. It is particularly useful when you need to share data across multiple widgets in a subtree without having to pass it down manually at each level. This can simplify state management and make your code more maintainable. You can utilize InheritedWidget to create a state widget that wraps a common ancestor in the widget tree, as demonstrated in this example:

Question
- Describe the term state in your own words.
- Sketch an app with two screens and some widgets and its data flow and think, in which widget you might need which shared data.
Unlike many other frameworks where state management and overall architecture are integrated components, Flutter does not impose a specific architecture or state management solution. Consequently, there are multiple effective architectures and state management approaches available for Flutter.
There are several approaches to managing app state, as detailed in this guide on different state management options.
However, we now have the new architectural guidelines, which utilize the plain Provider package for dependency injection and ChangeNotifier (included in the Flutter SDK) to implement the observer pattern.
13.7.2 Suggested Approach
Based on Simple app state management, use this three-step approach:
13.7.2.1 Step 1: Create Your Model with ChangeNotifier (Observable/Publisher)
ChangeNotifier enables objects to notify listeners when the models state changes. Call notifyListeners()
after state modifications.
class CartModel extends ChangeNotifier {
/// Internal, private state of the cart.
final List<Item> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// The current total price of all items (assuming all items cost $42).
int get totalPrice => _items.length * 42;
/// Adds [item] to cart. This and [removeAll] are the only ways to modify the
/// cart from the outside.
void add(Item item) {
_items.add(item);
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners(); // Triggers widget rebuilds of listeners
}
/// Removes all items from the cart.
void removeAll() {
_items.clear();
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
}
Counter Example
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
13.7.2.2 Step 2: Register Models in main()
Use MultiProvider to make models available throughout your app:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
ChangeNotifierProvider(create: (context) => CounterModel()),
],
child: const MyApp(),
),
);
}
13.7.2.3 Step 3: Listen to Changes in Widgets
Option A: Consumer Widget Wraps only widgets that need to rebuild when the model changes:
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);
Option B: context.watch()
Listens to model changes directly:
// code source: https://github.com/flutter/samples/blob/main/provider_shopper/lib/screens/cart.dart
@override
Widget build(BuildContext context) {
var cart = context.watch<CartModel>();
return ListView.builder(
itemCount: cart.items.length,
itemBuilder:
(context, index) => ListTile(
leading: const Icon(Icons.done),
trailing: IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () {
cart.remove(cart.items[index]);
To call model methods without listening to changes, use context.read<CounterModel>().increment()
.
Handling Loading States For asynchronous operations, add loading indicators:
Widget build(BuildContext context) {
final cart = context.watch<CartModel>();
return cart.isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) {
final item = cart.items[index];
...
No StatefulWidget Needed
With Provider, state updates happen in the model via notifyListeners()
. No need for setState()
or StatefulWidget
.
Provider Organization
Create separate providers for unrelated data. Group related data in the same provider for efficiency.
Alternative: You can also use ListenableBuilder for listening to model changes.
13.7.3 Notifier and Provider
ChangeNotifier isn't your only option for notifier. Choose based on your needs:
- ValueNotifier: Use for single values (int, bool, String) that change immediately without async operations. Perfect for simple UI controls like toggles or counters.
- ChangeNotifier: Use for multiple related values or when you need async operations with loading/error states. Gives you full control over when the UI updates.
Both work with ChangeNotifierProvider.
- FutureProvider: Use for data that loads once at app startup and doesn't change afterward. No loading states needed in your UI since the data is static once loaded.
See main_demo_different_providers.dart in Moodle for implementation examples.
13.8 Navigation
In mobile applications, users typically move between different screens as they use the app. There are two main types of navigation to understand:
- Root Navigation: These are the main screens users can access directly, often through a bottom navigation bar (like "Home", "Search", "Settings"). Think of these as the main sections of your app.
- Stack Navigation: When users move from one screen to another within a section, Flutter uses a "stack" system - much like a stack of cards. When you open a new screen, it's placed on top of the stack (called "pushing"), and when you go back, the top screen is removed (called "popping"), see figure below.


Basic Navigation in Flutter, see also Send data to a new screen and Return data from a screen
/ Push: Open a new screen
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
// Push with data: Open a new screen and pass information to it
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(todo: todos[index]),
),
);
// Pop: Return to previous screen
Navigator.pop(context);
// Pop with data: Return to previous screen with some result
Navigator.pop(context, "Some result data");
// Waiting for result when screen returns
final result = await Navigator.push(...); // Will contain the data passed to pop
For more complex apps, especially those with bottom navigation bars, Flutter developers often use a package called go_router.
graph TD
A[Products]
A --> B[Details]
B -->A
C[Playground]
D[Settings]
- Define your routes (like a map of your app's screens):
abstract class Routes {
// Base routes
static const String products = '/products'; // This is now our home route
static const String playground = '/playground';
static const String settings = '/settings';
// Product details route is nested under products
static const String productDetails = '/:id'; // works with /:id and with :id
// Helper method for product details path
static String productDetailsPath(String id) => '/products/$id';
}
- Set up navigation keys (these help Flutter keep track of navigation state):
// based on https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart
// root navigator key and for each entry in the bottom navigation bar a navigator key
final _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'home=products');
final _settingsNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'settings');
final _playgroundNavigatorKey = GlobalKey<NavigatorState>(
debugLabel: 'playground',
);
final router = GoRouter(
initialLocation: Routes.products,
navigatorKey: _rootNavigatorKey,
debugLogDiagnostics: true,
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNavigationBar(navigationShell: navigationShell);
},
branches: _bottomNavBranches,
),
],
);
final _bottomNavBranches = [
StatefulShellBranch(
routes: [
GoRoute(
path: Routes.products,
builder: (context, state) => const ProductsPage(),
routes: [
GoRoute(
path: Routes.productDetails,
pageBuilder: (context, state) {
final id = state.pathParameters['id']!;
// more transitions see https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/transition_animations.dart
return CustomTransitionPage(
child: ProductDetailsPage(id: id),
transitionsBuilder:
(_, animation, _, child) =>
ScaleTransition(scale: animation, child: child),
);
}
),
],
),
],
),
StatefulShellBranch(
navigatorKey: _playgroundNavigatorKey,
routes: [
GoRoute(
path: Routes.playground,
builder: (context, state) => const PlaygroundPage(),
),
],
),
StatefulShellBranch(
navigatorKey: _settingsNavigatorKey,
routes: [
GoRoute(
path: Routes.settings,
builder: (context, state) => const SettingsPage(),
),
],
),
];
class ScaffoldWithNavigationBar extends StatelessWidget {
const ScaffoldWithNavigationBar({
required this.navigationShell,
Key? key,
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavigationBar'));
/// The navigation shell and container for the branch Navigators.
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: NavigationBar(
destinations: <NavigationDestination>[
NavigationDestination(icon: Icon(Icons.home), label: 'Products'),
NavigationDestination(icon: Icon(Icons.play_arrow), label: 'Playground'),
NavigationDestination(
icon: Icon(Icons.settings), label: 'Settings'),
],
selectedIndex: navigationShell.currentIndex,
onDestinationSelected: (int index) => _onDestinationSelected(context, index),
),
);
}
void _onDestinationSelected(BuildContext context, int index) {
navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
);
}
}
- Finally, connect everything in your main app by using
MaterialApp.router
class MainApp extends StatelessWidget { const MainApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( ... routerConfig: router, ); } }
13.9 Libs
There are many packages available at pub.dev. To select a package check for
- last updated -- is it still maintained?
- many contributors -- will it be available in the future, because a company is involved or many people?
- is it popular -- many likes and hence many examples?
13.10 Persistence
After restarting an app you might need data to be persistent. We distinguish the data on your device (client) from the data shared between devices and or users (server).
13.10.0.1 Client
On the client there are two options to save data on all platforms
- small amount of data may be stored as key-value-pairs using the package shared preferences
- or in a file, see Read and write files
- larger amount of structured data should be stored
- a SQLite database
- a non-SQL database using sembast or isar -- the students of the last course preferred sembast.
13.10.0.2 Server
The communication with the server is based on REST and hence typically on https-calls.
Cite
"The HTTP API is CRUD (Create, Retrieve, Update, and Delete):
- GET = “give me some info” (Retrieve)
- POST = “here’s some update info” (Update)
- PUT = “here’s some new info” (Create)
- DELETE = “delete some info” (Delete)
- PATCH = The HTTP method PATCH can be used to update partial resources. For instance, when you only need to update one field of the resource, PUTting a complete resource representation might be cumbersome and utilizes more bandwidth."3
You may either use the dart package http or the package dio with a global configuration and some options to pass data and files more easily.
You need to configure your app to access the internet, see network access for Android or macOS.
Next implement the repository pattern described earlier.
13.10.0.2.1 Firebase
For real time application you might consider to use Firebase. - Codelab: Get to know Firebase for Flutter - firebase data model - Flutter & Firebase Auth on macOS: Resolving Common Issues
13.10.1 Asynchronous Programming
The following snippet shows the following principles for asynchronous programming and is a summary of Codelabs: Asynchronous programming: futures, async, await and Asynchronous programming: futures, async, await.
- Future: A future represents the result of an asynchronous operation, and can have two states: uncompleted or completed.
- async/await: The async and await keywords provide a declarative way to define asynchronous functions and use their results. An async function runs synchronously until the first await keyword. This means that within an async function body, all synchronous code before the first await keyword executes immediately.
Future<void> printOrderMessage() async {
print('Awaiting user order...');
var order = await fetchUserOrder();
print('Your order is: $order');
}
Future<String> fetchUserOrder() {
// Imagine that this function is more complex and slow.
return Future.delayed(const Duration(seconds: 4), () => 'Large Latte');
}
build method async
The build
method can be called up to 60-120 times per second, so it is important to avoid running long or expensive operations inside it.
WidgetsFlutterBinding
When you need to interact with platform-specific code or perform asynchronous operations before the app's main widget tree is built, you need to call WidgetsFlutterBinding.ensureInitialized()
void main() {
WidgetsFlutterBinding.ensureInitialized();
// now initialize firebase or do other async
...
For real time applications you will need streams. For further explanations and code snippets see Asynchronous programming: Streams.
13.10.2 Dio
Dio is an alternative lib to http. Offering more configuration options. With pretty_dio_logger you get nice logging information.
13.10.3 Freezed
Freezed is widely used for writing Data-Transfer-Objects (DTO) and or Domain Models. If you use it, you need to write only a view lines of code, to define data classes including serialization to JSON and back.
The following code shows an example of a person class.
part 'filename.freezed.dart';
part 'filename.g.dart';
@freezed
class Person with _$Person {
const factory Person({
required int id,
@JsonKey(name: 'last_name') required String lastName,
String? firstName,
@Default([]) List<Person> friends,
}) = _Person;
factory Person.fromJson(Map<String, Object?> json) => _$PersonFromJson(json);
}
In your repository you may now write
final person = Person.fromJson(json.decode(response.data!) as Map<String, Object?>);
...
final response = await dio.put<String>(url, data: person.toJson());
The build runner needs to run in the background, and you have to follow the naming convention.
Depending on your backend, e.g. with Retool, you might need to remove the id field with a post query (e.g. create a new person), this can be done with the following extension method
extension JsonWithoutId on Person {
String toJsonWithoutId() {
final map = toJson();
map.remove('id');
return json.encode(map);
}
}
...
final response = await dio.post<String>(url, data: person.toJsonWithoutId());
13.11 Testing
Testing is not covered in this course. Nevertheless, you need it in real apps.
13.12 Packages & VS Code Extensions
The following list was given by Verena Zaiser and complemented by the students during a talk in December 23 at the h_da.
- Preferences
- fix on save and others see settings.json
- Packages & Tools
- ui
- animations
- openapi_generator
- patrol - access native features of the platform
- fluttium - user flow testing tool
- faker - generating fake data
- alchemist - make golden testing in Flutter easier
- sentry_flutter - support to native crashes
- VS Code Plugins
- Coverage Gutters - Display test coverage generated by lcov or xml in your editor.
- Awesome Flutter Snippets
- Build Runner
- Dart Barrel File Generator
- Dart Data Class Generator
- Dart Code Metrics
- Flutter Color
- Flutter Coverage
- Pubspec Assist
- Pubspec Dependency
- Version Lens
- Android Studio Extensions
- Other
Slivers
- Flutter Version Manager
- Firebase Alternatives
- [Supabase](https://supabase.com/
- AppWrite
Flutter für Dummies
You may also checkout mason and brickhub for a quick start of projects and/or features.
Further libs I find useful - cached_network_image: A flutter library to show images from the internet and keep them in the cache directory.
13.13 Architecture and Design Principles -- optional
13.13.1 Repository Pattern
The repository pattern is commonly used in mobile development, especially for apps that retrieve data from a server via an API. Additionally, data may be stored locally, particularly in offline-capable apps. In a layered architecture, it's crucial that the domain and view layers don't manage the logic of how or where data is fetched (whether from a local source or a server). This responsibility is delegated to the data layer. Below is an example of a typical three-layered architecture.


"Repository classes are responsible for the following tasks:
- Exposing data to the rest of the app.
- Centralizing changes to the data.
- Resolving conflicts between multiple data sources.
- Abstracting sources of data from the rest of the app.
- Containing business logic." Data Layer
13.13.2 Riverpod
State management is a major issue in reactive programs and in flutter. Flutter suggests in its first codelab to use provider.
A provider is basically a piece of state, i.e. you do not want to hold every data relevant for your app in one class, but in several separated parts.
Provider
A provider holds a specific piece of your app's state. Hence, a provider "provides" a specific piece of state to any part of the app, which needs it.
For example, you have a state to handle favorites and a state for contacts and another state for login.
The package provider has some flaws, e.g. it is connected to the widget tree and hence testing is difficult. There exists a more sophisticated package called riverpod, more precisely flutter_riverpod.
"Since riverpod is more flexible than provider and does not rely on the Flutter widget tree to give objects, it can behave more naturally and allow for the representation of more complicated patterns." Unleashing Flutter Riverpod: State Management Mastery
Riverpod addresses the following problems
- UI and Logic Separation
- Asynchronous Requests
- Stream Management
- Data Caching
- Targeted Widget Rebuilding -see How I Simplified Flutter State Management using Riverpod
graph LR
A[App] --> B[ProviderScope]
A --> C[ConsumerWidget - Page 1]
C --ref---> B
A --> G[ConsumerWidget - Page 2]
G --ref---> B
B --- D[repositoryProvider]
B --- E[state1Provider]
E --ref---> D
B --- F[state2Provider]
my videos
Sometimes you read, that all providers in Riverpod are global. That is not true, every provider, i.e. every piece of state or every provided object lives in the ProviderScope. ProviderScope stores the state of all the providers we create. We may access providers using the ref-object. To create the connection between the widget tree and a provider you need ConsumerWidget, which gives you a ref-object. Thus, derive your widget from ConsumerWidget and you have access to every provider and you may share objects and data. With the ref-object it is easy to watch for changes of a piece of state and rebuild/redraw the widget with the new value. This ref-object is accessible in each provider so that even outside the widget tree every provider may access every other provider. This ref-object is basically used to read and hence access a specific provider or to observe its state.
You may use Riverpod with its generator or implement it by yourself. I suggest to use the generator.
install riverpod
Follow the instructions to use riverpod.
To use the generator a listener must run to generate the code according to your typing
dart pub run build_runner watch
13.13.2.1 Provider
There are mainly four use cases for provider
- make an object available
- make a changeable single value accessible to different parts of your app: simple state
- implement a controller/viewmodel with state and methods to change the state
- make asynchronous operations
All cases are implemented in the sample application.
13.13.2.1.1 "global object": Provider
The most basic provider gives access to an object, e.g. a repository, a http-connection or a logger, that don't change.
// define the provider, with or without the generator
// final peopleRepositoryProvider = Provider<PeopleRepository>((ref) {
// return PeopleRepository(dio: ref.read(dioProvider));// declared elsewhere
// });
@riverpod
PeopleRepository peopleRepository(PeopleRepositoryRef ref) =>
PeopleRepository(dio: ref.read(dioProvider));
13.13.2.1.2 simple state that can change: StateProvider
The most simple piece of state is just an object with a getter and a setter, e.g. you want to change a selected person or a selected city in one widget, e.g. you select an element in a list, and other widgets like details information are related to it. To do so you may define a StateProvider.
// define the provider, with or without the generator
// final currentPersonProvider = StateProvider<Person?>((ref) { return null});
@riverpod
Person? currentPerson(CurrentPersonRef ref) {
return null;
}
// set the person
ref.read(currentPersonProvider.notifier).state = person
// observe changes
final state = ref.watch(currentPersonProvider)
The following code shows the counter example with riverpod
// 1. declare a [StateProvider]
final counterProvider = StateProvider<int>((ref) {
return 0;
});
// 2. create a [ConsumerWidget] subclass
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 3. watch the provider and rebuild when the value changes
final counter = ref.watch(counterProvider);
return ElevatedButton(
// 4. use the value
child: Text('Value: $counter'),
// 5. change the state inside a button callback
onPressed: () => ref.read(counterProvider.notifier).state++,
);
}
}
13.13.2.1.3 Controller or ViewModel
To keep the UI and build methods simple, it is recommended to separate the interaction with the data layer and hence repositories. Use the following code to define a controller using the generator
@riverpod
class EditPersonController extends _$EditPersonController {
@override
FutureOr<Person?> build() {...}
// methods calling the repository methods
}
// and in a widget
final state = ref.watch(editPersonControllerProvider);
final controller = ref.read(editPersonControllerProvider.notifier);
Or the counter example (complete code)
@riverpod
class Counter extends _$Counter {
/// Classes annotated by `@riverpod` **must** define a [build] function.
/// This function is expected to return the initial state of your shared state.
/// It is totally acceptable for this function to return a [Future] or [Stream] if you need to.
/// You can also freely define parameters on this method.
@override
int build() => 0;
void increment() => state++;
}
class Home extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
child: Text('${ref.watch(counterProvider)}'),
),
floatingActionButton: FloatingActionButton(
// The read method is a utility to read a provider without listening to it
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}
Or a more complex viewmodel with async
@riverpod
class EditPersonController extends _$EditPersonController {
@override
FutureOr<Person?> build() {
state = const AsyncData(null);
return state.value;
}
// async methods to change the state
}
naming convention -- needed by the generator
Function name EditPersonController
needs to extend _$EditPersonController
.
13.13.2.1.4 Asynchronous operations
Sometimes you do not need a full controller or access to the complete repository, but you just want to fetch a list of movies or a String asynchronously.
@riverpod
Future<String> boredSuggestion(BoredSuggestionRef ref) async {
final response = await http.get(
Uri.https('https://boredapi.com/api/activity'),
);
final json = jsonDecode(response.body);
return json['activity']! as String;
}
// watching changes and show loading, error, changes
class Home extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final boredSuggestion = ref.watch(boredSuggestionProvider);
// Perform a switch-case on the result to handle loading/error states
return boredSuggestion.when(
loading: () => Text('loading'),
error: (error, stackTrace) => Text('error: $error'),
data: (data) => Text(data),
);
}
}
naming convention -- needed by the generator
Function name boredSuggestion
needs the parameter BoredSuggestionRef ref
.
The when/loading/error-part is needed often. Thus, it is recommended to put it into a custom widget, e.g. AsyncValueWidget.
Riverpod
Basically you need the annotation @riverpod
and the naming convention. You do not need to know what provider is generated in the background.
- class based notation
class Counter extends _$Counter
- function based notation
Person? currentPerson(CurrentPersonRef ref)
13.13.2.2 Edge cases
Usually the provider lives as long as someone is interested in it, e.g. as long as there is a widget watching it. Sometimes you need to stay the provider alive, to do so use the annotation with a capital R.
@Riverpod(keepAlive:true)
Sometimes, you may want to force the destruction of a provider. This can be done by using ref.invalidate, which can be called from another provider or from a widget.
For everything else read the excellent documentation.
App Initialization
For real apps check out the article How to Build a Robust Flutter App Initialization Flow with Riverpod.
13.13.2.3 REST
If your data is not on one mobile device only, but you fetch weather data from a server, exchange data with other users or do CRUD (create-read-update-delete) operations on a database in the cloud or on a server, you will read and write data asynchronously. A proper architecture realizing such an application is shown below.

13.13.3 MVVM
graph LR
A[View] -->|UI event / command| B[ViewModel]
B -->|change event| A
A -->|get data| B
B -->|read data| C[Model]
C -->|notify/change event| B
B -->|Updates| C
MVVM (Model-View-ViewModel) was first introduced by Microsoft as an adaptation of the MVC (Model-View-Controller) design pattern. While both patterns aim to separate concerns and promote maintainability, they achieve this in slightly different ways.
- Core Similarities Between MVVM and MVC
- Separation of Concerns: Both patterns aim to decouple the user interface from the underlying business logic, ensuring that the application's code is more modular and easier to maintain.
- Model: In both patterns, the Model represents the data and business logic of the application. It is responsible for data access, processing, and ensuring that the correct information is provided to the rest of the application.
- View: The View in both patterns represents the UI elements that the user interacts with. It is responsible for displaying information and receiving user input, though the way it interacts with the rest of the components differs between the patterns.
- Key Differences Between MVVM and MVC
- Controller vs. ViewModel
- In MVC, the Controller serves as the intermediary between the View and the Model. It handles user input from the View, processes it (often modifying the Model), and updates the View accordingly. The logic in the Controller is typically imperative, meaning it explicitly defines how the View and Model interact.
- In MVVM, the ViewModel acts as the intermediary between the View and the Model, but the interaction is typically more data-driven and declarative. Rather than the imperative approach seen in MVC, the ViewModel exposes data and commands that the View binds to (via mechanisms like data binding in frameworks like WPF or MAUI). The ViewModel does not directly manage user interaction; instead, it holds the presentation logic and state, providing the View with data in a form that's easier to display.
- Data Binding: One of the key features of MVVM is data binding, where the View is automatically updated when the ViewModel changes. Similarly, user interactions with the View (such as button clicks or text input) are reflected in the ViewModel through bound properties or commands. This creates a more declarative UI, as the UI is automatically synchronized with the underlying data, reducing the need for manual updates.
- Controller vs. ViewModel
The Flutter architecture guide recommends an MVVM-inspired approach, incorporating the Repository Pattern and the Command Pattern. The diagram below illustrate how these patterns integrate within the architecture.

Read and Understand
Read and understand the Guide to app architecture AND the Architecture case study.
In summary, the architecture follows a clear separation of concerns, see Communicating between layers:
- View: Presents the UI, aware of exactly one ViewModel. When created, Flutter passes the view model to the view as an argument (dependency injection), exposing the view model's data and command callbacks to the view. Often a view is a screen, i.e. a collection of widgets. But it might be a single logout-button as well.
- ViewModel: Acts as the intermediary between the View and one or more Repositories. The ViewModel contains logic and state specific to the View, and it implements the command pattern and triggers a rebuild of the view calling notifyListeners. A ViewModel belongs to exactly one view, which can see its data. A view model is aware of one or more repositories, which are passed into the view model's constructor (dependency injection).
- Repository: Fetches and stores data, abstracting the data access layer. The repository is the single source of truth. A repository can be used by many view models, but it never needs to be aware of them.
- Service: Performs specific operations (e.g., network requests) used by Repositories.
13.13.3.1 Reactive Programming
"In computing, reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change." Wikipedia
The propagation of change and the redrawing of the UI is a major issue.
Streams may be associated with a stream of data. You might know streams from reading files from a file system. You could also think of real time applications like a chat app, where you get a stream of messages. "A stream is a sequence of ongoing events (state changes) ordered in time. Streams can emit three different things: a value (of some type), an error, or a "completed" signal. The events are captured asynchronously, by defining a function that will execute when a value is emitted, another function when an error is emitted, and another function when 'completed' is emitted. "Listening" to the stream is called subscribing. The functions we are defining are observers. The stream is the subject (or "observable") being observed." IBM
The widget and element trees are sometimes described as reactive, because new inputs provided in a widget’s constructor are immediately propagated as changes to lower-level widgets by the widget’s build method, and changes made in the lower widgets (for example, in response to user input) propagate back up the tree using event handlers. Aspects of both functional-reactive and imperative-reactive are present in the framework, depending on the needs of the widgets. Widgets with build methods that consist of just an expression describing how the widget reacts to changes in its configuration are functional reactive widgets (for example, the Material Divider
class). Widgets whose build methods construct a list of children over several statements, describing how the widget reacts to changes in its configuration, are imperative reactive widgets (for example, the Chip
class). What programming paradigm does Flutter’s framework use?
13.14 Security
Flutter, while providing a robust framework, requires developers to consciously implement security best practices.
Security is not an afterthought
Integrate security considerations from the very beginning of your app's design and development lifecycle, rather than trying to patch them in later.
13.14.1 Data Encryption
data encryption
Encrypt sensitive the data using crypto or pointycastle.
For secure local storage of sensitive data (even if encrypted), consider using platform-specific secure storage solutions like flutter_secure_storage or encrypted databases like sqflite_sqlcipher.
Consider Package Maintenance
While sqflite_sqlcipher and flutter_secure_storage are popular, note their primary single-developer maintenance. For core encryption, crypto or pointycastle are robust choices.
13.14.2 Authentication & Passwords
Never store raw user passwords. Instead, always store cryptographically hashed and salted passwords using robust algorithms. Hashing is a one-way process, ensuring the original password cannot be retrieved. Salting adds randomness, protecting against rainbow table attacks.
Password hashing is usually performed on the backend server. Your Flutter app should send the plain password securely via HTTPS to the backend, which then performs the hashing and stores the hash. If you need to store a password locally for remote server access, encrypt it - e.g. use crypto - don't hash it, so it can be securely transmitted. However, never store plain passwords anywhere.
secure authentication
Read and follow the article Authentication General Guidelines.
authentication with biometrics
Make authentication simple. Use local_auth to implement authentication with biometrics such as fingerprint or facial recognition.
13.14.2.1 OAuth
OAuth 2.0 is the industry-standard protocol for authorization. For authentication, it's often extended by OpenID Connect (OIDC), which provides an identity layer on top of OAuth 2.0, issuing an ID Token for user verification. What is OAuth 2.0 gives a good general introduction.
In your Flutter app, use the lib oauth2 or oauth2_client to facilitate user login via OAuth2/OIDC and utilize obtained access tokens for API calls. For mobile apps, PKCE (Proof Key for Code Exchange) is essential for security.
Note, your Flutter app must be registered with the OAuth2/OIDC provider (e.g., Google), and after successful authorization, a deep link is needed for your app to receive the authorization code, which is then securely exchanged with the Token Endpoint for the actual tokens (including the ID Token) to proceed (e.g., send to your REST API). See the sequence diagram below.
sequenceDiagram
participant U as User 👤
participant A as Flutter App 📱
participant G_Auth as Google Auth Server 🔑
participant G_Token as Token Endpoint of Google Auth Server 🏷️
participant R_API as Your REST API 🔌
participant DB as Your Cloud DB 🗄️
U->>A: 1. Initiates "Login with Google"
A->>G_Auth: 2. Redirect to Google Login (client_id, redirect_uri, data for PKCE)
G_Auth-->>U: 3. Google Login Screen
U->>G_Auth: 4. Authenticates & Grants Consent
G_Auth-->>A: 5. Authorization Code (via deep link)
A->>G_Token: 6. Request Tokens (code, code verifier for PKCE)
G_Token-->>A: 7. Returns Google ID Token ♦️
A->>R_API: 8. Initial Login Request (♦️)
R_API->>G_Auth: 9. Fetch Google Public Keys (once)
R_API->>R_API: 10. Validate ♦️ using public keys
R_API->>DB: 11. Verify/Create User in Your DB (based on data of ♦️)
DB-->>R_API: 12. User Info/Status
R_API-->>A: 13. Return Your API's Session Token 🔹
A->>R_API: 14. Subsequent API Calls (using 🔹)
R_API->>DB: 15. Access Data in Cloud DB
DB-->>R_API: 16. Return DB Data
R_API-->>A: 17. Return JSON Data
A-->>U: 18. Display to User
The following videos give a good explanation
Exploring OAuth 2.0: Must-Know Flows Explained
OAuth PKCE | OAuth Proof Key for Code Exchange explained
13.14.3 Roles
role based access
Define roles and access rules at table or row levels to manage data permissions. Distinguish between read, edit, and delete access to ensure proper data security and authorization. Do the same for your navigation.
backend security check
Never trust the client app alone for access control—always enforce role checks on the backend.
13.14.4 API Keys
API keys grant access to backend services and external APIs. Exposing them in your client-side Flutter code can lead to unauthorized access and abuse.
no hard codes keys
Do not embed API keys directly in your source code. When your app is compiled, these keys become easily extractable through reverse engineering.
There is an excellent article discussing different options how to handle API keys in your flutter app How to Store API Keys in Flutter: --dart-define vs .env files.
13.14.5 HTTPS
Always use HTTPS/TLS: Ensure all communication between your Flutter app and backend servers, or any external APIs, is encrypted using HTTPS (TLS). This prevents eavesdropping and man-in-the-middle attacks.
Flutter's http package and Dio automatically handle HTTPS when you use https:// URLs.
13.14.5.1 Cross-Origin Resource Sharing (CORS)
CORS is a browser-level security mechanism that restricts web pages from making requests to a different domain than the one from which they originated.
When your Flutter application is deployed as a web app and attempts to fetch data from a backend server on a different domain, port, or protocol, the browser will typically block these requests unless the server explicitly allows them via CORS headers. This results in common errors like "No 'Access-Control-Allow-Origin' header is present on the requested resource.
The most secure and recommended approach is to correctly configure your backend server.
Specific Origins Over Wildcards
Avoid Access-Control-Allow-Origin: * in production environments due to security risks. Instead, explicitly list the specific origins of your frontend applications.
Within this module you might run into this problem and solve it using flutter_cors.
Never disable CORS checks in production
Disabling CORS checks is acceptable for this module, but you should never do it in a production environment.
Read the Mozilla Developer Docs to learn more on Cross-Origin Resource Sharing (CORS).
13.14.6 Code Quality
Beyond specific security implementations, adopting sound development practices significantly contributes to a safer application. This includes leveraging language features like null safety to prevent common programming errors that could lead to vulnerabilities. A well-defined architectural pattern (e.g., BLoC, Provider, Riverpod) promotes code organization, testability, and separation of concerns, making it easier to identify and fix security flaws. Comprehensive unit and integration tests are crucial for verifying that security mechanisms function as intended and that data is handled correctly under various scenarios, reducing the risk of regressions and undiscovered vulnerabilities.
Furthermore, regularly updating dependencies to their latest stable versions is critical -- see Flutter Docs Security. These updates often include security patches for newly discovered vulnerabilities. Minimize the number of libraries used and carefully evaluate their quality, maintenance status, and the number and identity of their contributors or publishers.
While staying current, always specify a reasonable minimum SDK version (minSdkVersion for Android, iOS Deployment Target for iOS) to ensure your app runs on devices that still receive security updates from their respective OS vendors, thereby minimizing exposure to platform-level vulnerabilities.
13.14.7 Evaluation
Create a comprehensive test guide for your app that addresses both user experience (UX) and security concerns. Conduct regular tests to ensure ongoing ux and safety.
Mobile App Test Guide
Check the instructions of the Mobile App Test Guide.
In addition add security issues to your UX evaluation
- Measure user success objectives on UX security-related tasks.
- Test how users perform essential security actions, like managing permissions or changing passwords without confusion or heavy cognitive load4.
13.14.8 Data Protection (Datenschutz)
Data protection, or "Datenschutz" in German, refers to the legal and ethical handling of personal data. Compliance with regulations like GDPR (General Data Protection Regulation) is crucial, especially if your app targets users in the EU. Read and understand the key principles of GDPR.
Practical Implications for Apps:
- Privacy Policy: Clearly state what data your app collects, why, how it's used, and with whom it's shared. Make it easily accessible within the app.
- User Consent: Obtain explicit consent before collecting or processing personal data, especially for non-essential data. Provide clear opt-in/opt-out mechanisms.
- Data Subject Rights: Allow users to view, correct, delete and export their data.
- Data Minimization: Only request permissions and collect data that is strictly necessary for your app's core functionality.
13.14.9 Security vs. Usability
- Clear Communication: Explain security features and data practices in plain language. Avoid jargon.
- Intuitive Controls: Make privacy settings and security options easy to find and understand. Test it with real users.
- Feedback: Provide clear feedback when security actions are taken (e.g., "Password changed successfully," "Data deleted").
- Error Handling: Implement user-friendly error messages for security failures, guiding users on how to resolve issues without revealing sensitive details. Don't show exceptions to your users, i.e. do not provide any information about your systems, databases etc.
13.14.10 Further Reading
- The Connection Between System Security and User Experience Design - Enhancing Both for Better Performance
- Here’s How Recent Cybersecurity Lapses Are Impacting Consumer Trust and Behavior
-
Flutter architectural overview. 2024. URL: https://docs.flutter.dev/resources/architectural-overview#reactive-user-interfaces (visited on 15.02.2024). ↩
-
Common flutter errors. 2024. URL: https://docs.flutter.dev/testing/common-errors#a-renderflex-overflowed (visited on 12.03.2024). ↩
-
Sanjay Patni. Fundamentals of RESTful APIs, pages 1–15. Apress, Berkeley, CA, 2023. URL: https://link.springer.com/chapter/10.1007/978-1-4842-9200-6_1#Sec11, doi:10.1007/978-1-4842-9200-6_1. ↩
-
Sabine Roehl. Secure by design: a ux toolkit. 2025. URL: https://microsoft.design/articles/secure-by-design-a-ux-toolkit/ (visited on 11.07.2025). ↩