Flutter Reference

Created: Notes on the Flutter framework

Updated: 03 September 2023

Notes from the The Net Ninja Youtube Series

What is it

Flutter is a way to build native, cross-platoform applications using the dart language

  • Easy to build respoonsive applications
  • Smooth and responsive applications
  • Material design and iOS Cupertino pre-implemented

Install Flutter

Documentation

To install Flutter you need to:

  • Download the Flutter SDK
  • Extract into your ‘C:\flutter’ directory. Any directory that does not require elevated permissions should work
  • Add the flutter\bin directory to your System PATH (possibly also User Path, I had some issues with the VSCode Extension so maybe both?)
  • Close and reopen any Powershell Windows

You should then be able to run the flutter doctor command to verify the installation, you may see something like this:

1
[√] Flutter (Channel stable, v1.17.0, on Microsoft Windows [Version 10.0.18363.778], locale en-US)
2
[X] Android toolchain - develop for Android devices
3
X Unable to locate Android SDK.
4
Install Android Studio from: https://developer.android.com/studio/index.html
5
On first launch it will assist you in installing the Android SDK components.
6
(or visit https://flutter.dev/docs/get-started/install/windows#android-setup for detailed instructions).
7
If the Android SDK has been installed to a custom location, set ANDROID_SDK_ROOT to that location.
8
You may also want to add it to your PATH environment variable.
9
10
[!] Android Studio (not installed)
11
[!] VS Code (version 1.45.0)
12
X Flutter extension not installed; install from
13
https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter
14
[!] Connected device
15
! No devices available

You can address any of the issus that flutter identified during the previous

Setting Things Up

General Setup

Install the Flutter SDK as described above

  1. You will need to install Android Studio (so that you have an android debugger)
  2. Once installed, on the Android Studio start screen (bottom right) click on configure > AVD Manager > Create Virtual Device and then download the Pie system image and once that’s done select it
  3. Set a name for your virtual device (you can leave this as is)
  4. Click Finish and the virtual device will be created

Android Studio

Next, install the following plugins:

  1. configure > Plugins > Marketplace and search for Flutter
  2. Install the Flutter plugin and ask if it should install the Dart plugin as well
  3. Restart Android Studio, you should now have a Start a new Flutter project option on the start menu

VSCode

  • Install the Flutter and Dart Extensions

Create an Application

The most straightforward way to create a new flutter application is to use the flutter cli. To create an app like this simply run:

1
flutter create YourAppName

Which will create a directory with the name of your app containing the necessary application files

You can then open the project with VSCode or Android studio

We will need to launch an emulator before we can start the application, to view all available emulators run:

1
flutter emulator

To create an emulator you can use:

1
flutter emulator --create --name MyEmulatorName

If we’ve created an emulator called FlutterCLIEmulator we should see something like this when we run flutter emulator

1
> flutter emulator
2
2 available emulators:
3
4
FlutterCLIEmulator • FlutterCLIEmulator • Google • android
5
Pixel_2_API_28 • Pixel 2 API 28 • Google • android

To launch the emulator we previously installed from your terminal you can run:

1
flutter emulator --launch MyEmulatorName

The emulator will remain active so long as the terminal is open. Closing the window will close the emulator

Note that to delete emulators you will need to use the AVD Manager from Android studio

Now that your emulator is running you can use the following to start the application

1
flutter run

Flutter is also able to connect to a device for remote debugging if one is connected to your computer (instead of an emulator)

Once flutter is running the applciation we can see menu which allows us to use keys to control some stuff:

1
Flutter run key commands.
2
r Hot reload.
3
R Hot restart.
4
h Repeat this help message.
5
d Detach (terminate "flutter run" but leave application running).
6
c Clear the screen
7
q Quit (terminate the application on the device).
8
s Save a screenshot to flutter.png.
9
...

The App

Basic App

The entrypoint for our application is the lib/main.dart in which we can see the main function for our app. We also have an ios and android directory for any platform specific code and a test folder for writing unit tests

For now delete the test directory otherwise you’re going to get errors when your tests start failing

Overall our application is made of Widgets, a Widget in flutter is simply a Class that renders something

The start portion of our application looks like this:

main.dart

1
void main() {
2
runApp(MyApp());
3
}

MyApp is a class which extends StatelessWidget which is our root for the application

For the sake of simplicity we’ll replace all of the code in the main.dart file with the following which will simply display the text Hello World on the sreen. We are also using the MaterialApp which will use the material design for the app:

main.dart

1
import 'package:flutter/material.dart';
2
3
void main() {
4
runApp(MaterialApp(
5
home: Text("Hello World"),
6
));
7
}

To ensure that the reload happens fully use R and not r

Laying Things Out

We’ll use a Scaffold to allow us to lay out a whole bunch of things, and inside of this we can use the different parts of the layout with some stuff in it:

main.dart

1
void main() {
2
runApp(MaterialApp(
3
home: Scaffold(
4
appBar: AppBar(
5
title: Text("My App"),
6
centerTitle: true,
7
),
8
body: Center(child: Text('Hello World')),
9
floatingActionButton: FloatingActionButton(
10
child: Text("CLICK"),
11
),
12
),
13
));
14
}

Styling

Using the method above we can add a lot more layout elements to our application layout. We can also make use of fonts. To do so just add the .tff files into a folder in our project. For our purpose we’ll just add the font files to the assets/fonts directory

Next, we add the fonts to our pubspec.yml file in the fonts property (this is already in the file but is just commmented out)

pubspec.yml

1
...
2
flutter:
3
uses-material-design: true
4
fonts:
5
- family: DMMono
6
fonts:
7
- asset: assets/fonts/DMMono-Regular.ttf
8
- asset: assets/fonts/DMMono-Italic.ttf
9
style: italic
10
- asset: assets/fonts/DMMono-Light.ttf
11
weight: 300
12
- asset: assets/fonts/DMMono-LightItalic.ttf
13
weight: 300
14
style: italic
15
- asset: assets/fonts/DMMono-Medium.ttf
16
weight: 500
17
- asset: assets/fonts/DMMono-MediumItalic.ttf
18
style: italic
19
weight: 500

Ensure that the spacing in your yml file is correct otherwise this will not work as you expect

Our app with the styles and fonts now applied looks like this:

1
import 'package:flutter/material.dart';
2
3
void main() {
4
const String fontFamily = 'DMMono';
5
Color themeColor = Colors.blue[600];
6
7
runApp(MaterialApp(
8
theme: ThemeData(fontFamily: fontFamily),
9
home: Scaffold(
10
appBar: AppBar(
11
title: Text(
12
"My App",
13
style: TextStyle(
14
fontSize: 20,
15
letterSpacing: 2,
16
color: Colors.grey[200],
17
),
18
),
19
centerTitle: true,
20
backgroundColor: themeColor,
21
),
22
body: Center(child: Text('Hello World')),
23
floatingActionButton: FloatingActionButton(
24
child: Text("CLICK"),
25
onPressed: () => print("I was pressed"),
26
backgroundColor: themeColor,
27
),
28
),
29
));
30
}

Stateless Widget

A stateless widget is a widget that’s data does not change over the course of the widget’s lifetime. These can contain data but the data cannot change over time

The following snippet creates a basic Stateless Widget:

1
class Home extends StatelessWidget {
2
@override
3
Widget build(BuildContext context) {
4
return Container();
5
}
6
}

One of the benefits of creating custom widgets is that we are able to re use certain components. In this widget we need to return a Widget from the build function. We can simply move our application’s Scaffold as the return of this widget

We can also have data for a stateless widget but this will be data that does not change. We can define a widget like this:

1
class QuoteCard extends StatelessWidget {
2
final Quote quote;
3
final String label;
4
5
QuoteCard({this.quote, this.label});
6
7
...//widget implementation
8
}

The use of final says that this property will not be reassigned after the first assignment

To setup automatic hot reloading we need to make use of a StatelessWidget which is a widget that can be hot reloaded, this only works when running the application in Debug mode via an IDE and not from the terminal. For VSCode this can be done with the Flutter Extensions and same for Android Studio

Our component now looks something like this:

1
import 'package:flutter/material.dart';
2
3
void main() {
4
const String fontFamily = 'DMMono';
5
6
runApp(MaterialApp(
7
theme: ThemeData(fontFamily: fontFamily),
8
home: Home(),
9
));
10
}
11
12
class Home extends StatelessWidget {
13
@override
14
Widget build(BuildContext context) {
15
Color themeColor = Colors.blue[600];
16
17
return Scaffold(
18
appBar: AppBar(
19
...
20
),
21
body: Center(child: Text('Hello World')),
22
floatingActionButton: FloatingActionButton(
23
...
24
),
25
);
26
}
27
}

Images

We can use either Network Images or Local Images. To use an image we use the Image widget with a NetworkImage widget as the image property, an example of the NetworkImage would look like so:

1
...
2
body: Center(
3
child: Image(
4
image: NetworkImage(
5
"https://media.giphy.com/media/nDSlfqf0gn5g4/giphy.gif"
6
),
7
)
8
),
9
...

If we want to use a locally stored image we need to do a few more things

  1. Download the image and save in the assets/images folder
  2. Update the pubspec.yml file to contain the asset listing:
1
flutter:
2
# The following line ensures that the Material Icons font is
3
# included with your application, so that you can use the icons in
4
# the material Icons class.
5
uses-material-design: true
6
7
# To add assets to your application, add an assets section, like this:
8
assets:
9
- assets/images/NerdBob.gif

We can alternatively just declare the folder so that we can include all images in a folder:

1
flutter:
2
# The following line ensures that the Material Icons font is
3
# included with your application, so that you can use the icons in
4
# the material Icons class.
5
uses-material-design: true
6
7
# To add assets to your application, add an assets section, like this:
8
assets:
9
- assets/images/
  1. And we’ll still be able to use the image the same way. To use the images use an AssetImage like this:
1
...
2
body: Center(
3
child: Image(
4
image: AssetImage("assets/images/NerdBob.gif"),
5
),
6
),
7
...

Because using images like this is a common task, flutter also provides us with the following two methods:

  1. Using Image.network
1
body: Center(
2
child: Image.network(
3
"https://media.giphy.com/media/nDSlfqf0gn5g4/giphy.gif"
4
),
5
),
  1. Using Image.asset
1
body: Center(
2
child: Image.asset("assets/images/NerdBob.gif"),
3
),

The above methods are pretty much just shorthands for the initial methods

Icons

An Icon Widget looks like this:

1
Icon(
2
Icons.airplanemode_active,
3
color: Colors.lightBlue[500],
4
)

And a Button looks like this

1
RaisedButton(
2
onPressed: () => {},
3
child: Text("IT'S GO TIME")
4
)

There are of course more options for both of these but those are the basics

We can also make use of an RaisedButton with an Icon which works like so

1
RaisedButton.icon(
2
color: Colors.red,
3
textColor: Colors.white,
4
onPressed: () => {print("We Gone")},
5
icon: Icon(Icons.airplanemode_active),
6
label: Text("IT'S GO TIME")
7
)

Or an IconButton:

1
IconButton(
2
color: Colors.red,
3
onPressed: () => {print("We Gone")},
4
icon: Icon(Icons.airplanemode_active),
5
)

Containers

Container Widgets are general containers. When we don’t have anything in them they will fill the available space, when they do have children they will take up the space that the children take up.

Containers also allow us to add padding and margin to it’s children

  • Padding is controlled using EdgeInsets
  • Margin is controlled using EdgeInsets
1
body: Container(
2
color: Colors.grey[800],
3
padding: EdgeInsets.all(20),
4
margin: EdgeInsets.all(20),
5
child: Text(
6
"Hello World",
7
style: TextStyle(color: Colors.white),
8
),
9
),

If we don’t need the colour and margin properties and only want padding, we can use a Paddint widget

1
body: Padding(
2
padding: EdgeInsets.all(20),
3
child: Text(
4
"Hello World",
5
style: TextStyle(color: Colors.white),
6
),
7
),

Layouts

We have a few Widgets we can use to make layouts, some of these are:

The Row Widget which can have children as an array of Widgets:

1
body: Row(
2
children: <Widget>[
3
Text("Hello World"),
4
RaisedButton(
5
onPressed: () {},
6
child: Text("I AM BUTTON"),
7
),
8
RaisedButton.icon(
9
onPressed: () {},
10
icon: Icon(Icons.alarm),
11
label: Text("Wake Up")
12
),
13
]
14
),

Out layout widgets also have the concept of a main and cross axis and we can control the layout along these axes. This works almost like FlexBox:

1
body: Row(
2
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
3
crossAxisAlignment: CrossAxisAlignment.center,
4
....
5
)

We can do the same with a Column layout:

1
body: Column(
2
mainAxisSize: MainAxisSize.min,
3
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
4
crossAxisAlignment: CrossAxisAlignment.center,
5
children: <Widget>[
6
Text("Hello World"),
7
RaisedButton(
8
onPressed: () {},
9
child: Text("I AM BUTTON"),
10
),
11
RaisedButton.icon(
12
onPressed: () {},
13
icon: Icon(Icons.alarm),
14
label: Text("Wake Up")
15
),
16
]
17
),

Refactor Menus

You can click on a widget and then click on the light bulb/refactor button, or the Flutter Overview Panel (VSCode or Android Studio) and that will allow you to do some things like:

  1. Move widgets around
  2. Add or remove wrapper Widgets
  3. Extract widget to function/new widget

Expanded Widgets

Expanded widgets allow us to make a Widget use up all the extra available space, this is a bit like setting a Flex grow value for a widget

This is a Wrapper widget that we can use to make a child expand into the available space in its main-axis like so:

1
Expanded(
2
child: Container(
3
padding: EdgeInsets.all(30),
4
color: Colors.cyan,
5
child: Text("1"),
6
),
7
),

We can also set a flex value which defines the portion of space we want a Widget to use

1
body: Row(
2
children: <Widget>[
3
Expanded(
4
flex: 2,
5
child: MyWidget(),
6
),
7
Expanded(
8
flex: 1,
9
child: MyWidget(),
10
),
11
Expanded(
12
flex: 0,
13
child: MyWidget(),
14
),
15
],
16
),

These are also useful for containing an image, for example:

1
body: Row(
2
children: <Widget>[
3
Expanded(
4
flex: 2,
5
child: Image.asset("assets/images/NerdBob.gif")
6
),
7
Expanded(
8
flex: 1,
9
child: Container(
10
padding: EdgeInsets.all(30),
11
color: Colors.cyan,
12
child: Text("Hello"),
13
),
14
),
15
],
16
),

Sized Box Widget

The Sized Box widget allows us to simply add a spacer, we can set the height and width

1
SizedBox(
2
height: 10,
3
width: 20,
4
),

The height and width properties are both optional

CircleAvatar Widget

Flutter also provides a widget for rendering circle shaped avatars:

1
CircleAvatar(
2
backgroundImage: AssetImage("assets/Avatar.gif"),
3
radius: 80,
4
),

Center Widget

A Center Widget can be used to just center things:

1
Center(
2
child: WidgetToCenter(),
3
),

Divider Widget

Flutter provides us with a Divider widget which will draw a simple horizontal line. We provide a height for the bounding box and a color for the line.

1
Divider(height: 60, color: Colors.amberAccent),

Stateful Widgets

A Widget that contains state usually consists of two classes, one for the widget itself that extends StatefilWidget and another for the type of the state itself which extends State<T>. This looks something like:

1
class MyWidget extends StatefulWidget {
2
@override
3
_MyWidgetState createState() => _MyWidgetState();
4
}
5
6
class _MyWidgetState extends State<MyWidget> {
7
@override
8
Widget build(BuildContext context) {
9
return Container();
10
}
11
}

We can also convert a current StatelessWidget to a StatefulWidget by using the recfctorings available for the class

Updating State

To update state in our component we use the inherited setState function and use it to perform state updates. This function takes a function as a parameter and we handle state updates in this

A function with a state property _level and a function for incrementing it’s state can be defined in our class like so:

1
class _CardState extends State<NinjaCard> {
2
int _level = 1;
3
4
void _incrementLevel() {
5
setState(() => _level++);
6
}

We can then use call the _incrementLevel from the onPressed handler from a button, and simply render the _level variable anywhere we want to use it

The state of the component can be updated independendently of the setState call, but this will not trigger a re-render of the component

Card Widget

We can use the Card widget to render a card:

1
Card(
2
margin: EdgeInsets.fromLTRB(15, 15, 15, 0),
3
color: Colors.grey[900],
4
elevation: 4,
5
child: Padding(
6
padding: const EdgeInsets.all(15),
7
child: Column(
8
...// widget content
9
),
10
),
11
);

SafeArea

If we are not using a layout that is designed to keep the content in the screen we can use a SafeArea widget which will ensure that whatever content we have on the screen does not end up under the time area, etc. See the usage below:

1
class Home extends StatefulWidget {
2
@override
3
_HomeState createState() => _HomeState();
4
}
5
6
class _HomeState extends State<Home> {
7
@override
8
Widget build(BuildContext context) {
9
return Scaffold(
10
body: SafeArea(child: Text("HOME")),
11
);
12
}
13
}

Routing

Routes in flutter make use of a Map and a function that makes use of the current app context which is used so that Widgets can get information about the app state and returns a Widget for the given route

To configure our application to use some routes we can do the following:

1
void main() {
2
runApp(MaterialApp(
3
routes: {
4
'/': (context) => Loading(),
5
'/home': (context) => Home(),
6
'/select-location': (context) => SelectLocation(),
7
},
8
));
9
}

By default when our application loads it will start at the / route but we can specify a different route using the initialRoute property

1
void main() {
2
runApp(MaterialApp(
3
initialRoute: "/home",
4
routes: {
5
'/': (context) => Loading(),
6
'/home': (context) => Home(),
7
'/select-location': (context) => SelectLocation(),
8
},
9
));
10
}

To navigate to a new route we can use the Navigator class and the methods available on that.

To push a new page on top of our existing page we can do:

1
Navigator.pushNamed(context, "/select-location")

To replace the current route instead of add a new page above it we can do:

1
Navigator.pushReplacementNamed(context, "/home");

And to go back to the previouse route we can do:

1
Navigator.pop(context),

We can use the above to make out /home route navigate to the /select-location route like so:

1
import 'package:flutter/material.dart';
2
3
class Home extends StatefulWidget {
4
@override
5
_HomeState createState() => _HomeState();
6
}
7
8
class _HomeState extends State<Home> {
9
@override
10
Widget build(BuildContext context) {
11
return Scaffold(
12
body: SafeArea(
13
child: Column(children: <Widget>[
14
FlatButton.icon(
15
onPressed: () => Navigator.pushNamed(context, "/select-location"),
16
icon: Icon(Icons.edit_location),
17
label: Text("Select Location"))
18
])),
19
);
20
}
21
}

Where clicking the button will cause a navigation.

Additionally, in our /select-location route we use a layout with an AppBar, what we get with this is a button that will automatically do the Navigator.pop functioning for navigating backwards

The routing process works by adding screens on top of one another, routing allows us to navigate up and down this route. The problem with is that we may end up with a large stack of routes and we need to be careful about how we manage the stack

We can also pass data through to routes as well as use Static routenames that are associated with the widgets for the screen they use, for example if we have a Home widget defined like so:

1
class Home extends StatefulWidget {
2
static const routeName = "/home";
3
4
@override
5
_HomeState createState() => _HomeState();
6
}
7
8
9
class _HomeState extends State<Home> {
10
Timezone data = Timezone();
11
...
12
}

And the class for the data we would like to pass to the screen like this:

1
class HomeArgs {
2
final Timezone data;
3
HomeArgs({this.data});
4
}

We can call load this screen with the relevant arguments like this:

1
Navigator.pushReplacementNamed(
2
context,
3
Home.routeName,
4
arguments: HomeArgs(data: data),
5
);

Or just using a map. Note that if we use a map then the class itself needs to be adapted to accept the Map type input:

1
Navigator.pushReplacementNamed(
2
context,
3
"/home",
4
arguments: {data: Timezone()},
5
);

We can then access the data that was passed using the context object in our build function

1
class _HomeState extends State<Home> {
2
Timezone data = Timezone();
3
4
@override
5
Widget build(BuildContext context) {
6
HomeArgs args = ModalRoute.of(context).settings.arguments;
7
data = args.data;
8
... // do the stuff like build the widget, etc
9
}
10
}

Using Route Changes to Get User Data

We can make use of a Navigator push action to open a screen to allow for user input/load some data, and then we can pop back to the initial screen witht the data that was recieved. By doing this we can view the process of this input (from the intial route’s perspective) as an async action that spans multipls routes

For example, consider something like the following flow:

  1. User clicks on a button like “update data” from the Home page which routes to a Data updating component
1
onPressed: () async {
2
dynamic result = await Navigator.pushNamed(context, "/get-data");
3
// will do stuff with the data
4
}
  1. Data component loads data/does whatever it does
  2. When the Data component is completed it pops back to the Home page passing the data
1
Navigator.pop(context, { ... data });
  1. The Home page uses that data to update it’s state, using setState
1
onPressed: () async {
2
dynamic result = await Navigator.pushNamed(context, "/get-data");
3
4
setState((){
5
this.data = result;
6
});
7
}

Widget Lifecycles

In Flutter we have two kinds of widgets:

  • Stateless
    • Does not have state that changes over time
    • Build function only runs once
  • Stateful
    • Can have state which changes over time
    • setState is used to change the state
    • Build function is called after the state has been changes
    • initState lifecycle method is called when the widget is created
      • Useful for subscribing to streams that can change our data
    • Dispose is triggered when the widget is removed

In a StatefulWidger we can override the initState function like this:

1
@override
2
void initState() {
3
super.initState();// call the parent init state
4
// do whatever setup is needed
5
// only runs once at the start of a lifecycle method
6
}

Packages

Flutter packages can be found at pub,dev. The http package can be used to make HTTP requests

To use the package we need to do the following:

  1. Add it to our pubspec.yml file

pubspec.yml

1
dependencies:
2
http: ^0.12.1
  1. Install it with:
1
flutter pub get

Fetching Data (http package)

We can make use of the http package to fetch some data from an API. for now we’ll use the JSONPlaceholder API. To get data we can use the get function of the http package

1
ToDo data;
2
bool hasError = false;
3
bool isLoading = true;
4
5
void getData() async {
6
Response res = await get("https://jsonplaceholder.typicode.com/todos/1");
7
8
print(res.body);
9
10
if (res.statusCode == 200) {
11
// no error
12
setState(() {
13
Map<String, dynamic> json = jsonDecode(res.body);
14
data = ToDo.fromJson(json);
15
isLoading = false;
16
});
17
} else {
18
// there was an error
19
print("Error");
20
setState(() {
21
hasError = true;
22
isLoading = true;
23
});
24
}
25
}

The data that comes from the API is a JSON Map, we need to parse this into a Dart object manually, we can do this from the class that we want to parse the object into:

1
class ToDo {
2
final int userId;
3
final int id;
4
final String title;
5
final bool completed;
6
7
ToDo({this.userId, this.id, this.title, this.completed});
8
9
// this is the function we call to parse the object from the JSON result
10
factory ToDo.fromJson(Map<String, dynamic> json) {
11
return ToDo(
12
userId: json["userId"],
13
id: json["id"],
14
title: json["title"],
15
completed: json["completed"],
16
);
17
}
18
}

Note that it is also possible for us to make use of more automatic means of doing this (such as with Code Generation), more information on this can be found in the Flutter documentation

ListViews

We can build list views using a ListView.builder which allows us to output a template for each item in a list automatically instead of us doing the data-template conversion on our own

The ListView.builder widget takes an itemCount which is the number of items in the list, and a itemBuilder which takes the context and index and uses it to then render a given element

1
ListView.builder(
2
itemCount: data.length,
3
itemBuilder: (context, index) => Card(
4
child: Text(data[index]),
5
),
6
);