Write Your First Flutter App, part 2

1. Introduction

Flutter is Google's UI toolkit for building beautiful, natively compiled apps for mobile, web, and desktop from a single codebase. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source.

In this codelab, you'll extend a basic, mobile Flutter app to include interactivity. You'll also create a second page (called a route) that the user can navigate to. Finally, you'll modify the app's theme (color). This codelab extends part 1, where you create an infinite lazily loaded list, but we'll provide the starting code, if you'd like to start with part 2.

What you'll learn in part 2

  • How to write a Flutter app that looks natural on iOS, Android, Windows, Linux, macOS, and the web
  • How to use hot reload for a quicker development cycle
  • How to add interactivity to a stateful widget
  • How to create and navigate to a second screen
  • How to change the look of an app using themes

What you'll build in part 2

You'll start with a simple app that generates an endless list of proposed names for a startup company. By the end of the codelab, your end users can select and unselect names, saving the best ones. Tapping the list icon in the upper right of the app bar navigates to a new page (called a route) that lists only the favorited names.

The following animated GIF shows how the finished app will work.

b17de15fa7831a1c.gif

What would you like to learn from this codelab?

I'm new to the topic, and I want a good overview. I know something about this topic, but I want a refresher. I'm looking for example code to use in my project. I'm looking for an explanation of something specific.

2. Set up your Flutter environment

If you haven't completed Part 1, see Set up your Flutter environment, in Write your first Flutter app, part 1, to set up your environment for Flutter development.

3. Get the starting app

If you have worked through part 1 of this codelab, you already have the starting app, startup_namer. You can proceed to the next step.

If you don't have startup_namer, no fear, you can get it using the following instructions.

a3c16fc17be25f6c.pngCreate a simple, templated Flutter app. Create a Flutter project called startup_namer as follows.

$ flutter create startup_namer
$ cd startup_namer

You'll mostly edit lib/main.dart, where the Dart code lives.

a3c16fc17be25f6c.png Delete all of the code from lib/main.dart. Replace it with the code from this file, which displays an infinite, lazily loaded list of proposed startup names.

a3c16fc17be25f6c.png Add the english_words package as a dependency of this app:

$ flutter pub add english_words
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
  characters 1.1.0 (1.2.0 available)
+ english_words 4.0.0
  matcher 0.12.10 (0.12.11 available)
  test_api 0.4.2 (0.4.5 available)
  vector_math 2.1.0 (2.1.1 available)
Changed 1 dependency!

The English words package generates pairs of random words, which are used as potential startup names.

a3c16fc17be25f6c.png Run the app.

Scroll as far as you want, viewing a continual supply of proposed startup names.

4. Add icons to the list

In this step, you'll add heart icons to each row. In the next step, you'll make them tappable and save the favorites.

a3c16fc17be25f6c.png 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 doesn't allow duplicate entries.

class _RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _saved = <WordPair>{};     // NEW
  final _biggerFont = TextStyle(fontSize: 18);
  ...
}

a3c16fc17be25f6c.png In the build function, add an alreadySaved check to ensure that a word pairing has not already been added to favorites.

final index = i ~/ 2; /*3*/
if (index >= _suggestions.length) {
  _suggestions.addAll(generateWordPairs().take(10));
}

final alreadySaved = _saved.contains(_suggestions[index]); // NEW

In the ListTile construction you'll add heart-shaped icons to the ListTile objects to enable favoriting. In the next step, you'll add the ability to interact with the heart icons.

a3c16fc17be25f6c.png Add the icons after the text, as shown below:

return ListTile(
  title: Text(
    _suggestions[index].asPascalCase,
    style: _biggerFont,
  ),
  trailing: Icon(    // NEW from here ...
    alreadySaved ? Icons.favorite : Icons.favorite_border,
    color: alreadySaved ? Colors.red : null,
    semanticLabel: alreadySaved ? 'Remove from saved' : 'Save',
  ),                 // to here.
);

a3c16fc17be25f6c.png Hot reload the app.

You should now see open hearts on each row, but they are not yet interactive.

Windows

iOS

Problems?

If your app isn't running correctly, you can use the code at the following link to get back on track.

5. Add interactivity

In this step, you'll make the heart icons tappable. 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.

To do that, you'll modify the _buildRow function. If a word entry has already been added to favorites, tapping it again removes it from favorites. When a tile has been tapped, the function calls setState() to notify the framework that state has changed.

a3c16fc17be25f6c.png Add onTap to the build method, as shown below:

return ListTile(
  title: Text(
    _suggestions[index].asPascalCase,
    style: _biggerFont,
  ),
  trailing: Icon(
    alreadySaved ? Icons.favorite : Icons.favorite_border,
    color: alreadySaved ? Colors.red : null,
    semanticLabel: alreadySaved ? 'Remove from saved' : 'Save',
  ),
  onTap: () {          // NEW from here ...
    setState(() {
      if (alreadySaved) {
        _saved.remove(_suggestions[index]);
      } else {
        _saved.add(_suggestions[index]);
      }
    });                // to here.
  },
);

a3c16fc17be25f6c.pngHot reload the app.

You should be able to tap any tile to favorite or unfavorite the entry. Tapping a tile generates an implicit ink splash animation emanating from the tap point.

Windows

iOS

Problems?

If your app isn't running correctly, you can use the code at the following link to get back on track.

6. Navigate to a new screen

In this step, you'll add a new page (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.

Next, you'll 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 saved favorites is pushed to the Navigator, displaying the icon.

a3c16fc17be25f6c.png Remove the Scaffold from the MyApp widget:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(          // MODIFY with const
      title: 'Startup Name Generator',
      home: RandomWords(),             // REMOVE Scaffold
    );
  }
}

The Scaffold is going to reappear in the _RandomWordsState. This will enable you to add a IconButton to the Scaffold's AppBar that interacts with the state of the application.

a3c16fc17be25f6c.png Wrap the ListView.builder with the Scaffold from MyApp, and add an IconButton to the AppBar's actions parameter, in the build method of the _RandomWordsState class:

class _RandomWordsState extends State<RandomWords> {
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(   // NEW from here ...
      appBar: AppBar(  
        title: const Text('Startup Name Generator'),
        actions: [
          IconButton(
            icon: const Icon(Icons.list),
            onPressed: _pushSaved,
            tooltip: 'Saved Suggestions',
          ),
        ],
      ),               
      body: ListView.builder(  // to here.
      ...
      ),                       // NEW
    );
  }
  ...
}

a3c16fc17be25f6c.pngAdd a _pushSaved() function to the _RandomWordsState class.

  void _pushSaved() {
  }

a3c16fc17be25f6c.png Hot reload the app. The list icon e401ba8d3a9e2379.png appears in the app bar. Tapping it does nothing yet because the _pushSaved function is empty.

Next, you'll build a route and push it to the Navigator's stack. That 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.

a3c16fc17be25f6c.png Call Navigator.push, as shown below, which pushes the route to the Navigator's stack. The IDE will complain about invalid code, but you will fix that in the next section.

void _pushSaved() {
  Navigator.of(context).push(
  );
}

As a quick aside, you may be wondering what the context argument of the Navigator.of call is. And we just happen to have a quick video explaining what the BuildContext is.

Next, you'll 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. The divided variable holds the final rows converted to a list by the convenience function, toList().

a3c16fc17be25f6c.pngAdd the code, as shown in the following code snippet:

  void _pushSaved() {
    Navigator.of(context).push(
      // Add lines from here...
      MaterialPageRoute<void>(
        builder: (context) {
          final tiles = _saved.map(
            (pair) {
              return ListTile(
                title: Text(
                  pair.asPascalCase,
                  style: _biggerFont,
                ),
              );
            },
          );
          final divided = tiles.isNotEmpty
              ? ListTile.divideTiles(
                  context: context,
                  tiles: tiles,
                ).toList()
              : <Widget>[];

          return Scaffold(
            appBar: AppBar(
              title: const Text('Saved Suggestions'),
            ),
            body: ListView(children: divided),
          );
        },
      ), // ...to here.
    );
  }

The builder property returns a Scaffold containing the app bar for the new route named SavedSuggestions. The body of the new route consists of a ListView containing the ListTiles rows. Each row is separated by a divider.

a3c16fc17be25f6c.png 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.

iOS - Main route

iOS - Saved suggestions route

Problems?

If your app isn't correctly running, then you can use the code at the following link to get back on track.

7. Change the UI using themes

In this step, you'll modify the app's theme. The theme controls the look and feel of your app. You can either use the default theme, which is dependent on the physical device or emulator, or customize the theme to reflect your branding.

You can easily change an app's theme by configuring the ThemeData class. The app uses the default theme, but you'll change the app's primary color to white.

a3c16fc17be25f6c.png Change the color in the MyApp class:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(          // Remove the const from here
      title: 'Startup Name Generator',
      theme: ThemeData(          // Add the 5 lines from here... 
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.white,
          foregroundColor: Colors.black,
        ),
      ),                         // ... to here.
      home: const RandomWords(), // And add the const back here.
    );
  }
}

a3c16fc17be25f6c.png Hot reload the app. The entire background is now white, even the app bar.

As an exercise, use ThemeData to change other aspects of the UI. The Colors class in the Material library provides many color constants that you can play with. Hot reload makes experimenting with the UI quick and easy.

Windows

iOS

Problems?

If you've gotten off track, then use the code from the following link to see the code for the final app.

8. Well done!

You wrote an interactive Flutter app that runs on iOS, Android, Windows, Linux, macOS, and web by doing the following:

  • Writing Dart code
  • Using hot reload for a faster development cycle
  • Implementing a stateful widget, adding interactivity to your app
  • Creating a route and adding logic for moving between the home route and the new route
  • Learning about changing the look of your app's UI using themes

9. Next steps

Now that you've written your first app, dive deeper into Flutter by creating a simple chat application in our Building beautiful UIs with Flutter codelab.

Learn more about the Flutter SDK from the following resources:

Other resources include:

Also, connect with the Flutter community!