Write Your First Flutter App
This is a guide to creating your first Flutter app. If you are familiar with object-oriented code and basic programming concepts such as variables, loops, and conditionals, you can complete this tutorial. You don’t need previous experience with Dart or mobile programming.
- Step 1: Create the starting Flutter app
- Step 2: Use an external package
- Step 3: Add a Stateful widget
- Step 4: Create an infinite scrolling ListView
- Step 5: Add interactivity
- Step 6: Navigate to a new screen
- Step 7: Change the UI using Themes
- Well done!
What you'll build
You’ll implement a simple mobile app that generates proposed names for a startup company. The user can select and unselect names, saving the best ones. The code generates ten names at a time. As the user scrolls, new batches of names are generated. The user can tap the list icon in the upper right of the app bar to move to a new route that lists only the favorited names.
The animated GIF shows how the finished app works.
What you’ll learn:
- Basic structure of a Flutter app.
- Finding and using packages to extend functionality.
- Using hot reload for a quicker development cycle.
- How to implement a stateful widget.
- How to create an infinite, lazily loaded list.
- How to create and navigate to a second screen.
- How to change the look of an app using Themes.
What you’ll use:
You’ll need to install the following:
-
Flutter SDK
The Flutter SDK includes Flutter’s engine, framework, widgets, tools, and a Dart SDK. This codelab requires v0.1.4 or later. -
Android Studio IDE
This codelab features the Android Studio IDE, but you can use another IDE, or work from the command line. -
Plugin for your IDE
The Flutter and Dart plugins must be installed separately for your IDE. Besides Android Studio, Flutter and Dart plugins are also available for the VS Code and IntelliJ IDEs.
See Flutter Installation and Setup for information on how to set up your environment.
Step 1: Create the starting Flutter app
Create a simple, templated Flutter app, using the instructions in Getting Started with your first Flutter app. Name the project startup_namer (instead of myapp). You’ll be modifying this starter app to create the finished app.
In this codelab, you’ll mostly be editing lib/main.dart, where the Dart code lives.
-
Replace lib/main.dart.
Delete all of the code from lib/main.dart. Replace with the following code, which displays “Hello World” in the center of the screen.import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Welcome to Flutter', home: new Scaffold( appBar: new AppBar( title: new Text('Welcome to Flutter'), ), body: new Center( child: new Text('Hello World'), ), ), ); } }
-
Run the app. You should see the following screen.
Observations
-
This example creates a Material app. Material is a visual design language that is standard on mobile and the web. Flutter offers a rich set of Material widgets.
-
The main method specifies fat arrow (
=>
) notation, which is short hand used for one-line functions or methods. -
The app extends StatelessWidget which makes the app itself a widget. In Flutter, almost everything is a widget, including alignment, padding, and layout.
-
The Scaffold widget, from the Material library, provides a default app bar, title, and a body property that holds the widget tree for the home screen. The widget subtree can be quite complex.
-
A widget’s main job is to provide a
build()
method that describes how to display the widget in terms of other, lower level widgets. -
The widget tree for this example consists of a Center widget containing a Text child widget. The Center widget aligns its widget subtree to the center of the screen.
Step 2: Use an external package
In this step, you’ll start using an open-source package named english_words, which contains a few thousand of the most used English words plus some utility functions.
You can find the english_words package, as well as many other open source packages, on pub.dartlang.org.
-
The pubspec file manages the assets for a Flutter app. In pubspec.yaml, add english_words (3.1.0 or higher) to the dependencies list. The new line is highlighted below:
dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.0 english_words: ^3.1.0
-
While viewing the pubspec in Android Studio’s editor view, click Packages get upper right. This pulls the package into your project. You should see the following in the console:
flutter packages get Running "flutter packages get" in startup_namer... Process finished with exit code 0
-
In lib/main.dart, add the import for
english_words
, as shown in the highlighted line:import 'package:flutter/material.dart'; import 'package:english_words/english_words.dart';
As you type, Android Studio gives you suggestions for libraries to import. It then renders the import string in gray, letting you know that the imported library is unused (so far).
-
Use the English words package to generate the text instead of using the string “Hello World”.
Make the following changes, as highlighted below:
import 'package:flutter/material.dart'; import 'package:english_words/english_words.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); return new MaterialApp( title: 'Welcome to Flutter', home: new Scaffold( appBar: new AppBar( title: new Text('Welcome to Flutter'), ), body: new Center( //child: new Text('Hello World'), // Replace the highlighted text... child: new Text(wordPair.asPascalCase), // With this highlighted text. ), ), ); } }
-
If the app is running, use the hot reload button () to update the running app. Each time you click hot reload, or save the project, you should see a different word pair, chosen at random, in the running app. This is because the word pairing is generated inside the build method, which is run each time the MaterialApp requires rendering, or when toggling the Platform in Flutter Inspector.
Problems?
If your app is not running correctly, look for typos. If needed, use the code at the following links to get back on track.
- pubspec.yaml (The pubspec.yaml file won’t change again.)
- lib/main.dart
Step 3: Add a Stateful widget
Stateless widgets are immutable, meaning that their properties can’t change—all values are final.
Stateful widgets maintain state that might change during the lifetime of the widget. Implementing a stateful widget requires at least two classes: 1) a StatefulWidget class that creates an instance of 2) a State class. The StatefulWidget class is, itself, immutable, but the State class persists over the lifetime of the widget.
In this step, you’ll add a stateful widget, RandomWords, which creates its State class, RandomWordsState. The State class will eventually maintain the proposed and favorite word pairs for the widget.
-
Add the stateful RandomWords widget to main.dart. It can go anywhere in the file, outside of MyApp, but the solution places it at the bottom of the file. The RandomWords widget does little else besides creating its State class:
class RandomWords extends StatefulWidget { @override createState() => new RandomWordsState(); }
-
Add the RandomWordsState class. Most of the app’s code resides in this class, which maintains the state for the RandomWords widget. This class will save the generated word pairs, which grow infinitely as the user scrolls, and also favorite word pairs, as the user adds or removes them from the list by toggling the heart icon.
You’ll build this class bit by bit. To begin, create a minimal class by adding the highlighted text:
class RandomWordsState extends State<RandomWords> { }
-
After adding the state class, the IDE complains that the class is missing a build method. Next, you’ll add a basic build method that generates the word pairs by moving the word generation code from MyApp to RandomWordsState.
Add the build method to RandomWordState, as shown by the highlighted text:
class RandomWordsState extends State<RandomWords> { @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); return new Text(wordPair.asPascalCase); } }
-
Remove the word generation code from MyApp by making the highlighted changes below:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); // Delete this line return new MaterialApp( title: 'Welcome to Flutter', home: new Scaffold( appBar: new AppBar( title: new Text('Welcome to Flutter'), ), body: new Center( //child: new Text(wordPair.asPascalCase), // Change the highlighted text to... child: new RandomWords(), // ... this highlighted text ), ), ); } }
Restart the app. If you try to hot reload, you might see a warning:
Reloading...
Not all changed program elements ran during view reassembly; consider
restarting.
It may be a false positive, but consider restarting in order to make sure that your changes are reflected in the app’s UI.
The app should behave as before, displaying a word pairing each time you hot reload or save the app.
Problems?
If your app is not running correctly, you can use the code at the following link to get back on track.
Step 4: Create an infinite scrolling ListView
In this step, you’ll expand RandomWordsState to generate
and display a list of word pairings. As the user scrolls, the list
displayed in a ListView widget, grows infinitely. ListView’s
builder
factory constructor allows you to build a list view
lazily, on demand.
-
Add a
_suggestions
list to the RandomWordsState class for saving suggested word pairings. The variable begins with an underscore (_
)—prefixing an identifier with an underscore enforces privacy in the Dart language.Also, add a
biggerFont
variable for making the font size larger.class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _biggerFont = const TextStyle(fontSize: 18.0); ... }
-
Add a
_buildSuggestions()
function to the RandomWordsState class. This method builds the ListView that displays the suggested word pairing.The ListView class provides a builder property,
itemBuilder
, a factory builder and callback function specified as an anonymous function. Two parameters are passed to the function—the BuildContext, and the row iterator,i
. The iterator begins at 0 and increments each time the function is called, once for every suggested word pairing. This model allows the suggested list to grow infinitely as the user scrolls.Add the highlighted lines below:
class RandomWordsState extends State<RandomWords> { ... Widget _buildSuggestions() { return new ListView.builder( padding: const EdgeInsets.all(16.0), // The itemBuilder callback is called once per suggested word pairing, // and places each suggestion into a ListTile row. // For even rows, the function adds a ListTile row for the word pairing. // For odd rows, the function adds a Divider widget to visually // separate the entries. Note that the divider may be difficult // to see on smaller devices. itemBuilder: (context, i) { // Add a one-pixel-high divider widget before each row in theListView. if (i.isOdd) return new Divider(); // The syntax "i ~/ 2" divides i by 2 and returns an integer result. // For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2. // This calculates the actual number of word pairings in the ListView, // minus the divider widgets. final index = i ~/ 2; // If you've reached the end of the available word pairings... if (index >= _suggestions.length) { // ...then generate 10 more and add them to the suggestions list. _suggestions.addAll(generateWordPairs().take(10)); } return _buildRow(_suggestions[index]); } ); } }
-
The
_buildSuggestions
function calls_buildRow
once per word pair. This function displays each new pair in a ListTile, which allows you to make the rows more attractive in the next step.Add a
_buildRow
function to RandomWordsState:class RandomWordsState extends State<RandomWords> { ... Widget _buildRow(WordPair pair) { return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), ); } }
-
Update the build method for RandomWordsState to use
_buildSuggestions()
, rather than directly calling the word generation library. Make the highlighted changes:class RandomWordsState extends State<RandomWords> { ... @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); // Delete these two lines. return new Text(wordPair.asPascalCase); return new Scaffold ( appBar: new AppBar( title: new Text('Startup Name Generator'), ), body: _buildSuggestions(), ); } ... }
-
Update the build method for MyApp. Remove the Scaffold and AppBar instances from MyApp. These will be managed by RandomWordsState, which makes it easier to change the name of the route in the app bar as the user navigates from one screen to another in the next step.
Replace the original method with the highlighted build method below:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Startup Name Generator', home: new RandomWords(), ); } }
Restart the app. You should see a list of word pairings. Scroll down as far as you want and you will continue to see new word pairings.
Problems?
If your app is not running correctly, you can use the code at the following link to get back on track.
Step 5: Add interactivity
In this step, you’ll add tappable heart icons to each row. When the user taps an entry in the list, toggling its “favorited” state, that word pairing is added or removed from a set of saved favorites.
-
Add a
_saved
Set to RandomWordsState. This Set stores the word pairings that the user favorited. Set is preferred to List because a properly implemented Set does not allow duplicate entries.class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = new Set<WordPair>(); final _biggerFont = const TextStyle(fontSize: 18.0); ... }
-
In the
_buildRow
function, add analreadySaved
check to ensure that a word pairing has not already been added to favorites.Widget _buildRow(WordPair pair) { final alreadySaved = _saved.contains(pair); ... }
-
Also in
_buildRow()
, add heart-shaped icons to the ListTiles to enable favoriting. Later, you’ll add the ability to interact with the heart icons.Add the highlighted lines below:
Widget _buildRow(WordPair pair) { final alreadySaved = _saved.contains(pair); return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), trailing: new Icon( alreadySaved ? Icons.favorite : Icons.favorite_border, color: alreadySaved ? Colors.red : null, ), ); }
-
Restart the app. You should now see open hearts on each row, but they are not yet interactive.
-
Make the hearts tappable in the
_buildRow
function. If a word entry has already been added to favorites, tapping it again removes it from favorites. When the heart has been tapped, the function callssetState()
to notify the framework that state has changed.Add the highlighted lines:
Widget _buildRow(WordPair pair) { final alreadySaved = _saved.contains(pair); return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), trailing: new Icon( alreadySaved ? Icons.favorite : Icons.favorite_border, color: alreadySaved ? Colors.red : null, ), onTap: () { setState(() { if (alreadySaved) { _saved.remove(pair); } else { _saved.add(pair); } }); }, ); }
Hot reload the app. You should be able to tap any row to favorite, or unfavorite, the entry. Note that tapping a row generates an implicit ink splash animation that emanates from the heart icon.
Problems?
If your app is not running correctly, you can use the code at the following link to get back on track.
Step 6: Navigate to a new screen
In this step, you’ll add a new screen (called a route in Flutter) that displays the favorites. You’ll learn how to navigate between the home route and the new route.
In Flutter, the Navigator manages a stack containing the app’s routes. Pushing a route onto the Navigator’s stack, updates the display to that route. Popping a route from the Navigator’s stack, returns the display to the previous route.
-
Add a list icon to the AppBar in the build method for RandomWordsState. When the user clicks the list icon, a new route that contains the favorites items is pushed to the Navigator, displaying the icon.
Add the icon and its corresponding action to the build method:
class RandomWordsState extends State<RandomWords> { ... @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('Startup Name Generator'), actions: <Widget>[ new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved), ], ), body: _buildSuggestions(), ); } ... }
-
Add a
_pushSaved()
function to the RandomWordsState class.class RandomWordsState extends State<RandomWords> { ... void _pushSaved() { } }
Hot reload the app. The list icon appears in the app bar. Tapping it does nothing yet, because the
_pushSaved
function is empty. -
When the user taps the list icon in the app bar, build a route and push it to the Navigator’s stack. This action changes the screen to display the new route.
The content for the new page is built in MaterialPageRoute’s
builder
property, in an anonymous function.Add the call to Navigator.push, as shown by the highlighted code, which pushes the route to the Navigator’s stack.
void _pushSaved() { Navigator.of(context).push( ); }
-
Add the MaterialPageRoute and its builder. For now, add the code that generates the ListTile rows. The
divideTiles()
method of ListTile adds horizontal spacing between each ListTile. Thedivided
variable holds the final rows, converted to a list by the convenience function,toList()
.void _pushSaved() { Navigator.of(context).push( new MaterialPageRoute( builder: (context) { final tiles = _saved.map( (pair) { return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), ); }, ); final divided = ListTile .divideTiles( context: context, tiles: tiles, ) .toList(); }, ), ); }
-
The builder property returns a Scaffold, containing the app bar for the new route, named “Saved Suggestions.” The body of the new route consists of a ListView containing the ListTiles rows; each row is separated by a divider.
Add the highlighted code below:
void _pushSaved() { Navigator.of(context).push( new MaterialPageRoute( builder: (context) { final tiles = _saved.map( (pair) { return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), ); }, ); final divided = ListTile .divideTiles( context: context, tiles: tiles, ) .toList(); return new Scaffold( appBar: new AppBar( title: new Text('Saved Suggestions'), ), body: new ListView(children: divided), ); }, ), ); }
-
Hot reload the app. Favorite some of the selections and tap the list icon in the app bar. The new route appears containing the favorites. Note that the Navigator adds a “Back” button to the app bar. You did not have to explicitly implement Navigator.pop. Tap the back button to return to the home route.
Problems?
If your app is not running correctly, you can use the code at the following link to get back on track.
Step 7: Change the UI using Themes
In this final step, you’ll play with the app’s theme. The theme controls the look and feel of your app. You can use the default theme, which is dependent on the physical device or emulator, or you can customize the theme to reflect your branding.
-
You can easily change an app’s theme by configuring the ThemeData class. Your app currently uses the default theme, but you’ll be changing the primary color to be white.
Change the app’s theme to white by adding the highlighted code to MyApp:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Startup Name Generator', theme: new ThemeData( primaryColor: Colors.white, ), home: new RandomWords(), ); } }
-
Hot reload the app. Notice that the entire background is white, even the app bar.
-
As an exercise for the reader, use ThemeData to change other aspects of the UI. The Colors class in the Material library provides many color constants you can play with, and hot reload makes experimenting with the UI quick and easy.
Problems?
If you’ve gotten off track, use the code from the following link to see the code for the final app.
Well done!
You’ve written an interactive Flutter app that runs on both iOS and Android. In this codelab, you’ve:
- Created a Flutter app from the ground up.
- Written Dart code.
- Leveraged an external, third party library.
- Used hot reload for a faster development cycle.
- Implemented a stateful widget, adding interactivity to your app.
- Created a lazily loaded, infinite scrolling list displayed with a ListView and ListTiles.
- Created a route and added logic for moving between the home route and the new route.
- Learned about changing the look of your app’s UI using Themes.
Next step