Welcome! I’m glad you’re here again for some more Dart and Flutter magic.
✨ In the previous episode of this series, we looked at Dart and went from basically zero to hero with all those types, classes and asynchrony. I hope you had enough practice on Dart because today, we’ll move forward to Flutter. Let’s get started!
Quick heads up: the “?” emoji will compare JS and React with Dart and Flutter language examples as of now. Just like in the previous episode,, the left side will be the JS/React, and the right side will be the Dart/Flutter equivalent, e.g. console.log("hi!");
? print("hello!");
What is Flutter, and why we’ll use it
Flutter and Dart are both made by Google. While Dart is a programming language, Flutter is a UI toolkit that can compile to native Android and iOS code. Flutter has experimental web and desktop app support, and it’s the native framework for building apps for Google’s Fuchsia OS.
This means that you don’t need to worry about the platform, and you can focus on the product itself. The compiled app is always native code as Dart compiles to ARM, hence providing you the best cross-platform performance you can get right now with over 60 fps.
Flutter also helps the fast development cycle with stateful hot reload, which we’ll make use of mostly in the last episode of this series.
Intro to the Flutter CLI
When building apps with Flutter, one of the main tools on your belt is the Flutter CLI. With the CLI, you can create new Flutter projects, run tests on them, build them, and run them on your simulators or emulators. The CLI is available on Windows, Linux, macOS and x64-based ChromeOS systems.
Once you have the CLI installed, you’ll also need either Android Studio, Xcode, or both, depending on your desired target platform(s).
(Flutter is also available on the web and for desktop, but they are still experimental, so this tutorial will only cover the Android and iOS related parts).
If you don’t wish to use Android Studio for development, I recommend VSCode. You can also install the Dart and Flutter plugins for Visual Studio Code.
Once you’re all set with all these new software, you should be able to run flutter doctor
. This utility will check if everything is working properly on your machine. At the time of writing, Flutter printed this into the console for me:
[✓] Flutter (Channel stable, v1.17.4, on Mac OS X 10.15.4 19E287, locale en-HU)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
[✓] Xcode - develop for iOS and macOS (Xcode 11.5)
[!] Android Studio (version 3.5)
✗ Flutter plugin not installed; this adds Flutter specific functionality.
✗ Dart plugin not installed; this adds Dart specific functionality.
[✓] VS Code (version 1.46.1)
[!] Connected device
! No devices available
You should get similar results for at least for the Flutter part too. Everything else depends on your desired target platforms and your preferred IDEs like Android Studio or VS Code. If you get an X
for something, check again if everything is set up properly.
Only move forward in this tutorial if everything works properly.
To create a new Flutter project, cd into your preferred working directory, and run flutter create <projectname>
. The CLI will create a directory and place the project files in there. If you use VS Code on macOS with an iOS target, you can use this little snippet to speed up your development process:
# Create a new project
flutter create <projectname>
# move there
cd projectname
# open VS code editor
code .
# open iOS Simulator - be patient, it may take a while
open -a Simulator.app
# start running the app
flutter run
And boom, you’re all set! ?
If you don’t wish to use the iOS simulator, you can always spin up your Android Studio emulator. Use Genymotion (or any other Android emulation software), or even connect a real device to your machine. This is a slower and more error-prone solution, so I recommend to only test on real devices when necessary.
Once they have booted, you can run flutter doctor
again and see if Flutter sees the connected device. You should get an output something just like this:
...
[✓] Connected device (1 available)
...
If you got this output – congratulations! ? You’re all set to move on with this tutorial. If, for some reason Flutter didn’t recognize your device, please go back and check everything again as you won’t be able to follow the instructions from now on.
Hello world! ?
If you didn’t run the magic snippet previously, run these commands now:
# Create a new project
flutter create <projectname>
# move there
cd projectname
# open VS code editor (optional if you use Studio)
code .
# start running the app
flutter run
This will spin up the Flutter development server with stateful hot reload and a lot more for you. You’ll see, that by default, Flutter creates a project with a floating action button and a counter:
Once you’re finished with playing around the counter, let’s dig into the code! ??
Flutter project structure
Before we dig right into the code, let’s take a look at the project structure of our Flutter app for a moment:
├── README.md
├── android
│ └── ton of stuff going on here...
├── build
│ └── ton of stuff going on here...
├── ios
│ └── ton of stuff going on here...
├── lib
│ └── main.dart
├── pubspec.lock
├── pubspec.yaml
└── test
└── widget_test.dart
We have a few platform-specific directories: android
and ios
. These contain the necessary stuff for building, like the AndroidManifest
, build.gradle
, or your xcodeproj
.
At this moment, we don’t need to modify the contents of these directories so we’ll ignore them for now. We’ll also ignore the test
directory as we won’t cover testing Flutter in this series (but we may look into it later if there’s interest ?), so that only leaves us to these:
├── lib
│ └── main.dart
├── pubspec.lock
├── pubspec.yaml
And this is where the magic happens. Inside the lib
directory, you have the main.dart
: that’s where all the code lives right now. We’ll peek into it later, but let’s just have a look at the pubspec.yaml
and pubspec.lock
.
What are those?
Package management in Flutter – pub.dev
When building a project with JavaScript, we often use third party components, modules, packages, libraries, and frameworks so that we don’t have to reinvent the wheel. The JavaScript ecosystem has npmnpm is a software registry that serves over 1.3 million packages. npm is used by open source developers from all around the world to share and borrow code, as well as many businesses. There are three components to npm: the website the Command Line Interface (CLI) the registry Use the website to discover and download packages, create user profiles, and... and yarn to provide you with all those spicy zeroes and ones, and they also handle the dependencies inside your project.
In the Dart ecosystem, this is all handled by pub.dev.
So, just a few quick facts:npm
? pub.dev
package.json
? pubspec.yaml
package-lock.json
? pubspec.lock
We’ll look into installing packages and importing them into our app in the last episode of this series, in which we’ll create a fun mini-game.
Digging into the Dart code
The only thing left from the file tree is main.dart
. main
is the heart of our app, it’s like the index.js
of most JS-based projects. By default, when creating a project with flutter create
, you’ll get a very well documented code with a StatelessWidget
, a StatefulWidget
, and its State
.
So instead of observing the demo code line by line together, I encourage you to read the generated code and comments by yourself and come back here later.
In the next part, we’ll look into what are widgets and the build
method.
We’ll learn why it is @override
d, and what’s the difference between stateful and stateless widgets. Then we’ll delete all the code from main.dart
and create a Hello world app by ourselves so that you can get the hang of writing declarative UI code in Flutter.
Go ahead, read the generated code and the documentation now! ?
In Flutter, everything is a widget!
As you have been reading the code, you may have noticed a few things. The first thing after importing Flutter is the entry method I have been talking about in the previous episode:
void main() {
runApp(MyApp());
}
And then, you could see all those classes and OOP stuff come back with the line class MyApp extends StatelessWidget
.
First things first: in Flutter, everything is a widget!
Oh, and speaking of widgets. Components ? Widgets!
The StatelessWidget
is a class from the Flutter framework, and it’s a type of widget. Another kind of widget is StatefulWidget
and we’ll look into the difference between those and how to use them later.
We can create our reusable widget by extending the base class StatelessWidget
with our own build
method. (By the way, render
in ReactJS ? build
in Flutter). We can see that the build
returns a Widget
because the return type is defined, and we can see an odd keyword in the previous line: @override
.
It’s needed because the StatelessWidget
class has a definition for build
by default, but we want to replace it (or override it) with our own implementation – hence the keyword @override
. Before we dig further into the code, let’s have a peek at using widgets in Flutter:
// using a React component
<button onClick={() => console.log(‘clicked!’)}>Hi, I’m a button</button>
// using a Flutter widget
RawMaterialButton(
onPressed: () {
print("hi, i'm pressed");
},
child: Text("press me!"),
),
You can see that Flutter has a different approach with declarative UI code.
Instead of wrapping children between ><
s and passing props next to the component name (e.g. <button onClick ...
), everything is treated as a property. This enables Flutter to create more flexible and well-typed widgets: we’ll always know if a child is supposed to be a standalone widget or if it can accept multiple widgets as a property, for example. This will come in handy later when we’ll build layouts with Row
s and Column
s.
Now that we know a bit more about widgets in Flutter, let’s take a look at the generated code again:
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
The build
method returns a MaterialApp
that has a type of Widget
and – unsurprisingly – comes from Flutter. This MaterialApp widget is a skeleton for your Flutter app. It contains all the routes, theme data, metadata, locales, and other app-level black magic you want to have set up. ?
You can see the MyHomePage
class being referenced as the home
screen. It also has a property, title
, set up. MyHomePage
is also a widget, and we can confirm that by looking at the definition of this class.
Quick tip: if you are using VSCode as your editor, hold Command and hover or click on the class reference and you’ll be directed to the code of the class.
We can see that MyHomePage
extends a StatefulWidget
. However, the structure of the code itself is a bit squiggly and weird. What’s this MyHomePage({Key key, this.title}) : super(key: key);
syntax? Why doesn’t this widget have a build
method? What’s a State
? What is createState
?
To answer these questions, we’ll have to look into one of the more hard-code topics in Flutter: state management.
Local state management in Flutter: StatefulWidgets
I previously talked about the two main types of widgets in Flutter: StatelessWidget
s and StatefulWidget
s. StatelessWidget
s are pretty straightforward: a snippet of code that returns a Widget, maybe some properties are being passed around, but that’s all complexity.
However, we don’t want to write applications that just display stuff! We want to add interactivity! And most interactions come with some state, whether it’s the data stored in an input field or some basic counter somewhere in your app. And once the state is updated, we want to re-render the affected widgets in our app – so that the new data is being displayed for the user.
Think of state management in React: it has the very same purpose with the goal of being as efficient as possible. It’s no different in Flutter: we want to have some very simple widgets (or StatelessWidgets
), and some widgets with a bit of complexity and interactivity (or StatefulWidgets
).
Let’s dive into the code: a StatefulWidget consists of two main components:
- a
StatefulWidget
(that is calledMyHomePage
in our case) - a typed
State
object (that is called_MyHomePageState
in this example)
We’ll call these “widget” and “state” (respectively) for the sake of simplicity. The widget itself contains all the props, and a createState
overridden method. As you can see, the prop is marked with a final
– that’s because you cannot change the prop from within the widget. When you modify a prop of a widget, Flutter throws the current instance away and creates a brand new StatefulWidget
.
Note that changing either the prop or the state will trigger a rebuild in Flutter – the key difference between the two is that changing the state can be initiated from within the widget while changing a prop is initiated by the parent widget.
Props help you pass data from parent to children. State helps you handle data change inside the children.
Now, let’s look into changing the state: inside the widget, we have a createState
method that only returns the state, _MyHomePageState()
. When modifying the state with the setState
method, this createState
method gets called and returns a new instance of your state. The old instance gets thrown away, and a new instance of your widget will be inserted into the widget tree.
(Sidenote: the widget tree is only a blueprint of your app, the element tree is the one that gets rendered for the user. It’s a bit more advanced, under-the-hood topic, so it won’t be covered in this series – however, I’ll link some video resources later on that will help you understand how Flutter works and what’s the deal with the widget tree and the element tree.)
The _MyHomePageState
class has a type of State
, typed with MyHomePage
.
This is needed so that you can access the properties set in the MyHomePage
instance with the widget
keyword – for example, to access the title
prop, write widget.title
. Inside the state, you have an overridden build
method, just like you’d see in a typical StatelessWidget
. This method returns a widget that renders some nice data, both from props (widget.title
) and from the state (_counter
).
Notice that you don’t need to type in anything before the _counter
. No this.state._counter
, no State.of(context)._counter
, just a plain old _counter
. That’s because from the perspective of the code, this variable is declared just like any other would be:
int _counter = 0;
However, when modifying this variable, we need to wrap our code in setState
, like this:
setState(() {
_counter++;
});
This will tell Flutter that “Hey! It’s time to re-render me!”.
The framework will call the previously discussed createState
method; a new instance of your state gets created; built; rendered; and boom! ? The new data is now on-screen.
It may seem a bit complicated or seem like you have to write a lot of boilerplate code to get this running. But don’t worry! With VS Code, you can refactor any StatelessWidget
into a stateful one with just one click:
And that’s it for managing your widget’s state! It may be a lot at first, but you’ll get used to it after building a few widgets.
A few notes about global state management in Flutter
Right now, we only looked at working with local state in Flutter – handling app-level, or global state is a bit more complex. There are, just like in JS, tons of solutions, ranging from the built-in InheritedWidget
to a number of third-party state management libraries. Some of those may already be familiar, for example, there is RxDart and Redux, just to name a few. To learn more about the most popular solutions, and which one to choose for your project, I suggest you watch this awesome video about global state management in Flutter by Fireship.
Widgets, widgets, and widgets
I already talked about how everything is a widget in Flutter – however, I didn’t really introduce you to some of the most useful and popular widgets in Flutter, so let’s have a look at them before we move on!
Flutter has widgets for displaying texts, buttons, native controls like switches and sliders (cupertino
for iOS and material
for Android style widgets), layout widgets like Stack
, Row
, Column
and more. There are literally hundreds of widgets that are available for you out of the box, and the list keeps growing.
The whole widget library can be found here in the Widget Catalog, and the Flutter team is also working on a very nice video series with new episodes being released weekly. This series is called Flutter Widget of the Week, and they introduce you to a Flutter widget, it’s use cases, show you code examples and more, in just about one minute! It’s really binge-worthy if you want to get to know some useful Flutter widgets, tips, and tricks.
Here a link for the whole series playlist, and here is the intro episode.
Some useful widgets in Flutter
As you’ll work with Flutter, you’ll explore more and more widgets, but there are some basic Flutter widgets you’ll absolutely need to build your first application. (We’ll probably use most of them in the next and last episode of this series, so stay tuned!)
First and foremost: Text
.
The Text widget delivers what its name promises: you can display strings with it. You can also style or format your text and even make multiline texts. (There’s are a lot of line of text-related widgets available, covering your needs from displaying rich text fields to creating selectable texts.)
An example Text widget in Flutter:
Text('hello world!'),
Adding buttons to your Flutter app is also easy as one two three. There are numerous button-related widgets available for you ranging from RawMaterialButton to FlatButton, IconButton, and RaisedButton, and there are also specific widgets for creating FloatingActionButtons and OutlineButtons. I randomly picked ? the RaisedButton
for us so that we can have a peek at how easy it is to add a nice, stylish button into our app:
RaisedButton(
onPressed: () {
print(
"hi! it's me, the button, speaking via the console. over.",
);
},
child: Text("press meeeeeee"),
),
Building layouts in Flutter
When building flexible and complex layouts on the web and in React-Native, the most important tool you used was flexbox. While Flutter isn’t a web-based UI library and hence lacks flexbox, the main concept of using flexible containers with directions and whatnot is implemented and preferred in Flutter. It can be achieved by using Row
s and Column
s, and you can stack widgets on each other by using Stack
s.
Consider the following cheatsheet I made:
Remember how I previously praised typing the props of a widget and how it’s one of the best tools in Flutter’s declarative UI pattern? The Row
, Column
and Stack
widgets all have a children
property that want an array of widgets, or [Widget]
. Lucky for you, the VS Code automatically completes the code for you once you start working with these widgets:
Just hit tab to let Code complete the code for you! Maybe in the future, you won’t need to write code at all, Flutter will just suck out the app idea out of your brain and compile that – but until then, get used to hitting tab.
Let’s look at an example where we display some names underneath each other:
Column(
children: <Widget>[
Text("Mark"),
Text("Imola"),
Text("Martin"),
Text("Zoe"),
],
),
You can see that you create a typed list with the <Widget>[]
syntax, you pass it as a prop for the Column, create some amazing widgets inside the list, and boom! The children will be displayed underneath each other. Don’t believe me? Believe this amazing screenshot. ?
Alignment
The real power of Column
s and Row
s isn’t just placing stuff next to each other, just like flexbox isn’t only about flex-direction
either. In Flutter, you can align the children of a Column
and Row
on two axes, mainAxis
and crossAxis
.
These two properties are contextual: whilst in a Row
, the main axis would be horizontal, and the crossing axis would be vertical, it would be switched in a Column
. To help you better understand this axis concept, I created a handy cheat sheet with code examples and more.
So, for example, if you want to perfectly center something, you’d want to use either the Center
widget; or a Row
or Column
with both mainAxisAlignment
and crossAxisAlignment
set to .center
; or a Row
and Column
with their mainAxisAlignment
s set to .center
. The possibilities are basically endless with these widgets! ✨
Rendering lists (FlatLists ? ListViews)
Whilst thinking about possible use cases for columns, you may have wondered about creating scrollable, dynamic, reorderable, or endless lists.
While these features could be achieved by using Column
s, it would take a lot of effort to do so, not even mentioning updating your list data or lazy rendering widgets when there’s a crapton of data. Lucky you, Flutter has a class for rendering lists of data, and it’s called a ListView
!
There are several ways to use a ListView
, but the most important ones are the ListView(...)
widget and the ListView.builder
method. Both of them achieve the very same functionality from the perspective of the user, but programmatically, they differ big time.
First, let’s look into the ListView(..)
widget. Syntactically, they are very similar to a Column
except that they lack the main and cross-axis alignment properties. To continue on with our previous example for columns when we placed names under each other, I’ll display the very same column converted into a ListView:
ListView(
children: <Widget>[
Text("Mark"),
Text("Imola"),
Text("Martin"),
Text("Zoe"),
],
),
Tada! ? Your first ListView in Flutter! When refreshing or rebuilding the app (by either pressing a small or capital R in the Flutter CLI), you’ll see the very same thing you saw previously.
However, if you try to drag it, you are now able to scroll inside the container! Note that when a Column
has bigger children than its bounds, it will overflow, but a ListView
will be scrollable.
ListView builder
While the ListView widget is cool and good, it may not be suitable for every use case. For example, when displaying a list of tasks in a todo app, you won’t exactly know the number of items in your list while writing the code, and it may even change over time. Sure, you are able to run .map
on the data source, return widgets as results, and then spread it with the ...
operator, but that obviously wouldn’t be performant, nor is it a good practice for long lists. Instead, Flutter provides us a really nice ListView builder.
Sidenote: while working with Flutter, you’ll see the word “builder” a lot. For example, in places like FutureBuilder, StreamBuilder, AnimatedBuilder, the build method, the ListView builder, and more. It’s just a fancy word for methods that return a Widget
or [Widget]
, don’t let this word intimidate or confuse you!
So how do we work with this awesome method? First, you should have an array or list that the builder can iterate over. I’ll quickly define an array with some names in it:
final List<String> source = ["Sarah", "Mac", "Jane", "Daniel"];
And then, somewhere in your widget tree, you should be able to call the ListView.builder
method, provide some properties, and you’ll be good to go:
ListView.builder(
itemCount: source.length,
itemBuilder: (BuildContext context, int i) => Text(source[i]),
),
Oh, and notice how I was able to use an arrow function, just like in JavaScript!
The itemCount
parameter is not required, but it’s recommended. Flutter will be able to optimize your app better if you provide this parameter. You can also limit the maximum number of rendered items by providing a number smaller than the length of your data source.
When in doubt, you can always have a peek at the documentation of a class, method, or widget by hovering over its name in your editor:
And that sums up the layout and list-related part of this episode. We’ll look into providing “stylesheets” (or theme data) for your app, look at some basic routing (or navigation) methods, and fetch some data from the interwebs with HTTP requests.
Theming in Flutter
While building larger applications with custom UI components, you may want to create stylesheets. In Flutter, they are called Themes, and they can be used in a lot of places. For example, you can set a default app color, and then the selected texts, buttons, ripple animations, and more will follow this color. You can also set up text styles (like headings and more), and you’ll be able to access these styles across the app.
To do so, you should provide a theme
property for your MaterialApp
at the root level of the application. Here’s an example:
return MaterialApp(
title: 'RisingStack Flutter Demo',
theme: ThemeData(
// Define the default brightness and colors.
brightness: Brightness.light,
primaryColor: Colors.green[300],
accentColor: Colors.green,
// Define button theme
buttonTheme: ButtonThemeData(
buttonColor: Colors.green,
shape: CircleBorder(),
),
// Define the default font family
// (this won’t work as we won’t have this font asset yet)
fontFamily: 'Montserrat',
// Define the default TextTheme. Use this to specify the default
// text styling for headlines, titles, bodies of text, and more.
textTheme: TextTheme(
headline1: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
headline6: TextStyle(fontSize: 36.0, fontStyle: FontStyle.italic),
bodyText2: TextStyle(fontSize: 14.0, fontFamily: 'Muli'),
),
),
home: Scaffold(...),
);
These colors will be used throughout our app, and accessing the text themes is also simple as a pickle! I added a RaisedButton
on top of the app so that we can see the new ButtonThemeData
being applied to it:
It’s ugly and all, but it’s ours! ? Applying the text style won’t be automatic, though. As we previously discussed, Flutter can’t really read your mind, so you explicitly need to tag Text widgets as a headline1
or bodyText2
, for example.
To do so, you’ll use the Theme.of(context)
method. This will look up the widget tree for the nearest Theme
providing widget (and note that you can create custom or local themes for subparts of your app with the Theme
widget!) and return that theme. Let’s look at an example:
Text(
"cool names",
style: Theme.of(context).textTheme.headline6,
),
You can see that we are accessing the theme with the Theme.of(context)
method, and then we are just accessing properties like it’s an object. This is all you need to know about theming a Flutter app as it really isn’t a complex topic!
Designing mobile navigation experiences
On the web, when managing different screens of the app, we used paths (e.g. fancysite.com/registration
) and routing (e.g., react-router
) to handle navigating back and forth the app. In a mobile app, it works a bit differently, so I’ll first introduce you to navigation on mobile, and then we’ll look into implementing it in Flutter.
Mobile navigation differs from the web in a lot of ways. Gestures and animations play a very heavy role in structuring out the hierarchy of the app for your user. For example, when a user navigates to a new screen, and it slides in from the right side of the screen, the user will expect to be able to move back with a slide from the left. Users also don’t expect flashy loadings and empty screens when navigating – and even though there are advancements on the web in this segment (e.g. PWAs), it’s by far not the default experience when using websites.
There are also different hierarchies when designing mobile apps. The three main groups are:
- Hierarchical Navigation (e.g. the Settings app on iOS)
- New screens slide in from left to right. The expected behavior for navigating back is with a back button on the upper left corner and by swiping from the left edge of the screen to the right.
- Flat Navigation (e.g. the Apple Music app)
- The default behavior for this hierarchy is a tab bar on the bottom.
- Tabs should always preserve location (e.g. if you navigate to a subscreen inside on tab one, switch to tab two and switch back to tab one, you’d expect to be on the subscreen, not on the root level screen.)
- Swiping between tabs is optional. It isn’t the default behavior and it may conflict with other gestures on the screen itself – be cautious and think twice before implementing swipeable tab bars.
- Custom, content-driven, or experimental navigation (Games, books and other content)
- When making experimental navigation, always try to be sane with the navigation. The user should always be able to navigate back and undo stuff.
I created a handy cheat sheet for you that will remind you of the most important things when in doubt:
Also, all of these can be mixed together, and other screens like modals can be added to the stack. Always try to KISS and make sure that the user can always navigate back and undo things. Don’t try to reinvent the wheel with navigation (e.g., reverse the direction of opening up a new screen) as it will just confuse the user.
Also, always indicate where the user is in the hierarchy (e.g., with labeling buttons, app title bar, coloring the bottom bar icons, showing little dots, etc.). If you want to know more about designing mobile navigation experiences and implementing them in a way that feels natural to the user, check out Apple’s Human Interface Guideline’s related articles.
Navigation in Flutter
When routing on the web with React or React-Native, you had to depend on third-party libraries to get the dirty work done for you (e.g. react-router
). Luckily, Flutter has native navigation capabilities out of the box, and they cover every need of most of the apps, and they are provided to you via the Navigator API.
The applications of this API and the possibilities to play around with navigation are endless. You can, for example, animate a widget between screens; build a bottom navigation bar or a hamburger menu; pass arguments; or send data back and forth. You can explore every navigation-related Flutter cookbook here. In this series, we’ll only look into initializing two screens, navigating between them, and sharing some widgets between them.
To get started with navigation, let’s create two widgets that we’ll use as screens and pass the first into a MaterialApp
as the home
property:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: ScreenOne(),
);
}
}
class ScreenOne extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text("hey! ?"),
),
);
}
}
class ScreenTwo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text("hi! ??"),
),
);
}
}
This was easy as a breeze. If you run this app in a simulator, you’ll see “hey! ?” on the center of the screen. Now, inside the MaterialApp
, we can define our routes
:
return MaterialApp(
title: 'Flutter Demo',
home: ScreenOne(),
routes: <String, WidgetBuilder>{
'/hey': (BuildContext context) => ScreenOne(),
'/hi': (BuildContext context) => ScreenTwo(),
},
);
Then, we’ll need something that will trigger the navigation. I’ll add a RaisedButton
to the ScreenOne
:
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("hey! ?"),
RaisedButton(
child: Text("take me there"),
onPressed: () {
print("hi!");
},
),
],
),
);
And now, we can navigate the user to the next screen when the button is pressed. Notice that I replaced the Center
with a Column
with both its main and cross axises centered. This was required because I wanted to have two children underneath each other: a Text
and a RaisedButton
. Inside the RaisedButton
, we only have to push the route to the stack and let Flutter handle the routing and animation:
Navigator.pushNamed(context, '/hi');
By default, we can navigate back to the previous screen by swiping from the left edge of the screen. This is the expected behavior, and we don’t intend to change it, so we’ll leave it as it is. If you want to add a button on the second screen to navigate back to the first screen, you can use Navigator.pop();
method.
Don’t ever push
to the same screen the user is on, nor the previous screen. Always use pop
when navigating backward.
This will be just enough to cover your basic navigation needs. Don’t forget, if you want to check out more advanced navigation features such as animating widgets between screens or passing data back and forth, check out the related Flutter cookbooks.
Networking, HTTP requests
Now that you can build widgets, layouts, display lists, and you can navigate between screens with Flutter, there’s only one thing left: communicating with your backend API. One of the most popular BaaS providers for mobile and Flutter is Firebase by Google. It allows you to use real-time databases, push notifications, crash reporting, app analytics, and a lot more out of the box. You can find the Flutter Firebase packages on pub.dev or you can follow this step-by-step tutorial.
If you are a more experienced developer and you have a complex project with a custom backend in mind, or if you are just genuinely looking forward to using your own selection of backend APIs – Firebase just won’t suit your needs.
That’s where the http package comes in handy.
Just add it into your dependency list inside the pubspec.yaml
, wait until flutter pub get
finishes (VSCode automatically runs it for you if it detects changes in the pubspec.yaml
), and then continue reading:
dependencies:
flutter:
sdk: flutter
http: any
http
is a Future-based library for making HTTP requests. To get started with it, just import it:
import 'package:http/http.dart' as http;
And then, you can start making requests with top-level methods like http.post
or http.get
. To help you experiment with making HTTP requests in Flutter, I have made a demo API that you can GET
on. It will return some names and ages. You can access it here (https://demo-flutter-api.herokuapp.com/people
).
Parsing JSON data in Flutter and Dart
After making your GET
request on the API, you’ll be able to get data out of it by accessing properties like this:
void request() async {
final response =
await http.get("https://demo-flutter-api.herokuapp.com/people");
print(response.body); // => [{"name":"Leo","age":17},{"name":"Isabella","age":30},{"name":"Michael","age":23},{"name":"Sarah","age":12}]
print(json.decode(response.body)[0]["name"]); // => Leo
}
However, this solution should not be used in production. Not only it lacks automatic code completion and developer tooling, but it’s very error-prone and not really well documented. It’s just straight-up crap coding. ?
Instead, you should always create a Dart class with the desired data structure for your response object and then process the raw body into a native Dart object. Since we are receiving an array of objects, in Dart, we’ll create a typed List
with a custom class. I’ll name the class Person
, and it will have two properties: a name
(with a type of String
) and age
(int
). I’ll also want to define a .fromJson
constructor on it so that we can set up our class to be able to construct itself from a raw JSON string.
First, you’ll want to import dart:convert
to access native JSON-related methods like a JSON encoder and decoder:
import 'dart:convert';
Create our very basic class:
class Person {
String name;
int age;
}
Extend it with a simple constructor:
Person({this.name, this.age});
And add in the .fromJson
method, tagged with the factory
keyword. This keyword informs the compiler that this isn’t a method on the class instance itself. Instead, it will return a new instance of our class:
factory Person.fromJson(String str) => Person.fromMap(json.decode(str));
factory Person.fromMap(Map<String, dynamic> json) => new Person(
name: json["name"],
age: json["age"],
);
Notice that I created two separate methods: a fromMap
and a fromJson
. The fromMap
method itself does the dirty work by deconstructing the received Map
. The fromJson
just parses our JSON string and passes it into the fromMap
factory method.
Now, we should just map over our raw response, use the .fromMap
factory method, and expect everything to go just fine:
List<Person> listOfPeople = json
.decode(response.body)
.map<Person>((i) => Person.fromMap(i))
.toList();
print(listOfPeople[0].name); // => Leo
Sidenote: I didn’t use the .fromJson
method because we already parsed the body before mapping over it, hence it’s unneeded right now.
There is a lot to unwrap in these few lines! First, we define a typed list and decode the response.body
. Then, we map over it, and we throw in the return type <Person>
to the map
so that Dart will know that we expect to see a Person
as a result of the map function. Then, we convert it to a List
as otherwise it would be an MappedListIterable
.
Rendering the parsed JSON: FutureBuilder and ListView.builder
Now that we have our app up and running with our basic backend, it’s time to render our data. We already discussed the ListView.builder
API, so we’ll just work with that.
But before we get into rendering the list itself, we want to handle some state changes: the response may be undefined at the moment of rendering (because it is still loading), and we may get an error as a response. There are several great approaches to wrap your head around handling these states, but we’ll use FutureBuilder
now for the sake of practicing using new Flutter widgets.
FutureBuilder
is a Flutter widget that takes a Future and a builder as a property. This builder will return the widget we want to render on the different states as the Future
progresses.
Note that FutureBuilder
handles state changes inside the widget itself, so you can still use it in a StatelessWidget
! Since the http
package is Future-based, we can just use the http.get
method as the Future for our FutureBuilder
:
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: http.get("https://demo-flutter-api.herokuapp.com/people"),
),
);
}
And we should also pass a builder
. This builder should be able to respond to three states: loading
, done
and error
. At first, I’ll just throw in a centered CircularProgressIndicator()
to see that our app renders something:
return Scaffold(
body: FutureBuilder(
future: http.get("https://demo-flutter-api.herokuapp.com/people"),
builder: (BuildContext context, AsyncSnapshot<http.Response> response) {
return Center(
child: CircularProgressIndicator(),
);
},
),
);
If you run this app, you’ll see a progress indicator in the center of the screen running indefinitely. We can get the state of the response
by the response.hasData
property:
builder: (BuildContext context, AsyncSnapshot<http.Response> response) {
if (response.hasData) {
// loaded!
} else if (response.hasError) {
// error!
return Center(
child: Text("error!"),
);
} else {
// loading...
return Center(
child: CircularProgressIndicator(),
);
}
},
And now, we can be sure that nothing comes between us and processing, then rendering the data, so inside the response.hasData
block, we’ll process the raw response with previously discussed parsing and mapping method, then return a ListView.builder
:
// loaded!
List<Person> listOfPeople = json
.decode(response.data.body)
.map<Person>((i) => Person.fromMap(i))
.toList();
return ListView.builder(
itemCount: listOfPeople.length,
itemBuilder: (BuildContext context, int i) => Text(
"${listOfPeople[i].name} (${listOfPeople[i].age})",
),
);
And that’s it! ? If you run this snippet right now, it will render four names and their corresponding ages next to them. Isn’t this amazing? It may have seemed like a lot of work for a simple list like this, but don’t forget that we created a whole-blown class, parsed JSON, and converted it into class instances, and we even handled loading and error states.
Summing it all up
Congratulations on making it this far into the course! You have learned a lot and came along a long way since we started in the previous episode.
You went from zero to hero both with Dart (types, control flow statements, data structures, OOP, and asynchrony) and Flutter (CLI, widgets, alignment, lists, themes, navigation and networking).
This really has been a lot of work, and you’ll still have to learn a lot until you get fluent in Flutter, but in the end, the only thing that will matter is the result of your hard work. And that’s what we’re going to harvest in the next and last episode of this Flutter series: we’ll build a fun mini-game with Dart and Flutter! ?
I’m really looking forward to seeing you here next week. Until then, stay tuned, and happy Fluttering! ✌️
All the bests, ?
Daniel from RisingStack