Flutter is Google's UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. In this codelab, you'll finish an app that reports the number of stars on a GitHub repository. You'll use Dart DevTools to do some simple debugging. You'll learn how to host your app on Firebase. Finally, you'll use a Flutter plugin to launch the app and open the hosted privacy policy.

What you'll learn

Prerequisites: This codelab assumes that you have some basic Flutter knowledge. If you are new to Flutter, you might want to first start with Write your first Flutter app on the web.

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.

A plugin (also called a plugin package) is a specialized Dart package that contains an API written in Dart code combined with one or more platform-specific implementations. Plugin packages can be written for Android (using Kotlin or Java), iOS (using Swift or Objective-C), web (using Dart), macOS (using Dart), or any combination thereof. (In fact, Flutter supports federated plugins, which allow support for different platforms to be split across packages.)

A package is a Dart library that you can use to extend or simplify your app's functionality. As previously mentioned, a plugin is a type of a package. For more information about packages and plugins, see Flutter Plugin or Dart Package?

You need three pieces of software to complete this lab: the Flutter SDK, an editor, and the Chrome browser. You can use your preferred editor, such as Android Studio or IntelliJ with the Flutter and Dart plugins installed, or Visual Studio Code with the Dart Code and Flutter extensions. You will debug your code using Dart DevTools on Chrome.

If you also want to run this app on a mobile device, you need to do additional setup and configuration.

Enable web support

Web support is in beta, so you must opt in. To manually enable web support, use the following instructions. In a terminal, run these commands:

$ flutter channel beta
$ flutter upgrade
$ flutter config --enable-web

You only need to run the config command once. After enabling web support, every Flutter app you create also compiles for the web. In your IDE (under the devices pulldown), or at the command line using flutter devices, you should now see Chrome and Web server listed. Selecting Chrome automatically starts Chrome when you launch your app. Selecting Web server starts a server that hosts the app so that you can load it from any browser. Use Chrome during development so that you can use Dart DevTools, and use the web server when you want to test your app on other browsers.

For this codelab, we provide much of the starting code so that you can quickly get to the interesting bits.

Create a simple, templated Flutter app.

Use the instructions in Create your first Flutter app. Name the project star_counter (instead of myapp).

Update the pubspec.yaml file.
Update the pubspec.yaml file at the top of the project:

name: star_counter
description: A GitHub Star Counter app
version: 1.0.0+1
environment:
  sdk: ">=2.1.0 <3.0.0"
dependencies:
  flutter:
    sdk: flutter
  flutter_markdown: ^0.3.0
  github: ^6.1.0
  intl: ^0.16.0
flutter:
  uses-material-design: true

Fetch the updated dependencies.
Click the Pub get button in your IDE or, at the command line, run flutter pub get from the top of the project.

Replace the contents of lib/main.dart.
Delete all of the code from lib/main.dart, which creates a Material-themed, count-the-number-of-button-presses app. Add the following code, which sets up a not-yet-complete, count-the-number-of-stars-on-a-GitHub-repo app:

import 'package:flutter/material.dart';

void main() {
  runApp(StarCounterApp());
}

class StarCounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        brightness: Brightness.light,
      ),
      routes: {
        '/': (context) => HomePage(),
      },
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String _repositoryName = "";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ConstrainedBox(
          constraints: BoxConstraints(maxWidth: 400),
          child: Card(
            child: Padding(
              padding: EdgeInsets.all(16.0),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Text(
                    'GitHub Star Counter',
                    style: Theme.of(context).textTheme.headline4,
                  ),
                  TextField(
                    decoration: InputDecoration(
                      labelText: 'Enter a GitHub repository',
                      hintText: 'flutter/flutter',
                    ),
                    onSubmitted: (text) {
                      setState(() {
                        _repositoryName = text;
                      });
                    },
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 32.0),
                    child: Text(
                      _repositoryName,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Run the app.
Run the app on Chrome. If you're using an IDE, then first select Chrome from the device pulldown. If you're using the command line, then from the top of the package, run flutter run -d chrome. (If flutter devices shows that you have web configured but no other connected devices, then the flutter run command defaults to Chrome.)

Chrome launches, and you should see something like the following:

Enter some text into the text field followed by pressing Return. The text you typed is displayed at the bottom of the window.

Next, instead of displaying the text that was entered in the form, "google/flutter.widgets", you modify the app to show the number of stars for that repo.

Create a new file in lib called star_counter.dart:

import 'package:flutter/material.dart';
import 'package:github/github.dart';
import 'package:intl/intl.dart' as intl;

class GitHubStarCounter extends StatefulWidget {
  /// The full repository name, e.g. torvalds/linux
  final String repositoryName;

  GitHubStarCounter({
    @required this.repositoryName,
  });

  @override
  _GitHubStarCounterState createState() => _GitHubStarCounterState();
}

class _GitHubStarCounterState extends State<GitHubStarCounter> {
  // The GitHub API client
  GitHub github;

  // The repository information
  Repository repository;

  // A human-readable error when the repository isn't found.
  String errorMessage;

  void initState() {
    super.initState();
    github = GitHub();

    fetchRepository();
  }

  void didUpdateWidget(GitHubStarCounter oldWidget) {
    super.didUpdateWidget(oldWidget);

    // When this widget's [repositoryName] changes, 
    // load the Repository information.
    if (widget.repositoryName == oldWidget.repositoryName) {
      return;
    }

    fetchRepository();
  }

  Future<void> fetchRepository() async {
    setState(() {
      repository = null;
      errorMessage = null;
    });

    var repo = await github.repositories
        .getRepository(RepositorySlug.full(widget.repositoryName));
    setState(() {
      repository = repo;
    });
  }

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    final textStyle = textTheme.headline4.apply(color: Colors.green);
    final errorStyle = textTheme.bodyText1.apply(color: Colors.red);
    final numberFormat = intl.NumberFormat.decimalPattern();

    if (errorMessage != null) {
      return Text(errorMessage, style: errorStyle);
    }

    if (widget.repositoryName != null &&
        widget.repositoryName.isNotEmpty &&
        repository == null) {
      return Text('loading...');
    }

    if (repository == null) {
      // If no repository is entered, return an empty widget.
      return SizedBox();
    }

    return Text(
      '${numberFormat.format(repository.stargazersCount)}',
      style: textStyle,
    );
  }
}

Observations

Add the following import to main.dart:

import 'star_counter.dart';

Use the new GitHubStarCounter widget.

In main.dart, replace the Text widget (lines 60-62) with the 3 new lines that define the GitHubStarCounterWidget:

Padding(
  padding: const EdgeInsets.only(top: 32.0),
  child: GitHubStarCounter(              // New
    repositoryName: _repositoryName,     // New
  ),                                     // New
),

Run the app.

Hot restart the app by clicking the Run button again in the IDE (without first stopping the app), clicking the hot restart button in the IDE , or by typing r in the console. This updates the app without refreshing the browser.

The window looks the same as before. Enter an existing repo, such as the one suggested: flutter/flutter. The number of stars is reported below the text field, for example:

Are you ready for a debugging exercise? In the running app, enter a non-existent repo, such as foo/bar. The widget is stuck saying "Loading...". You fix that now.

Launch Dart DevTools.

You may be familiar with Chrome DevTools, but to debug a Flutter app, you'll want to use Dart DevTools. Dart DevTools was designed to debug and profile Dart and Flutter apps. There are a number of ways to launch Dart DevTools, depending on your workflow. The following pages have instructions about how to install and launch DevTools:

Bring up the debugger.

The initial browser page you see when Dart DevTools launches can be different, depending on how it was launched. Click the Debugger tab , to bring up the debugger.

Bring up the star_counter.dart source code.

In the Libraries text field, in the lower left, enter star_counter" Double-click the package:star_counter/star_counter.dart entry from the results list, to open it in the File view.

Set a breakpoint.

Find the following line in the source: var repo = await github.repositories. It should be on line 52. Click to the left of the line number, and a circle appears, indicating that you set a breakpoint. The breakpoint also appears in the Breakpoints list on the left. On the upper right, select the Break on exceptions checkbox. The UI should look like the following:

Run the app.

Enter a non-existent repository and press Return. In the error pane, below the code pane, you'll see that the github package threw a "repository not found" exception:

Error: GitHub Error: Repository Not Found: /
    at Object.throw_ [as throw] (http://localhost:52956/dart_sdk.js:4463:11)
    at http://localhost:52956/packages/github/src/common/xplat_common.dart.lib.js:1351:25
    at github.GitHub.new.request (http://localhost:52956/packages/github/src/common/xplat_common.dart.lib.js:10679:13)
    at request.next (<anonymous>)
    at http://localhost:52956/dart_sdk.js:37175:33
    at _RootZone.runUnary (http://localhost:52956/dart_sdk.js:37029:58)
    at _FutureListener.thenAwait.handleValue (http://localhost:52956/dart_sdk.js:32116:29)
    at handleValueCallback (http://localhost:52956/dart_sdk.js:32663:49)
    at Function._propagateToListeners (http://localhost:52956/dart_sdk.js:32701:17)
    at _Future.new.[_completeWithValue] (http://localhost:52956/dart_sdk.js:32544:23)
    at async._AsyncCallbackEntry.new.callback (http://localhost:52956/dart_sdk.js:32566:35)
    at Object._microtaskLoop (http://localhost:52956/dart_sdk.js:37290:13)
    at _startMicrotaskLoop (http://localhost:52956/dart_sdk.js:37296:13)
    at http://localhost:52956/dart_sdk.js:32918:9

Catch the error.

In star_counter.dart, find the following code (lines 52-56):

      var repo = await github.repositories
          .getRepository(RepositorySlug.full(widget.repositoryName));
      setState(() {
        repository = repo;
      });

Replace that code with code that uses a try-catch block, to behave more gracefully by catching the error and printing a message:

try {
  var repo = await github.repositories
      .getRepository(RepositorySlug.full(widget.repositoryName));
  setState(() {
    repository = repo;
  });
} on RepositoryNotFound {
  setState(() {
    repository = null;
    errorMessage = '${widget.repositoryName} not found.';
  });
}

Hot restart the app.

In DevTools, the source code is updated to reflect the changes. Once again, enter a non-existent repo. You should see the following:


You've found something special!

In this step you'll add a privacy policy page to your app. At first, you will embed the privacy policy text in your Dart code.

Add a lib/privacy_policy.dart file.
In the lib directory, add a privacy_policy.dart file to your project:

import 'package:flutter/widgets.dart';
import 'package:flutter_markdown/flutter_markdown.dart';

class PrivacyPolicy extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Markdown(
      data: _privacyPolicyText,
    );
  }
}

// The source for this privacy policy was generated by
// https://app-privacy-policy-generator.firebaseapp.com/
var _privacyPolicyText = '''
## Privacy Policy

Flutter Example Company built the Star Counter app as an Open Source app. This SERVICE is provided by Flutter Example Company at no cost and is intended for use as is.

This page is used to inform visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our Service.

If you choose to use our Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that we collect is used for providing and improving the Service. We will not use or share your information with anyone except as described in this Privacy Policy.

The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Star Counter unless otherwise defined in this Privacy Policy.
''';

Add the following import to main.dart:

import 'privacy_policy.dart';

Add a new route (page) for the privacy policy.

After line 17, add the route for the privacy policy page:

routes: {
  '/': (context) => HomePage(),
  '/privacypolicy': (context) => PrivacyPolicy(),  // NEW
},

Add a button to display the privacy policy.

In the _HomePageState's build() method, add a FlatButton to the bottom of the Column, after line 65:

FlatButton(
  color: Colors.transparent,
  textColor: Colors.blue,
  onPressed: () => Navigator.of(context).pushNamed('/privacypolicy'),
  child: Text('Privacy Policy'),
),

Run the app.

Hot restart the app. It now has a Privacy Policy link at the bottom of the screen:

Click the Privacy Policy button.

Note that the privacy policy displays, and the URL changes to /privacypolicy.

Go back.

Use the browser's Back button to return to the first page. You get this behavior for free.

The advantage of a hosted page is that you can change that page, without releasing a new version of your app.

From the command line, at the root of the project, use the following instructions:

Install the Firebase CLI.

Log in to Firebase to authenticate using firebase login.

Initialize a Firebase project using firebase init.

Use the following values:

At the command line, you'll see something like the following after you finish running firebase init:

At the completion of the init command, the following files are added to your project:

Make sure that the public field in your firebase.json specifies build/web, for example:

{
  "hosting": {
    "public": "build/web",    # This is important!
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

Build a release version of your app.

Configure your IDE to build a release version of your app using one of the following approaches:

Confirm that this step worked by examining the build/web directory of your project. The directory should contain a number of files, including index.html.

Deploy your app.

At the command line, run firebase deploy from the top of the project to deploy the contents of the public build/web directory. This shows the URL where it's hosted, https://project-id>.web.app.

In the browser, go to https://<project-id>.web.app or https://<project-id>.web.app/#/privacypolicy to see the running version of your privacy policy.

Next, instead of embedding the privacy policy in the Dart code, you'll host it as an HTML page using Firebase.

Delete privacy_policy.dart.

Remove the file from the lib directory of your project.

Update main.dart.

In lib/main.dart, remove the import statement import privacy_policy.dart and the PrivacyPolicyPage widget at the bottom.

Add privacy_policy.html.

Place this file in the web directory of your project.

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>Privacy Policy</title>
</head>

<body>
<h2 id="privacy-policy">Privacy Policy</h2>
<p>Flutter Example Company built the Star Counter app as an Open Source app. This SERVICE is provided by Flutter Example Company at no cost and is intended for use as is.</p>
<p>This page is used to inform visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our Service.</p>
<p>If you choose to use our Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that we collect is used for providing and improving the Service. We will not use or share your information with anyone except as described in this Privacy Policy.</p>
<p>The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Star Counter unless otherwise defined in this Privacy Policy.</p>
</body>
</html>

Next, you use the url_launcher plugin to open the privacy policy in a new tab.

Flutter web apps are Single Page Apps (SPAs). This is why using the standard routing mechanism in our web example opens the privacy policy in the same web page. The URL launcher plugin, on the other hand, opens a new tab in the browser, launches another copy of the app, and routes the app to the hosted page.

Add a dependency.

In the pubspec.yaml file, add the following dependency (and remember, white space matters in a YAML file, so make sure that the line starts with two blank spaces):

  url_launcher: ^5.4.0

Fetch the new dependency.

Stop the app, because adding a dependency requires a full restart of the app. Click the Pub get button in your IDE or, at the command line, run flutter pub get from the top of the project.

Add the following import to main.dart:

import 'package:url_launcher/url_launcher.dart';

Update the FlatButton's handler.

Also in main.dart, replace the code that is called when the user presses the Privacy Policy button. The original code (on line 70) uses Flutter's normal routing mechanism:

onPressed: () => Navigator.of(context).pushNamed('/privacypolicy'),

The new code for onPressed calls the url_launcher package:

onPressed: () => launch(
  '/#/privacypolicy',
  enableJavaScript: true,
  enableDomStorage: true,
),

Run the app.

Click the Privacy Policy button to open the file in a new tab. The primary advantage of using the url_launcher package is that (after the privacy policy page is hosted), it works on web and mobile platforms. An additional benefit is that you can modify the hosted privacy policy page without recompiling your app.

For fun, you might want to launch an Android emulator, the iOS simulator, or a connected mobile device to test whether your app (and the device policy) works there too. After your finish with your project, remember to clean up:

Delete your firebase project.

Congratulations, you successfully completed the GitHub Star Counter app! You also got a small taste of Dart DevTools, which you can use to debug and profile all Dart and Flutter apps, not just web apps.

What's next?

Continue learning about Flutter: