Getting Started With Flutter
Dive into the Flutter framework, which lets you build iOS, Android, web and desktop apps with a single codebase, by writing a cross-platform app using VS Code. By Jonathan Sande.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started With Flutter
30 mins
- Getting Started
- Introduction to Flutter
- Setting up Your Development Environment
- Creating a New Project
- Editing the Code
- Running the App
- Using Hot Reload
- Importing a File
- Understanding Widgets
- Creating Widgets
- Making Network Calls
- Importing Packages
- Using Asynchronous Code
- Using a ListView
- Adding Dividers
- Parsing to Custom Types
- Downloading Images With NetworkImage
- Cleaning up the Code
- Adding a Theme
- Where to Go From Here?
With various operating systems running the world’s mobile and desktop devices, cross-platform development has long been a goal for app development. Being able to write one codebase and deploy to multiple platforms saves significant time and effort for your company and team.
One of the most recent frameworks to enter the cross-platform arena is Flutter from Google. While Flutter initially only supported the Android and iOS mobile platforms, it has since expanded to include support for the web, macOS, Windows, Linux, Fuchsia and embedded devices. This — in combination with Flutter’s fast development cycle, flexible UI design and native app performance — make it a very attractive target for new and experienced developers alike.
In this tutorial, you’ll build a Flutter app called GHFlutter that queries the GitHub API for team members in a GitHub organization and displays the information in a scrollable list:
You can develop the app using iOS Simulator, the Android emulator, a web browser, a native desktop app or all of the above!
Here are some things you’ll learn how to do with Flutter while you build the app:
- Set up a development environment.
- Create a new project.
- Use hot reload.
- Import files and packages.
- Create and use widgets.
- Make network calls.
- Show items in a list.
- Add an app theme.
You’ll also learn a little Dart along the way! :]
Getting Started
Download the starter project for this tutorial by clicking the Download Materials button at the top or bottom of this tutorial.
You can use macOS, Linux, Windows or Chrome OS for your Flutter development. While you can use any editor with the Flutter toolchain, there are IDE plugins for IntelliJ IDEA, Android Studio and Visual Studio Code that make the development cycle easier.
This tutorial uses VS Code.
Introduction to Flutter
Flutter apps use the Dart programming language. Dart shares many of the features of other modern languages, such as Kotlin and Swift. You can also transcompile Dart into JavaScript code. If you’d like to learn more about Dart before continuing with this Flutter tutorial, check out our Dart Basics tutorial.
As a cross-platform framework, Flutter most closely resembles React Native. Both allow for a reactive and declarative style of programming. Unlike React Native, however, Flutter doesn’t need to use a JavaScript bridge, which improves app startup times and overall performance. Dart achieves this by using Ahead-Of-Time (AOT) compilation.
Dart can also use Just-In-Time (JIT) compilation. JIT compilation with Flutter improves the development workflow by allowing a hot reload capability to refresh the UI during development without the need for an entirely new build.
As you’ll see in this tutorial, the Flutter framework is built around the idea of widgets. In Flutter, you don’t only use widgets for your app’s views, but also for entire screens and even for the app itself.
Setting up Your Development Environment
Find instructions for setting up your development machine with the Flutter framework on Flutter’s Get started page. The specific steps vary by platform, but they follow this basic format:
- Download the installation bundle for your development machine’s operating system to get the latest stable release of the Flutter SDK.
- Extract the installation bundle in the desired location.
- Add the
flutter
tool to your path. - Run the
flutter doctor
command, which alerts you to any problems with the Flutter installation. - Install missing dependencies.
- Set up your IDE with a Flutter plugin/extension.
- Test drive an app.
The instructions provided on the Flutter website are very well done and allow you to easily set up a development environment on your platform of choice. The remainder of this tutorial assumes you’ve set up VS Code for Flutter development and that you’ve addressed any issues flutter doctor
found. You can also use Android Studio to follow along.
To run your project as a mobile app, you’ll need to use one of the following options:
- Run either iOS Simulator or an Android emulator.
- Have an iOS or Android device set up for development.
- Run your code as a web app.
- Finally, you can run your code as a desktop app.
Even if your final target is mobile, using a web or desktop app during development gives you the advantage of being able to resize the app and observe how it would look with various screen sizes. If you have an older computer, the web or desktop version will also load faster than the Android emulator or iOS Simulator.
Creating a New Project
In VS Code with the Flutter extension installed, open the command palette by choosing View ▸ Command Palette… or pressing Command-Shift-P on macOS or Control-Shift-P on Linux or Windows. Enter Flutter: New Application Project into the palette and press Return.
Select a folder to store the project. Then, enter ghflutter for the project’s name, press Return and wait for Flutter to set up the project in VS Code. When the project is ready, you’ll see main.dart in your editor.
In VS Code, you see a panel on the left-hand side that shows your project structure. There are folders for Android, iOS and the web. These folders contain the files necessary to deploy your app on those platforms. There’s also a lib folder that contains main.dart and will have code that applies to both platforms. You’ll work mainly in the lib folder in this tutorial.
Testing is an important part of Flutter development. However, it isn’t the focus of this tutorial, so delete the test folder by right-clicking it and selecting Delete from the menu.
Editing the Code
Next, replace the code in main.dart with the following:
import 'package:flutter/material.dart';
void main() => runApp(const GHFlutterApp());
class GHFlutterApp extends StatelessWidget {
const GHFlutterApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GHFlutter',
home: Scaffold(
appBar: AppBar(
title: const Text('GHFlutter'),
),
body: const Center(
child: Text('GHFlutter'),
),
),
);
}
}
If you’re using Android Studio, don’t worry about purposefully saving — it autosaves by default.
In the code above, main
uses the =>
operator for a single line function to run the app. You have one class for the app named GHFlutterApp
.
Your app itself is a stateless widget. Most entities in a Flutter app are widgets, either stateless or stateful. You override build
to create your app widget. Right after the return
keyword, you see the MaterialApp
widget, which helps your app conform to the Material Design guidelines.
Running the App
Now, it’s time to run your simple app. Click the currently selected platform in the bottom right to get a list of all the available platforms that can run your app. The image here shows that the Chrome web browser, iOS simulator and a Pixel Android emulator are available.
Select a platform — for example, the Pixel mobile emulator — and wait while the emulator launches.
Once the emulator is ready, build and run by pressing F5, by selecting Debug ▸ Start Debugging from the menu or by clicking the triangular Play icon in the top right. The Debug Console will open and, if you’re running on Android, you’ll see Gradle doíng the build. If running on iOS, you’ll see Xcode building the project.
Here’s the app running in the Android emulator:
Take a moment to appreciate the fact that you just ran your first Flutter app. Nice job!
The DEBUG banner you see across the top-right corner indicates that the app is running in debug mode.
Stop the running app by clicking the square red Stop button at the right of the toolbar that’s at the top of the VS Code window:
Return to the Project Explorer view by clicking the Explorer icon at the upper left of VS Code or by choosing View ▸ Explorer.
Using Hot Reload
One of the best aspects of Flutter development is that you can hot reload your app as you make changes. This feature allows you to get instant feedback as you update different parts of the UI.
Build and run again:
Now, without stopping the running app, change the app bar string in main.dart to something else:
appBar: AppBar(
title: const Text('GHFlutter App'),
),
Previously, the banner read GHFlutter; now, it says GHFlutter App.
Now, save main.dart. This will automatically trigger a hot reload, but you can also click the Hot Reload button:
You’ll see the change reflected in the running app almost immediately:
The hot reload feature might not always work — the Hot Reload official docs do a really nice job of explaining the cases where it does not work — but overall, it’s a great time saver when you’re building out your UI.
Importing a File
Rather than keeping all your Dart code in the single main.dart file, you want to be able to import code from other files you create. You’ll see an example now for importing strings, which will help when you need to localize your user-facing strings.
Create a file named strings.dart in the lib folder by clicking lib and then clicking the New File button:
Add the following line to the new file:
const appTitle = 'GHFlutter';
Add the following import to the top of main.dart:
import 'strings.dart' as strings;
Change your widget to use the new strings.dart. GHFlutterApp
will look like this:
class GHFlutterApp extends StatelessWidget {
const GHFlutterApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: strings.appTitle,
home: Scaffold(
appBar: AppBar(
title: const Text(strings.appTitle),
),
body: const Center(
child: Text(strings.appTitle),
),
),
);
}
}
Save your changes and hot reload. The app is back in its original state now, but you’re using a string from strings.dart.
Understanding Widgets
Almost every element of your Flutter app is a widget. Widgets are designed to be immutable, or unchangeable, since using immutable widgets helps keep the app UI lightweight. You can think of widgets like blueprints that tell how the UI should look. A different look needs a different blueprint.
You’ll use two fundamental types of widgets:
- Stateless: Widgets that depend only upon their own configuration info, such as a static image in an image view.
- Stateful: Widgets that need to maintain dynamic information. They do so by interacting with a State object.
Both stateless and stateful widgets redraw whenever the Flutter framework tells them to. The difference is that the stateful widgets delegate their configuration to a State
object.
Creating Widgets
To make your own widgets, go to the bottom of main.dart and start typing stful, an abbreviation for “stateful”. This will give you a pop-up similar to the following:
Press Return to select the first option.
VS Code will help you fill in the name using multiple cursors. Write GHFlutter:
This is the new code that you just added:
class GHFlutter extends StatefulWidget {
const GHFlutter({ Key? key }) : super(key: key);
@override
_GHFlutterState createState() => _GHFlutterState();
}
class _GHFlutterState extends State<GHFlutter> {
@override
Widget build(BuildContext context) {
return Container(
);
}
}
Here are some things to note:
- You’ve made a
StatefulWidget
subclass namedGHFlutter
. - The line that begins with
const
is the class constructor. - You’re overriding
createState
to create the stateful widget’s state object. -
_GHFlutterState
is the name of the state class. The underscore in front of_GHFlutterState
means that this class is file-private. It can’t be imported into other files. -
build
is the primary place where you construct your widgets. This one currently returns an emptyContainer
by default. You’ll swap that out with something else next.
Replace the entire build
method in _GHFlutterState
with the following:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(strings.appTitle),
),
body: const Text(strings.appTitle),
);
}
A Scaffold is a container for Material Design widgets. It acts as the root of a widget hierarchy. Here, you’ve added an AppBar
and a body
to the Scaffold, and each contains a Text
widget.
Replace GHFlutterApp
with the following code:
class GHFlutterApp extends StatelessWidget {
const GHFlutterApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: strings.appTitle,
home: const GHFlutter(),
);
}
}
Now, GHFlutterApp
uses your new GHFlutter
as its home
attribute, instead of building a Scaffold of its own.
const
, which you sometimes see in front of widgets and variables, indicates a compile-time constant. It’s not always possible or necessary to add const
, but doing so allows Flutter to make some optimizations.Hot reload and you’ll see the new widget in action:
You haven’t changed much yet, but now you’re set up to build out the new widget.
Making Network Calls
Earlier, you imported strings.dart into the project. Similarly, you can import other packages from the Flutter framework or even from other developers. For example, you’ll now use a couple of additional packages to make an HTTP network call and parse the resulting JSON response into Dart objects.
Importing Packages
Add two new imports at the top of main.dart:
import 'dart:convert';
import 'package:http/http.dart' as http;
You’ll notice the http
package isn’t available. That’s because you haven’t added it to the project yet.
Navigate to pubspec.yaml in the root folder of your project. In the dependencies section, right under cupertino_icons: ^1.0.2, add the following line:
http: ^0.13.3
cupertino_icons
package has.Now, when you save your pubspec.yaml, the Flutter extension in VS Code will run the flutter pub get
command. Flutter will get the declared http package and make it available in main.dart.
You’ll now see blue lines under your two most recent imports in main.dart, which indicates they’re currently unused.
Don’t worry. You’ll use them in just a bit.
Using Asynchronous Code
Dart apps are single-threaded, but Dart provides support for running code on other threads. It also supports running asynchronous code that doesn’t block the UI thread. It uses the async/await pattern to do this.
async
methods all runs on the UI thread. It’s just scheduled to run later, when the UI isn’t busy. If you actually did want to run some code on another thread, then you’d need to create what’s called a new Dart isolate.Next, you’ll make an asynchronous network call to retrieve a list of GitHub team members. To do this, add an empty list as a property in _GHFlutterState
as well as a property to hold a text style:
var _members = <dynamic>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
As you learned earlier, the underscores at the beginning of the names make the members of the class file-private. The dynamic
keyword tells Dart that the list could hold anything. Generally, it’s not ideal to use dynamic
because it opts out of the type safety system that Dart has. However, when making network calls, dealing with dynamic
is unavoidable.
To make the asynchronous HTTP call, add _loadData
to _GHFlutterState
:
Future<void> _loadData() async {
const dataUrl = 'https://api.github.com/orgs/raywenderlich/members';
final response = await http.get(Uri.parse(dataUrl));
setState(() {
_members = json.decode(response.body) as List;
});
}
Here, you’ve added the async
keyword onto _loadData
to tell Dart that it’s asynchronous. Another clue that it’s asynchronous is the Future
return type. You put await
in front of http.get()
because that’s another asynchronous call that might take a while.
When the HTTP call completes, you pass a callback to setState
that runs synchronously on the UI thread. In this case, you’re decoding the JSON response and assigning it to the _members
list. If you set _members
without calling setState
, then Flutter wouldn’t rebuild the UI and your users wouldn’t realize that the state had changed.
Add an initState
override to _GHFlutterState
:
@override
void initState() {
super.initState();
_loadData();
}
This method calls _loadData
when the state class is first created.
Now that you’ve created a list of members in Dart, you need a way to display them in a list in the UI.
Using a ListView
Dart provides a ListView that lets you show the data in a list. ListView
acts like a RecyclerView
on Android or a UICollectionView
on iOS, recycling views as the user scrolls through the list to achieve smooth scrolling performance.
Add _buildRow
to _GHFlutterState
:
Widget _buildRow(int i) {
return ListTile(
title: Text('${_members[i]['login']}', style: _biggerFont),
);
}
Here, you’re returning a ListTile that shows the login name parsed from the JSON for the member at index i
. It also uses the text style you created before.
Replace the body
line in the build
method of _GHFlutterState
with the following:
body: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _members.length,
itemBuilder: (BuildContext context, int position) {
return _buildRow(position);
}),
The padding adds some empty space around each list item — 16 logical pixels, in this case. Setting itemCount
tells ListView
how many rows it will have in total. Finally, call itemBuilder
for every new row that’s visible on the screen, which you use as an opportunity to build your custom ListTile
in _buildRow
.
At this point, you’ll probably need to do a full restart rather than a hot reload. The Hot Restart button is useful for that. It’s still faster than completely stopping the app and rebuilding it.
After the restart, you’ll see the following:
That’s how easy it is to make a network call, parse the data and show the results in a list!
Now, it’s time to make the list a bit prettier.
Adding Dividers
To add dividers into the list, you’ll use ListView.separated
instead of ListView.builder
. Replace the Scaffold’s body
with the code below:
body: ListView.separated(
itemCount: _members.length,
itemBuilder: (BuildContext context, int position) {
return _buildRow(position);
},
separatorBuilder: (context, index) {
return const Divider();
}),
Using ListView.separated
gives you a separatorBuilder
option, which allows you to add a Divider between the list tiles. Now that you have dividers, you also removed the padding from the builder.
Hot reload. Now, you’ll see dividers between the rows:
To add padding to each row, wrap ListTile
with Padding
inside _buildRow
. The easiest way to do that in VS code is to put your cursor on ListTile
and press Command-. on macOS or Control-. on Windows. Then increase the padding to 16.0
, as shown in the animated GIF below.
Alternatively, you can replace _buildRow
with the following code to achieve the same result:
Widget _buildRow(int i) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ListTile(
title: Text('${_members[i]['login']}', style: _biggerFont),
),
);
}
ListTile
is now a child widget of Padding
. Hot reload to see the padding appear on the rows, but not on the dividers.
Parsing to Custom Types
In the previous section, the JSON parser took the members in the JSON response and assigned them to _members
. Although you defined the list as dynamic
, the actual type that Dart put in the list was Map
, a data structure that holds key-value pairs. This is the equivalent of a Map in Kotlin or a Dictionary in Swift.
However, you also want to be able to use your own custom types.
Add a new Member
type at the bottom of main.dart:
class Member {
Member(this.login);
final String login;
}
Member
has a constructor that sets the login property when you create a member object.
Update the _members
declaration in _GHFlutterState
so that it’s a list of Member
objects:
final _members = <Member>[];
You used final
in place of var
because, instead of reassigning a new list to _members
, you’re going to add items to the existing list now.
Replace setState
in _loadData
with the following code:
setState(() {
final dataList = json.decode(response.body) as List;
for (final item in dataList) {
final login = item['login'] as String? ?? '';
final member = Member(login);
_members.add(member);
}
});
This turns each decoded map into a Member
and adds it to the list of members.
?
and double ??
question marks? Check out the Dart Basics tutorial for more details about these operators.Flutter is still complaining inside ListTile
because it was expecting Map
rather than Member
. Replace the title
line of ListTile
with the following:
title: Text('${_members[i].login}', style: _biggerFont),
You’ll see an error if you try a hot reload, so hot restart. You’ll see the same screen as before, except that it now uses your new Member
class.
Downloading Images With NetworkImage
In GitHub, each member has a URL for their avatar. Your next improvement is to add that avatar to the Member
class and show the avatars in the app.
Update Member
to add an avatarUrl
property. It should look like this now:
class Member {
Member(this.login, this.avatarUrl);
final String login;
final String avatarUrl;
}
Since avatarUrl
is now a required parameter, Flutter complains at you in _loadData
. Replace the setState
callback in _loadData
with the following updated version:
setState(() {
final dataList = json.decode(response.body) as List;
for (final item in dataList) {
final login = item['login'] as String? ?? '';
final url = item['avatar_url'] as String? ?? '';
final member = Member(login, url);
_members.add(member);
}
});
The code above uses the avatar_url
key to look up the URL value in the map parsed from JSON, then set it to the url
string, which you pass on to Member
.
Now that you have access to the URL for the avatar, add it to your ListTile
. Replace _buildRow
with the following:
Widget _buildRow(int i) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ListTile(
title: Text('${_members[i].login}', style: _biggerFont),
leading: CircleAvatar(
backgroundColor: Colors.green,
backgroundImage: NetworkImage(_members[i].avatarUrl),
),
),
);
}
This adds a CircleAvatar
to the leading edge of your ListTile
. While you’re waiting for the images to download, the background of the CircleAvatar
will be green.
Do a hot restart rather than a hot reload. You’ll see your member avatars in each row:
Cleaning up the Code
Most of your code is now in main.dart. To make the code a little cleaner, you’ll refactor classes into their own files.
Create files named member.dart and ghflutter.dart in the lib folder. Move Member
into member.dart and both _GHFlutterState
and GHFlutter
into ghflutter.dart.
You won’t need any import statements in member.dart, but the imports in ghflutter.dart should be:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'member.dart';
import 'strings.dart' as strings;
You also need to update the imports in main.dart. Replace the entire file with the following:
import 'package:flutter/material.dart';
import 'ghflutter.dart';
import 'strings.dart' as strings;
void main() => runApp(const GHFlutterApp());
class GHFlutterApp extends StatelessWidget {
const GHFlutterApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: strings.appTitle,
// TODO: add theme here
home: const GHFlutter(),
);
}
}
Save everything and rerun the app. You won’t see a change, but the code is now a little cleaner. :]
You might have noticed a new comment: // TODO: add theme here
. There, you’ll make one last change before the tutorial is over.
Adding a Theme
Your final improvement is to easily add a theme to the app by adding a theme attribute to the MaterialApp
you created in main.dart.
Find // TODO: add theme here
and replace it with the following:
theme: ThemeData(primaryColor: Colors.green.shade800),
Here, you’re using a shade of green as a Material Design color value for the theme.
Save and hot reload to see the new theme in action:
The app screenshots so far have been from the Android emulator. You can also run the final themed app in iOS Simulator:
And here’s how it looks on the Chrome web browser:
Feel free to run it as a Windows, Mac or Linux app as well. It only requires a little extra setup and adding desktop support to your app. On macOS, you should also give the app permission to access the internet.
Here’s the app running on macOS:
Now that’s what you call cross-platform! :]
Where to Go From Here?
Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
You can open the project in either VS Code or Android Studio.
Open it in VS Code by opening the root folder. You’ll need to fetch packages before running the project. Do so by pressing Command-Shift-P on MacOS or Control-Shift-P on Windows or Linux to open the command palette and running the Flutter: Get Packages command.
To open the project in Android Studio, choose Open an existing project from the Welcome to Android Studio screen and navigate to choose the root folder of the final project. Then choose Get dependencies on the 'Pub get' has not been run
line in Android Studio.
There’s a lot more to learn about Flutter and Dart. Here are some places to start:
- The main Flutter page at flutter.dev. You’ll find lots of great documentation and other information.
- Learn more about widgets in Introduction to Widgets.
- If you’re coming from another platform, check out the Flutter guides for Android, iOS and React Native developers.
- For a deep dive into the most important aspects of Flutter, read our Flutter Apprentice book.
And you’ll definitely want to browse the raywenderlich.com Flutter library! There’s an ever-increasing selection of books, articles and videos to help you on your Flutter journey.
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!