Flutter Reference
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
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\bindirectory to your SystemPATH(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 devices3 X Unable to locate Android SDK.4 Install Android Studio from: https://developer.android.com/studio/index.html5 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 from13 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter14[!] Connected device15 ! No devices availableYou can address any of the issus that flutter identified during the previous
Setting Things Up
General Setup
Install the Flutter SDK as described above
- You will need to install Android Studio (so that you have an android debugger)
- Once installed, on the Android Studio start screen (bottom right) click on
configure > AVD Manager > Create Virtual Deviceand then download thePiesystem image and once that’s done select it - Set a name for your virtual device (you can leave this as is)
- Click Finish and the virtual device will be created
Android Studio
Next, install the following plugins:
configure > Plugins > Marketplaceand search forFlutter- Install the
Flutterplugin and ask if it should install theDartplugin as well - Restart Android Studio, you should now have a
Start a new Flutter projectoption 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:
1flutter create YourAppNameWhich 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:
1flutter emulatorTo create an emulator you can use:
1flutter emulator --create --name MyEmulatorNameIf we’ve created an emulator called FlutterCLIEmulator we should see something like this when we run flutter emulator
1> flutter emulator22 available emulators:3
4FlutterCLIEmulator • FlutterCLIEmulator • Google • android5Pixel_2_API_28 • Pixel 2 API 28 • Google • androidTo launch the emulator we previously installed from your terminal you can run:
1flutter emulator --launch MyEmulatorNameThe 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 Managerfrom Android studio
Now that your emulator is running you can use the following to start the application
1flutter runFlutter 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:
1Flutter run key commands.2r Hot reload.3R Hot restart.4h Repeat this help message.5d Detach (terminate "flutter run" but leave application running).6c Clear the screen7q Quit (terminate the application on the device).8s 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
testdirectory 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
1void 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
1import 'package:flutter/material.dart';2
3void main() {4 runApp(MaterialApp(5 home: Text("Hello World"),6 ));7}To ensure that the reload happens fully use
Rand notr
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
1void 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...2flutter:3 uses-material-design: true4 fonts:5 - family: DMMono6 fonts:7 - asset: assets/fonts/DMMono-Regular.ttf8 - asset: assets/fonts/DMMono-Italic.ttf9 style: italic10 - asset: assets/fonts/DMMono-Light.ttf11 weight: 30012 - asset: assets/fonts/DMMono-LightItalic.ttf13 weight: 30014 style: italic15 - asset: assets/fonts/DMMono-Medium.ttf16 weight: 50017 - asset: assets/fonts/DMMono-MediumItalic.ttf18 style: italic19 weight: 500Ensure 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:
1import 'package:flutter/material.dart';2
3void 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:
1class Home extends StatelessWidget {2 @override3 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:
1class QuoteCard extends StatelessWidget {2 final Quote quote;3 final String label;4
5 QuoteCard({this.quote, this.label});6
7 ...//widget implementation8}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
StatelessWidgetwhich is a widget that can be hot reloaded, this only works when running the application inDebugmode 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:
1import 'package:flutter/material.dart';2
3void main() {4 const String fontFamily = 'DMMono';5
6 runApp(MaterialApp(7 theme: ThemeData(fontFamily: fontFamily),8 home: Home(),9 ));10}11
12class Home extends StatelessWidget {13 @override14 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...2body: 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
- Download the image and save in the
assets/imagesfolder - Update the
pubspec.ymlfile to contain the asset listing:
1flutter:2 # The following line ensures that the Material Icons font is3 # included with your application, so that you can use the icons in4 # the material Icons class.5 uses-material-design: true6
7 # To add assets to your application, add an assets section, like this:8 assets:9 - assets/images/NerdBob.gifWe can alternatively just declare the folder so that we can include all images in a folder:
1flutter:2 # The following line ensures that the Material Icons font is3 # included with your application, so that you can use the icons in4 # the material Icons class.5 uses-material-design: true6
7 # To add assets to your application, add an assets section, like this:8 assets:9 - assets/images/- And we’ll still be able to use the image the same way. To use the images use an
AssetImagelike this:
1...2body: 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:
- Using
Image.network
1body: Center(2 child: Image.network(3 "https://media.giphy.com/media/nDSlfqf0gn5g4/giphy.gif"4 ),5),- Using
Image.asset
1body: 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
1RaisedButton(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
1RaisedButton.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:
1IconButton(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
1body: 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
1body: 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:
1body: 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:
1body: Row(2 mainAxisAlignment: MainAxisAlignment.spaceEvenly,3 crossAxisAlignment: CrossAxisAlignment.center,4 ....5)We can do the same with a Column layout:
1body: 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:
- Move widgets around
- Add or remove wrapper Widgets
- 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:
1Expanded(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
1body: 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:
1body: 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
1SizedBox(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:
1CircleAvatar(2 backgroundImage: AssetImage("assets/Avatar.gif"),3 radius: 80,4),Center Widget
A Center Widget can be used to just center things:
1Center(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.
1Divider(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:
1class MyWidget extends StatefulWidget {2 @override3 _MyWidgetState createState() => _MyWidgetState();4}5
6class _MyWidgetState extends State<MyWidget> {7 @override8 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:
1class _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
setStatecall, but this will not trigger a re-render of the component
Card Widget
We can use the Card widget to render a card:
1Card(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 content9 ),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:
1class Home extends StatefulWidget {2 @override3 _HomeState createState() => _HomeState();4}5
6class _HomeState extends State<Home> {7 @override8 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:
1void 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
1void 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:
1Navigator.pushNamed(context, "/select-location")To replace the current route instead of add a new page above it we can do:
1Navigator.pushReplacementNamed(context, "/home");And to go back to the previouse route we can do:
1Navigator.pop(context),We can use the above to make out /home route navigate to the /select-location route like so:
1import 'package:flutter/material.dart';2
3class Home extends StatefulWidget {4 @override5 _HomeState createState() => _HomeState();6}7
8class _HomeState extends State<Home> {9 @override10 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:
1class Home extends StatefulWidget {2 static const routeName = "/home";3
4 @override5 _HomeState createState() => _HomeState();6}7
8
9class _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:
1class HomeArgs {2 final Timezone data;3 HomeArgs({this.data});4}We can call load this screen with the relevant arguments like this:
1Navigator.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:
1Navigator.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
1class _HomeState extends State<Home> {2 Timezone data = Timezone();3
4 @override5 Widget build(BuildContext context) {6 HomeArgs args = ModalRoute.of(context).settings.arguments;7 data = args.data;8 ... // do the stuff like build the widget, etc9 }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:
- User clicks on a button like “update data” from the Home page which routes to a Data updating component
1onPressed: () async {2 dynamic result = await Navigator.pushNamed(context, "/get-data");3 // will do stuff with the data4}- Data component loads data/does whatever it does
- When the Data component is completed it pops back to the Home page passing the data
1Navigator.pop(context, { ... data });- The Home page uses that data to update it’s state, using
setState
1onPressed: () 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
Buildfunction only runs once
- Stateful
- Can have state which changes over time
setStateis used to change the stateBuildfunction is called after the state has been changesinitStatelifecycle method is called when the widget is created- Useful for subscribing to streams that can change our data
Disposeis triggered when the widget is removed
In a StatefulWidger we can override the initState function like this:
1@override2void initState() {3 super.initState();// call the parent init state4 // do whatever setup is needed5 // only runs once at the start of a lifecycle method6}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:
- Add it to our
pubspec.ymlfile
pubspec.yml
1dependencies:2 http: ^0.12.1- Install it with:
1flutter pub getFetching 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
1ToDo data;2bool hasError = false;3bool isLoading = true;4
5void 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 error12 setState(() {13 Map<String, dynamic> json = jsonDecode(res.body);14 data = ToDo.fromJson(json);15 isLoading = false;16 });17 } else {18 // there was an error19 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:
1class 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 result10 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
1ListView.builder(2 itemCount: data.length,3 itemBuilder: (context, index) => Card(4 child: Text(data[index]),5 ),6);