This tutorial gives a introduction into developing mobile applications with the Flutter SDK.
1. Flutter SDK
The Flutter SDK allows you to build native apps for iOS, Android, the web and desktop with a single codebase. It was introduced by Google in 2017 and has had its first 1.0 release on December 4th, 2018.
It is written in C, C++ and Dart. The SDK utilizes the Skia Graphics Engine to render.
Flutter is motivated by the fact that the most successful apps, like the Netflix one, looks the same on platforms like iOS and Android. They still behave different, e.g., the scroll behavior is different and Flutter supports that out of the box.
Flutter has to draw everything itself, so if for example you add a progress indicator in Flutter, it has to draw it for every update. Native component like the one used in the Android SDK may handle such updates in the native components.
2. Installing Flutter
To develop Flutter applications you need:
-
the Flutter SDK
-
the Android SDK for developing Android applications
-
XCode for developing iOS applications
Follow the installation instructions:
If you are using a Mac, please also follow the XCode installation procedure.
iOS apps can only be developed on macOS devices. |
2.1. Required Java version
Currently some part of the Android SDK requires Java 8.
2.2. Android SDK Licenses
The Android SDK requires you to sign SDK package licenses.
This is relevant for publishing the app, but $ flutter doctor
will nag you about this.
You can run the command $ flutter doctor --android-licenses
to view and accept the licenses.
If you get a Java exception when running the above command, make sure the shell you’re currently in has Java 8 available.
(check by using |
2.3. Test the Flutter installation
To test your installation run the $ flutter doctor
command.
The output should look like this:
[✓] Flutter (Channel stable, v1.2.1, on Linux, locale en_US.UTF-8)
[✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[!] Android Studio (version 3.3)
✗ Flutter plugin not installed; this adds Flutter specific functionality. (1)
✗ Dart plugin not installed; this adds Dart specific functionality. (1)
[!] VS Code (version 1.32.3)
✗ Flutter extension not installed; install from
https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter (2)
[!] Connected device
! No devices available (3)
1 | If you’re not using Android Studio for development you can ignore these errors |
2 | Configuration of the editor will be covered later |
3 | Configuring devices for debugging will be covered later |
2.4. Special Note for Linux Users
If you are on Linux (Ubuntu, Fedora, Arch, etc.) you might need to install an additional package.
The output of $ flutter doctor
will show you the following message:
✗ Downloaded executables cannot execute on host.
See https://github.com/flutter/flutter/issues/6207 for more information
On Debian/Ubuntu/Mint: sudo apt-get install lib32stdc++6
On Fedora: dnf install libstdc++.i686
On Arch: pacman -S lib32-libstdc++5 (you need to enable multilib:
https://wiki.archlinux.org/index.php/Official_repositories#multilib)
-
For Ubuntu:
sudo apt-get install lib32stdc++6
-
For Fedora:
dnf install libstdc++.i686
-
For Arch:
pacman -S lib32-libstdc++5
along with the instructions on this link: https://wiki.archlinux.org/index.php/Official_repositories#multilib
3. Setup of an Android device
3.1. Setup of an Android Virtual Device (AVD)
The emulator can be a powerful replacement of a physical Android device for testing mobile applications.
Before setting it up it is advisable to enable VM Acceleration on your development machine. This can sometimes drastically improve performance of the emulator.
Emulators can be created in Android Studio or via VSCode. The Flutter tools can start the emulator for you.
If you already have a project in Android Studio, click on
.Once open, click on + Create Virtual Device.
You will be presented with a list of devices available for emulation. These cover most of what is available. For this purpose it is not important which device you choose. Just keep in mind, that the devices have different aspect-ratios and resolutions. As a result your layout might look different to what is presented in this series.
The next step is to choose an Android version. At the time of writing this is Android Q, but for Flutter apps there is not much of a difference. For more production-ready apps however, you should check your app for inconsistencies across your supported versions.
On the next page you can name your device. It is advisable to choose a name you recognize, as you will have to choose the correct device to run your Flutter apps on.
You can close the AVD Manager now.
To check if the emulator is available, run the $ flutter doctor
command again.
There should be 1 connected device:
[✓] Connected device (1 available)
3.2. Setup of ADB on a Physical Device
Flutter allows to use your physical android device as a debug target. To enable this functionality you need to enable ADB debugging on the device. This setting can be found in the developer options of your phone.
Open the settings app on the phone and go to About Phone and tap Build Number seven times. This enables a sub menu in the settings app.
Go to Developer options at the bottom and enable USB-Debugging.
On some devices the Developer Options are located or activated differently. If unsure search for the instructions for your specific device. |
Connect your smartphone to your PC via USB.
Now run $ flutter doctor
again and there should be 1 connected device:
[✓] Connected device (1 available)
On Windows, you might have to restart your PC first.
On some devices you will first have to accept the connection by your PC. |
4. Development environment
Flutter can be developed by only using a text editor and the command line.
More advanced support is delivered by the following tools:
We currently recommend Visual Studio Code as the development tool, as it is fast, stable and lightweight. |
4.1. Visual Studio Code
Visual Studio Code is a lightweight text editor with support for a wide-array of plugins.
To install it, follow the instructions on https://code.visualstudio.com/ for your platform.
For Flutter development there are two plugins required:
By clicking each link above Visual Studio Code will open the installation page of each plugin. Install both and reload the program if prompted.
Alternatively you can click on the extension icon on the left sidebar and search for Dart and Flutter and install the extensions manually.

5. Using Visual Studio Code for Flutter development
5.1. Starting and creating an emulator via VS code
You can start or create you emulator via VS code.

Creating emulators does not always work, try "flutter emulator create" in VS Code fails and if that fails use Android Studio to create an emulator. |
5.2. Basic shortcuts
The following tables lists the most important shortcuts for using Visual Studio Code for Flutter development.
Shortcut | Description |
---|---|
Ctrl+. |
Quick fix for solving issues |
Ctrl+Space |
Content assist/ code completion |
Ctrl+Shift+p |
Execute command |
F5 |
Start Flutter application |
5.3. Editing shortcuts
Shortcut | Description |
---|---|
Ctrl+Shift+i |
Format source |
Ctrl+x |
Cut line |
Ctrl+k+c |
Comment selected code |
Ctrl+k+u |
Uncomment selected code |
Ctrl+Shift+k |
Delete line |
5.4. Code standards
The Dart linter allows you to define rules for your code which are analyzed by the Dart SDK.
The rules are configured via a analysis_options.yaml
file which for example could contain the following rules.
linter:
rules:
- annotate_overrides
- hash_and_equals
- prefer_is_not_empty
See https://github.com/dart-lang/linter for the available rules. See https://github.com/tenhobi/effective_dart for the rules which comply with the Dart effective guide.
5.5. Helpful extensions
*Awesome Flutter Snippets provides additional templates for Flutter development
6. Exercise: Creating a new Project from the command line
6.1. Creating the Project
Alternatively to this approach, you can also generate a new app via Visual Studio Code. |
On the command line create a new folder and switch to this folder.
Create a new Flutter project called hello_world using the following commands.
flutter create hello_world
By default Flutter uses Kotlin for the Android native code.
Use
|
6.2. Run the Project
cd first_app
flutter run
This requires that you have already created and started a virtual or physical device. |
6.3. Review the code and change the application logic
Open the main.dart
file located in the lib
folder in a text editor or Visual Studio Code.
Change the application logic to increase the counter by 2 instead of 1 if you click the button.
7. Add output to the console
Frequently we need to output a value to the Debug console.
Flutter provides the print
method for this purpose.
Add a print output statement somewhere in the code and ensure that you see the output in the Debug Console of Visual Studio Code.
int help = 5;
print("Hello $help");
8. Optional: Create and review some of the sample applications
The Flutter command line tooling allows you to create sample applications which demonstrates certain behavior. Use the following command to create a file containing a description of the available templates.
flutter create --list-samples test.json
To create one of these samples, use
flutter create --sample=widgets.SingleChildScrollView.1 mysample
9. Exercise: Creating a new Project in Visual Studio Code
9.1. Creating the project
Open VSCode and invoke the command palette via
or Ctrl + Shift + P. Select Flutter: New Project.
Use flutterui as name for your project.

Select a folder where the project should be saved.
VSCode restarts and after it restarted it will creates the project.
Your explorer on the left should look similar to this:

If the project is not automatically created, make sure the Dart and Flutter extensions are installed and enabled.
|
9.2. Ensure you have a device available
On the right side of the status bar at the bottom of VSCode you see multiple information about your development environment. One of these indicators shows the current status of the target device (or emulator):
If no device is currently available you can click on the No Devices button.

A pop up opens that lets you choose which device you want to develop on. The list contains the emulators as well as any real device you have configured for development.
Choose one of the listed devices. If the target device is an emulator wait for it to fully start.
9.3. Start the application
Select
(or use the default shortcut:[F5)] to build, deploy and start the application.
Depending on your Internet connection and host device speed the initial build could take several minutes.
You can see the current step/progress in the builtin DEBUG CONSOLE of VSCode.
If it did not open automatically you can open it using the command Debug: Focus on Debug Console View
|
After the build process finished the app appears on your target device:
Tap 3 times on the + in the bottom right.
Now search for the primarySwatch color setting in the source code and switch the color to yellow. Notice how the number of times you clicked the button stayed the same but the color changed. This a feature of the Flutter toolkit that allows state to be persisted between hot reloads.
You can trigger a hot reload manually by using the respective button in the debug toolkit at the top of you VSCode instance:

9.4. Change the styling of the application
In the ThemeData
of you main.dart
file change Colors.blue
to Colors.red
.
Save and see that the emulator switches the color immediately while it preseves its state (the count).

9.5. Review the code
Have a look at the generated code. Press the Control key and click on the Scaffold widget. This jumps into the Dart source code and allows you to review the documentation of this class.
10. Flutter Applications
The main entry point for Flutter apps is the lib/main.dart
file.
This file contains a main()
method that calls the runApp(…)
method of the Flutter framework.
This method creates the widget hierarchy.
The top level widget created by the runApp(…)
method is the root of your widget hierarchy.
Typical you create a MaterialApp
from the build(BuildContext context)
method of the root widget.
MaterialApp
can be used to:
-
set routes (navigation)
-
set the theme
-
and various other default settings.
The following listing shows a main.dart
file:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp()); (1)
class MyApp extends StatelessWidget {
// This widget is the root of your application
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: SimpleWidget(title: 'Flutter Demo Home Page'), (2)
);
}
}
class SimpleWidget extends StatelessWidget {
final String title;
SimpleWidget({Key key, this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Text('Hello'),
);
}
}
1 | The main method is called on startup |
2 | The widget initially shown in the material app |
10.1. Widgets
In Flutter every UI Element is a widget and has the base type Widget
.
The Flutter library provides lots of standard widgets. These range from button implementations to more complex widgets like lists or stacks. Widgets can be customized using properties and may have one or several children.
See widgets overview page for comprehensive list of widgets. |
10.2. Stateful- vs. StatelessWidgets
Flutter provides StatelessWidget
and StatefulWidget
widgets.
-
StatelessWidget - used when there is no mutable state in this widget
-
StatefulWidget - used when the widget manages mutable state
10.2.1. StatelessWidget
The StatelessWidget
is used when the widget does not have any mutable state.
Hence every field in it should be marked as final
.
The Dart compiler does not enforce that all fields are marked as final .
So it is possible to use non-final fields in StatelessWidgets.
However, changing their values can result in negative site-effects.
|
Consider this simple example:
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Homepage(count: 10),
);
}
}
class Homepage extends StatelessWidget {
final int count; (1)
Homepage({this.count});(2)
@override
Widget build(BuildContext context) {
return Text('Pressed: $count');
}
}
1 | the count field was marked as final , therefore the Dart compiler does not allow any changes at runtime |
2 | count is defined as an optional parameter |
10.2.2. StatefulWidget
A stateful widget extends StatefulWidget
.
A widget’s state consists of properties of its class.
A stateful widget must provide a second class for the state.
This state class extends State<T>
with T
being the type of the corresponding StatefulWidget
.
The created state class is exclusive to T
and cannot be used in other StatefulWidget
implementations.
You have to use the setState(…)
function to update the state.
This method is available in all State<T>
classes.
setState(…)
takes a function (VoidCallback
) as an argument.
In this VoidCallback
you can modify the widget’s properties.
Flutter will then re-render all impacted widgets.
A stateless widget can contain stateful widgets and vice-versa.
The following is an example for an application using a stateful widget.
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Homepage(),
);
}
}
class Homepage extends StatefulWidget {
@override
_HomepageState createState() => _HomepageState();
}
class _HomepageState extends State<Homepage> { (1)
int count = 0; (2)
@override
Widget build(BuildContext context) {
return MaterialButton(
child: Text('Pressed: $count'),
onPressed: () => setState(() => count++), (3)
);
}
}
1 | _ (an underscore) marks the class as private |
2 | this attribute isthe state of the widget in this example |
3 | In setState(…) we update the value of the count field.
Flutter then updates the relevant widgets. |
Flutter takes care of a lot of the state related operations for you. In other words: You don’t have to care about which (nested) widget needs to be updated or repainted, etc.
There are libraries and patterns that allow for more complex state management. Some of them will be covered in a later chapter. |
10.2.3. Use a StatelessWidget instead of extending existing widgets
Sometimes it might seem intuitive to just extend a Button
widget if you want to generalize an often used configuration.
But in this case you should rather use a StatelessWidget
and return a Button
with the desired configuration.
10.3. Using Keys
Key are used to identify StatefulWidgets
.
Keys are not required for StatelessWidgets s, as they don’t have any state associated with it.
This means they can be recycled without messing up the state.
|
So in case StatefulWidgets
of the same type are dynamically reordered, added or removed you need to use keys.
This is necessary so that Flutter can reconnect the state object with the correct element in the widget tree.
An example would be a list in which you can reorder list items.
The key should be placed on the top of the widget tree which should be preserved.
You can create keys with the UniqueKey()
method or if it based on your data you can use ValueKey(yourdata.yourid);
For a detailed overview of using keys, see https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d.
11. Important Widgets
11.1. Scaffold
The Scaffold
widget provides convenient APIs for displaying common Material widgets like drawers, snack bars, bottom sheets and floating actions buttons.
It sets various defaults (like theming), provides direct properties for a host of different child widgets and much more.
An example of a Scaffold could look like this:
Scaffold(
appBar: AppBar(
title: Text('App title')
),
body: Text('A body'),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add)
),
bottomNavigationBar: BottomNavigationBar(
items: []
)
)
Property | Type | Description |
---|---|---|
|
|
An |
|
|
The content of your page |
|
|
A round button, commonly located near the bottom right of the screen. Typically used as a contextual action button for frequently performed actions. It is often referred to as a FAB. |
|
|
A bar of actions at the bottom of the screen. Typically, this is an omnipresent navigation bar that provides access to other pages of the app. |
Here is an example for a Scaffold
widget with an app bar.
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(
Icons.menu,
),
onPressed: () {},
),
title: Text("Hello"),
actions: <Widget>[
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'search',
),
onPressed: () {
print('Search button');
},
),
IconButton(
icon: Icon(
Icons.filter_list,
semanticLabel: 'filter',
),
onPressed: () {
print('Filter button');
},
),
],
),
// TODO: Add a grid view (102)
body: Center(
child: Text('Scaffold with AppBar'),
),
resizeToAvoidBottomInset: false,
);
}
}

There are quite a few more settings related to the positioning, theming and other behavior, which will be covered more in depth later.
As the scaffold shows the app bar and other structurally important parts of the app you typically want to do any dynamic loading inside of the body property instead of wrapping the Scaffold .
|
11.2. Container
The container widget is a simple container that can receive one child. A container widget without children tries to be as big as possible.
It can also have a background color and can be styled using its decoration
property.
As its height and width can also be set directly this can be used if you are certain about the constrains of the element.
Container(
margin: EdgeInsets.all(10.0),
child: Text('Text'),
width: 150.0,
height: 150.0,
color: Colors.green
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5.0),
),
)
This would render a 150 by 150 pixel big, green box with the text "Text" displayed in it.
If a container widget is placed in a widget with unlimited space, like a |
11.3. Text and TextFields
-
Text
is a label which shows some text -
TextField
allows the user enter text
11.4. Buttons
Flutter provides multiple default button implementations
-
RaisedButton - a button with a background color, used to represent the prominent button.
-
FlatButton - a text button
-
IconButton - a button with an icon, typical used in a application bar, has no child property but a icon property
-
OutlineButton
-
FloatingActionButton
-
ToogleButton
A layout should contain a single prominent button. This makes it clear that other buttons have less importance. This prominent button represents the action we most want our users to take in order to advance through our app.
These buttons typically have the following properties:
* child
: contains the Widget for this button, frequently a Text
widget
* onPressed
: takes a method reference that will be executed when the button is pressed.
To group buttons you can use the ButtonBar
which is similar to a Row
but automatically wraps its children if their is not sufficient space.
Buttons which have no onPressed property set or only null assigned are disabled by the Flutter framework.
11.5. Images
The Image
widget provides helper methods to create images from several sources.
-
Image.assert
-
Image.network
For example, Image.network("https://picsum.photos/id/10/200/200");
creates an image from the given URL.
By default, Image
does not provide caching or a placeholder image until it is loaded.
Use the cached_network_image
package for that.
This requires that you add a dependency to this library in you pubspec.yaml.
CachedNetworkImage(
placeholder: (context, url) => CircularProgressIndicator(),
imageUrl: 'https://picsum.photos/id/10/200/200',
);
Some widgets require an ImageProvider
, e.g. NetworkImage
.
You can use a ColorFiltered
widget to change the color of an image.
For example the following grays out an image.
ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.grey,
BlendMode.saturation,
),
11.6. Using GestureDetector
With a GestureDector
you can register a click listener to any widget.
Contain
GestureDetector(
onTap: () {
print("onTap called.");
},
child: Text("foo"),
),
11.7. Flexible, Column, Row
The widgets of the Flex
family are widgets that allow putting multiple widgets next to each other on the same axis.
The Column
and Row
widgets are specialized versions of the Flex
widget.
They set the direction
attribute of the Flex
constructor set to Axis.vertical
and Axis.horizontal
respectively.
So if you know in what direction the widget will layout its children, use the Column
or Row
directly.
All of these widgets also take a <Widget>[…]
(list of Widget
) as a constructor parameter.
In here you can add all the widgets that should be displayed.
Column lays out its children vertically.
Column(
children: <Widget>[
Text('I will be above'),
Text('I will be below'),
]
)
Row lays out its children horizontal.
Or for the Row
widget:
Row(
children: <Widget>[
Text('I will be left'),
Text('I will be right'),
]
)
Flexible (or its specialized version of Expanded) allow to distribute the available space based on the weight defined by the flex
attribute.
Default for the flex attribute is 1.
Column(
children: <Widget>[
Flexible(
child: Container(color: Colors.black, child: Text("Hello")),
flex: 2,
),
Flexible(
child: Container(color: Colors.red, child: Text("Testing")),
flex: 1,
)
],
)
You can control the actual direction of the list by setting the mainAxisAlignment property.
|
11.8. Layout related widgets
11.8.1. Wrap
If there is not enough space to display all children, Flutter will show an error.

The Wrap
widgets wraps its children, if there is not enough space.

11.8.2. SizedBox, FittedBox and Spacer
SizedBox
can be used to force a widget into a certain size or to add some fixed sized space between widgets.
Spacer
can be used to allocate empty space.
FittedBox
can be used to resize a SizedBox
based on give constraints from its container
11.9. ListView, GridView and ListTile
The ListView
and GridView
widgets allow to show multiple widgets in a list or in a grid.
It takes care of overflowing widgets by automatically enabling scrolling if its children extend over the screen edge.
class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: _createChildren(),
);
}
}
List<Widget> _createChildren() {
return [Text("Hello"), Text("Lars"), Text("Testing")];
}
List provides also a builder to create list items.
class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (_, index)=>
Text('Item $index'),
);
}
}
You can also use a separator.
class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (_, index)=>
Text('Item $index'),
separatorBuilder: (_,index) =>Divider(),
itemCount: 100,
);
}
}
If your list you be reorderable, use the ReorderableListView
widget.
One drawback to keep in mind is that you have to know the height of a widget beforehand or else you will get exceptions as the ListView does not know how much space it should calculate. There are however solutions to this, as some specialized widgets take care of this for you.
11.10. PageView
The PageView
widget allows to swipe between different screens.
It allows to define a controller which can set the initial page.

import 'package:flutter/material.dart';
import 'listview.dart';
import 'listviewbuilder.dart';
class MyPager extends StatelessWidget {
final PageController controller = PageController(
initialPage: 1,
);
@override
Widget build(BuildContext context) {
return Container(
child: PageView(
children: <Widget>[
MyListView(), // a custom widget
ListViewBuilder(), // another custom widget
],
),
);
}
}
11.11. Loading asserts
Asserts must be declared in the pubspec.yaml.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
asserts:
asserts/events.json
Afterwards you can load them, for example to load a text file:
DefaultAssetBundle.of(context).loadString("asserts/events.json");
11.12. Flavor specific widgets
Most components are available in both Cupertino (iOS) and Material (Android) styles. But of course it is up to you, which flavor you use and not dependent on the host OS. However, it is important to keep in mind, that users of the Android platform might expect Material style apps and iOS users Cupertino styles.
Conditional styling is possible, but it increases complexity in the app and depending on the size of the app might make UI-testing a lot harder.
You can access the currently running host OS using Platform.isIOS
and Platform.isAndroid
.
12. Implementing Navigation in Flutter via Routing
Flutter supports navigation between different pages.
As everything in Flutter, a page is just a widget.
It can either be a StatelessWidget
or a StatefulWidget
.
Using different widgets which can be reached via navigations helps separating concerns, encourages encapsulation of code and thus makes the code base easier to read and maintain.
The navigation is handled by the Navigator
class.
It has different functions available that allow for either:
-
showing a page directly.
-
using named routes
This works as follows:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Route()),
);
With named routes, you pass a map of all routes to the routes
parameter of the MaterialApp
constructor.
The keys of the map should be a string with the name of the route (typically these start with a /
, similar to HTTP paths).
The value of each key is a function that takes the context of the app as an argument and returns an instance of the widget of the page.
For a larger code base it is recommended to use a central map of all named routes. This way you can always change the class that is shown on a route in one single place instead of searching the code base for occurrences of the route. However sometimes it might be useful to just show a widget as a page. |
Named routes may be pushed like this:
Navigator.pushNamed(context, '/issue')
Keep in mind though, that you have to register each route during instantiation of the app.
This could look like this:
MaterialApp(
// ...
routes: {
'/': (context) => Homepage(),
'/second': (context) => SecondPage(),
// etc.
},
initialRoute: '/', (1)
),
1 | The initial route is the first page the app shows when opening the app |
As the app grows larger this part could become very crowded. One simple way to do this is, storing these routes and their builder functions in a separate file and expose them via a map. This is very common and you might see this in existing Flutter apps. |
12.1. Passing data to a route
Sometimes you might want to pass data to a route. This could be a user id for showing their details or just something a user entered on the first screen.
For this, Navigator.pushNamed(…)
accepts an optional named argument called arguments
.
You may pass any Dart object to it but is is advisable to create a "wrapper" object that has the fields you want to pass for type safety.
Creating a wrapper object might look like this:
class RouteArguments {
final int userId;
final String message;
RouteArguments(this.userId, this.message);
}
Passing the argument to the route:
Navigator.pushNamed(context, '/second', arguments: RouteArguments(8, 'This is a message'));
To access this data in a route, you will need to add it to the constructor of the widget.
@override
Widget build(BuildContext context) {
// ...
final RouteArguments args = ModalRoute.of(context).settings.arguments;
// ...
// And later you may access its values directly
int id = args.userId;
}
You can only receive these arguments where you have a BuildContext instance.
|
Dart automatically assigns the object to your RouteArguments
type.
But keep in mind that it can’t do so, if the object passed in was not of type RouteArguments
.
In this case it would throw an exception.
12.2. Handling unknown routes
In some cases you might want to generate the routes dynamically or you want to be safe and have an unknown route screen.
Luckily there is a onUnknownRoute
handler in the MaterialApp
constructor.
This takes a function that returns a Route
(e.g. a MaterialPageRoute
or CuptertinoPageRoute
).
Its builder
should return an instance of your page.
MaterialApp(
// ...
routes: ...,
onUnknownRoute: (settings) {
return MaterialPageRoute(builder: (context) => NotFoundPage());
},
initialRoute: '/',
),
13. Managing application state in Flutter
An application needs to handle its state (data). The following explanation speaks about state, keep in mind that this is data, e.g., one or several objects stored somewhere.
While each stateful widget in Flutter can have its own state, state typically needs to be available in multiple widgets in the application. In Flutter it is common to keep the state in the hierarchy above the widgets which use it as you have to rebuild your widgets to change the UI.
As developer you can:
-
manage this state yourself via widget constructors
-
use
InheritedWidget
which is provided by the Flutter SDK -
use Provider which is an external library and currently the recommended approach for doing this
-
Using Bloc
-
Using other alternatives
13.1. Using widget constructors
Once approach to manage the state is that the root widget stores the data and pass it down the widget tree via the widget constructor. Widgets which update the state would have to call the other widgets which would lead to highly coupled widgets. It also create a lot of overhead and maintaining these kinds of apps can be difficult.
Therefore, this solution is not recommended.
13.2. Using InheritedWidget
InheritedWidget
provides a simple way of accessing data in a widget hierarchy.
The parent widget extends InheritedWidget
class MyWidget extends InheritedWidget {
const MyWidget({
Key key,
this.color,
Widget child,
}) : assert(color != null),
assert(child != null),
super(key: key, child: child);
final Color color;
static MyWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(MyWidget) as MyWidget;
}
@override
bool updateShouldNotify(MyWidget old) => color != old.color;
}
Widgets below the hierarchy can access the data via the convenient MyWidget.of(context)
method.
InheritedWidget
works fine, but a even better solution is the usage of the provider package as described in the next chapter.
13.3. Using the Provider package
The Provider
package provides a means to handle state.
Google recommended currently this approach for state management in Flutter apps.
See https://pub.dev/packages/provider for its latest version, which you can add as dependency to your pubspec.yaml
file.
It consists of providers that can provide any kind of data (from singleton services to data values that might change during the runtime of the app) down the widget hierarchy.
To provide data for all children of a widget, wrap this widget with a Provider
widget.
This widget allows to set the state via the builder
property.
If you want to provide data for your whole application, wrap your top level widget.
For example, if you want to provide one instance of UserService
in your app, you can provide an instance of this class via a Provider
widget.
@override
Widget build(BuildContext context) {
return Provider(
create: (context) => UserService(),
child: MaterialApp(
// Relevant app configuration
)
);
}
To access this instance of UserService
in another widget use the static function Provider.of<T>(context)
where T is the type of your value (in this case the UserService
).
//...
var userService = Provider.of<UserService>(context);
//...
This enables you to build your own providers for your data and consume them inside any children of your initial provider.
Keep in mind that this always requires a BuildContext which is available in any build(…) method in your State<T> or StatelessWidget classes.
|
13.3.1. Multiple Provider
In case you have multiple providers, you can use the MultiProvider
class.
It has a providers
parameter that accepts a list of providers.
To use it, wrap your MaterialApp
with a MultiProvider
.
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider(
create: (context) => UserService(),
),
Provider(
create: (context) => EventService(),
),
],
child: MaterialApp(
// Relevant app configuration
)
);
}
To access any of these providers in the widget hierarchy below its declaration, you can again use the Provider.of<T>(context)
function.
13.3.2. Change Notifiers
ChangeNotifier
which is part of the Flutter SDK and provides change notifications to its listeners.
The function notifyListeners()
can be called to notify listeners that something changed.
Other frameworks calls such a class an observable, in case you are familiar with this term.
class TaskModel with ChangeNotifier {
final List<String> _todos = [];
void addTask(String task) {
_items.add(task);
notifyListeners();
}
}
This also works with asynchronous methods.
class AuthenticationService with ChangeNotifier {
bool isAuthenticated = false;
Future<void> _authenticate(String email, String password) async {
// do authentication
isAuthenticated = true;
notifyListeners();
}
}
13.3.3. Using ChangeNotifier
with the Provider package
A class that implements ChangeNotifier
can be provided through a ChangeNotifierProvider
.
ChangeNotiferProvider(
create: (context) => TaskModel(),
child: MyApp(),
),
Changes can be listened to by Consumer<T>
widgets where T is the type of your ChangeNotifier.
Inside its builder parameter you can return other widgets based on the changed value.
@override
Widget build(BuildContext context) {
return Consumer<AuthenticationService>(
builder: (context, authenticationService, child) { (1)
if(authenticationService.isAuthenticated) {
return UserPage();
} else {
return LoginPage();
}
}
)
}
1 | The builder also receives the optional child parameter of the Consumer which can be returned inside the builder if you just want to do updates to the local state. |
13.3.4. Using the Consumer
widget
You can access your ChangeNotifierProvider
via the Consumer
widget.
return Consumer<TaskModel>(
builder: (context, tasks, child) {
return Text("Total number of items: ${tasks.size()}");
},
);
The builder is called with three arguments.
-
The first one is context, which you also get in every build method
-
The second argument of the builder function is the instance of the ChangeNotifier. You can use the data in the model to define what the UI
-
The third argument is child, which is there for optimization. If you have a large widget subtree under your Consumer that doesn’t change when the model changes, you can construct it via the
child
attribute. This will be passed here to you.
13.4. BLoC
The BLoC pattern is a little bit more Dart specific as it’s based on streams which are implemented as a first class citizen in Dart. BLoC stand for Business Logic Component. The gist of this is that business logic, the logic of your app, should be moved to so called BLoCs. This removes said logic from the presentation layer and helps separating the logic from UI.
Per definition these BLoCs should also be platform independent. This has the added benefit of being very transportable to other versions of your app (for example a Dart server backend or an AngularDart frontend). These BLoCs rely exclusively on Streams as their primary means of communication to other parts of the app.
BLoC helper packages are also available from pub.dev.
13.5. MobX and Redux
13.6. Futures
A Future<T>
is a special class that is essential for async computation in Dart.
It provides a way to delay reactions to data until the data is actually there.
Use cases for this is network communication as you most likely want to wait for the data returned from the server before proceding with your computation.
Futures are very deeply integrated into the core of the Dart language.
The async
/await
syntax can be used to "await" the result of a computation before going further in the current method’s execution.
This does not mean that the main thread is blocked while waiting. You can read more on that here: https://dart.dev/codelabs/async-await#execution-flow-with-async-and-await |
Many built-in methods of the Dart & Flutter SDKs return futures where appropriate.
A Future<T> is very similar to a Promise in JavaScript, Task<T> in C# or a CompletableFuture<T> in Java.
|
13.7. The http
Library
Network communication in Flutter can be simplified by using the http
library by the dart.dev
team.
To use the library you have to add it to the pubspec.yaml
file of the project:
dependencies:
# ... more dependencies
http: 0.12.0+2
# ...
The library contains a set of high level APIs to make working with network requests easy.
The usage of http
is pretty straightforward.
It exposes the most common HTTP operations directly and returns the responses wrapped in a Future
.
-
http.get(…)
-
http.post(…)
-
http.put(…)
-
http.delete(…)
-
For a full list refer to the official documentation.
This is useful as you can use the async
/await
syntax to "await" the response.
import 'package:http/http.dart' as http;
var response = await http.post('https://api.dev/...', { body: 'Some body'}); (1)
1 | The await keyword is special Dart syntax that waits the current method execution and unwraps the returned Future.
The response variable is now of type Response and not Future<Response> . |
13.8. Using Futures in UI
Normally, when working with network requested data, you want to wait until the data is loaded before your show the user the corresponding UI elements.
In Flutter this can be done using the FutureBuilder
widget.
It takes three parameters:
-
future
: The future that should be waited upon -
initialData
: The data contained in thesnapshot
parameter of thebuilder
until thefuture
is completed. -
builder
: A function with the signature(BuildContext context, AsyncSnapshot<T> snapshot)
. It will be called every time something changes in the future (successful or erroneous completion). Typically this method should return a widget.
The documentation advises that the builder method could be called on every frame.
|
An example usage of the FutureBuilder
:
Scaffold(
body: FutureBuilder(
future: _someFuture, (1)
builder: (BuildContext context, AsyncSnapshot<String> snapshot) { (2)
if (snapshot.hasData) { (3)
return Text(snapshot.data);
} else if (snapshot.hasError) { (4)
return Text(snapshot.error.toString());
} else { (5)
return CircularProgressIndicator();
}
},
),
);
1 | A previously defined Future<String> |
2 | In this case the type of Data contained in the AsyncSnapshot is of type String .
But it could be of any type. |
3 | If the snapshot has data, a Text widget is returned that displays the data |
4 | If the Future was erroneous its error should be displayed |
5 | The default case, when there is no data yet |
14. Calling native code
The communication with Flutter and native code works via channels.
Both the native components as well as the Flutter components have to use the same key for the channels to address them.
The channels need to be registered in the Android activity.
private static final String BATTERY_CHANNEL = "samples.flutter.dev/battery";
// more code
//This goes to the end of the onCreate method
new MethodChannel(getFlutterView(), BATTERY_CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
result.notImplemented();
}
}
}
);
And Flutter needs to call it.
class _MyHomePageState extends State<MyHomePage> {
final logger = Logger();
static const platform = const MethodChannel('samples.flutter.dev/battery');
// Get battery level.
// Get battery level.
String _batteryLevel = 'Unknown battery level.';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
logger.e("Called");
_batteryLevel = batteryLevel;
});
}
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('Get Battery Level'),
onPressed: _getBatteryLevel,
),
Text(_batteryLevel),
],
),
),
);
}
}
15. Activate code checks
It is time to inherit to Dart conventions by activating automatic checks for it via the Dart linter.
Create the analysis_options.yaml
file in your top-level folder.
Add the following content.
linter:
rules:
- avoid_empty_else
- avoid_init_to_null
- avoid_relative_lib_imports
- avoid_return_types_on_setters
- avoid_shadowing_type_parameters
- avoid_types_as_parameter_names
- curly_braces_in_flow_control_structures
- empty_catches
- empty_constructor_bodies
- library_names
- library_prefixes
- no_duplicate_case_values
- null_closures
- prefer_contains
- prefer_equal_for_default_values
- prefer_is_empty
- prefer_is_not_empty
- prefer_iterable_whereType
- recursive_getters
- slash_for_doc_comments
- type_init_formals
- unawaited_futures
- unnecessary_const
- unnecessary_new
- unnecessary_null_in_if_null_operators
- unrelated_type_equality_checks
- use_rethrow_when_possible
- valid_regexps
The Flutter and Dart compiler should start complaining about violations against this standard.
Fix them until you have no more errors.
If you see no errors / warnings, try adding the new
keyword in front of your widgets and ensure that you see a warning.
16. Guide to write standard, clean and effective Dart code
-
Be consistent
Try to make two pieces of code similar. Different only in meaningful way.
-
Be brief
If there is multiple way to say something, pick the concise one. The goal is to make code economical, not dense.
16.1. Naming TODO
-
class name example: SliderMenu(), HttpRequest…
-
file name, package name, directories and library: file_system.dart, slider_menu.dart…
-
make import prefix lowercase: import 'dart.math' as math
-
name identifiers, constatn names in lowerCamelCase:
i.e., the first word starts with lowercase with following start with uppercase
var item;
HttpRequest httpRequest
16.2. Naming NOT TODO
-
capitalize acronyms
not to do:
HTTPConnection (better: HttpConnectionInfo)
IOStream (better: IoStream)
-
Make abbreviation like words:
not to do:
ID (better: Id)
-
Do not use leading underscore for identifiers if not private
-
Do not use prefix letters: kDefaultTimeout
16.3. Ordering
-
dart imports before other imports
-
package imports before relative imports
-
place external package imports before other imports
-
specify exports in a separate section after all imports
-
order each subsection alphabetically
import 'dart:async';
import 'dart:html';
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';
import 'package:my_package/util.dart';
import 'util.dart';
export 'src/error.dart';
16.4. format
For formatting, Dart provides a automated code formatter dartfmt.
Avoid lines longer than 80 characters.
16.5. Documentation
-
Format comments like sentences and use '//':
// Not if there is nothing before it.
if (_chunks.isEmpty) return false;
-
use '///' to doc comments for members and types
16.6. Include auto Package
Since google internally check for errors, warnings and hints, by including this, it implement the guidelines set out in guidance above.
include: package:pedantic/analysis_options.1.9.0.yaml
-
About Dart Linter, it enforces code standards, giving effective and standard code guide.
Simply create 'analysiis_options.yaml' in your root folder containing your rules.
Refer previous sections for more details on analysis_options.yaml
16.7. Reference
For more detailed guides, refer to Guide
17. Using OAuth2.0 to connect with StackOverflow
17.1. OAuth2.0
The basic idea of OAuth2.0 is commonly used nowadays.
When someone try to login from one website, he might be asked to login via Facebook. This case he authenticates with Facebook, giving the website access to required user-info from Facebook.
Here you use your application, StackOverflow, and one general user as a example.
For the explicit flow, you first register your app with StackOverflow (SO). Then you can get the client_id and client_secret from SO.
Then, you send the user, who is using your app, to the SO OAuth page. While connecting to the authentication page, you also send your client_id (so that SO recognizes that this is your app), scope(the data for which out app needs), the redirectUri (the Uri which you redirect to after user authenticates with the SO).
Once the user is at the SO page, he types in his Id and password to authenticate himself with SO authentication server.
Upon one successful authentication, the user is redirected to the redirectUri. Also one Code (the authentication code you need for access token later) you get from SO authentication server.
Then after getting the code, your app, together with the client_id and client_secret, connects with SO server. You ask for the relevant access_token which allows us to interact with SO database.
And the access_token is what you interact with SO API to get the relevant data of the user.
Note that in the process, client_id and client_secret are used to identify your identity (the app) to SO.
And the reason for having both code and access_token passed is because the authentication code is passed in frontend through browsers. This is considered unsafe. But your client_secret and the code can be used together to get the access_token from server side. This is then considered more safe.
17.2. Other OAuth2.0 methods with flutter
There are some packages you can use to reduce the work load when working with OAuth on flutter.
For general requesting, authentication, etc. FlutterOAuth
For getting the Authorization code grant, client credentials, Respurces Owner Password Grant: oauth2
For already pre-implemented calls for different API, Amazon, Dropbox, Facebook, etc. simple_auth
18. Keep Secret Keys Out
This section introduces you the way to securely store secret keys. As some private keys might be needed for configuration, you need to keep them out of your app. The way used here is to store them locally, as one asset.
18.1. Setup
Create one secret.json
file under assets folder.
Create one file for storing and handling data, key.dart
.
Then add this to the yaml file, to claim your dependency.
flutter: assets: - assets/secret.json
18.2. Adding the data to json file
First, put your secret in json file.
{ "key": "***the key to be added***" }
18.3. Data File
To handle the data from json file, you need to first define one data class.
class Secret{
final String key;
Secret({this.key = ""});
factory Secret.fromJson(Map<String, dynamic> jsonMap){
return Secret(key: jsonMap["key"]);
}
}
Then since the json file is stored locally, you need to implement the methods to extract the local json data.
create on SecretLoader
class.
class SecretLoader{
final String secretPath;
SecretLoader({this.secretPath});
Future<Secret> load() {
return rootBundle.loadStructuredData<Secret>(this.secretPath,
(jsonStr) async{
final secret = Secret.fromJson(json.decode(jsonStr));
return secret;
});
}
}
18.4. Using Key
After you implemented the Json file and data class, then you can use it in the source code. This way, you avoid putting secrets in source code and keeping it out of version control system.
Secret secret = await SecretLoader(secretPath: "assets/secret.json").load();
19. Links and Literature
19.1. General information about Flutter
19.2. Videos about Flutter
20. vogella training and consulting support
Appendix A: Copyright, License and Source code
Copyright © 2012-2019 vogella GmbH. Free use of the software examples is granted under the terms of the Eclipse Public License 2.0. This tutorial is published under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Germany license.