Using a plugin with a Flutter web app

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

  • How to use a Flutter plugin in a web app
  • The difference between a package and a plugin
  • How to debug a web app using Dart DevTools
  • How to host an app on Firebase

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.

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

b2f84ff91b0e1396.pngCreate a simple, templated Flutter app.

Create a Flutter project called star_counter and migrate to null safety as follows.

$ flutter create star_counter
$ cd star_counter
$ dart migrate --apply-changes

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

pubspec.yaml

name: star_counter
description: A GitHub Star Counter app
version: 1.0.0+1
environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  flutter_markdown: ^0.6.0
  github: ^8.0.0
  intl: ^0.17.0

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

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

b2f84ff91b0e1396.png 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:

lib/main.dart

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,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

b2f84ff91b0e1396.pngRun 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:

97cb2368f34eb03c.png

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.

b2f84ff91b0e1396.pngCreate a new file in lib called star_counter.dart:

lib/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
  late 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;
    });

    if (widget.repositoryName.isNotEmpty) {
      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.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,
    );
  }
}

cf1e10b838bf60ee.png Observations

  • The star counter uses the github Dart package to query GitHub for the number of stars a repo earned.
  • You can find packages and plugins on pub.dev.
  • You can also browse and search packages for a particular platform. If you select FLUTTER from the landing page, then on the next page, select WEB. This brings up all of the packages that run on the web. Either browse through the pages of packages, or use the search bar to narrow your results.
  • The Flutter community contributes packages and plugins to pub.dev. If you look at the page for the github package, you'll see that it works for pretty much any Dart or Flutter app, including WEB.
  • You might pay particular attention to packages that are marked as Flutter Favorites. The Flutter Favorites program identifies packages that meet specific criteria, such as feature completeness and good runtime behavior.
  • Later, you add a plugin from pub.dev to this example.

b2f84ff91b0e1396.pngAdd the following import to main.dart:

lib/main.dart

import 'star_counter.dart';

b2f84ff91b0e1396.png Use the new GitHubStarCounter widget.

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

lib/main.dart

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

b2f84ff91b0e1396.pngRun 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 293160db29e53878.png 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:

78a5f531b1acfd58.png

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.

b2f84ff91b0e1396.png 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:

b2f84ff91b0e1396.png 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 3d8c8053deda4caa.png, to bring up the debugger.

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

b2f84ff91b0e1396.png 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:

eeec16d42e7012ba.png

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

b2f84ff91b0e1396.png Catch the error.

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

if (widget.repositoryName.isNotEmpty) {
  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:

lib/star_counter.dart

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

b2f84ff91b0e1396.png 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:

f1b3847ee101a85b.png

f5077295022a18df.pngYou'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.

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

lib/privacy_policy.dart

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.
''';

b2f84ff91b0e1396.pngAdd the following import to main.dart:

lib/main.dart

import 'privacy_policy.dart';

b2f84ff91b0e1396.pngAdd a new route (page) for the privacy policy.

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

lib/main.dart

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

b2f84ff91b0e1396.pngAdd a button to display the privacy policy.

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

lib/main.dart

TextButton(
  style: ButtonStyle(
    foregroundColor: MaterialStateProperty.all(Colors.blue),
    overlayColor: MaterialStateProperty.all(Colors.transparent),
  ),
  onPressed: () => Navigator.of(context).pushNamed('/privacypolicy'),
  child: Text('Privacy Policy'),
),

b2f84ff91b0e1396.pngRun the app.

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

ae990c7f6e0918e5.png

b2f84ff91b0e1396.png Click the Privacy Policy button.

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

c233a1dea9abfaec.png

b2f84ff91b0e1396.pngGo 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:

b2f84ff91b0e1396.png Install the Firebase CLI.

b2f84ff91b0e1396.png Log in to Firebase to authenticate using firebase login.

b2f84ff91b0e1396.png Initialize a Firebase project using firebase init.

Use the following values:

  • Which Firebase features? Hosting
  • Project setup: Create a new project
  • What project name? [yourname]-my-flutter-app (must be unique)
  • What to call your project? Press Return to accept the default (which is the same as the name used in the previous question).
  • What public directory? build/web (This is important.)
  • Configure as a single page app? Yes
  • Set up automatic builds and deploys with GitHub? No

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

55135b9eda3c41ef.png

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

  • firebase.json, the configuration file
  • .firebaserc, containing your project data

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

firebase.json

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

b2f84ff91b0e1396.pngBuild a release version of your app.

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

  • In Android Studio or IntelliJ, specify --release in the Additional arguments field of the Run > Edit Configuration dialog. Then, run your app.
  • At the command line, run flutter build web --release.

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.

b2f84ff91b0e1396.pngDeploy 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.

b2f84ff91b0e1396.pngDelete privacy_policy.dart.

Remove the file from the lib directory of your project.

b2f84ff91b0e1396.pngUpdate main.dart.

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

b2f84ff91b0e1396.pngAdd privacy_policy.html.

Place this file in the web directory of your project.

web/privacy_policy.html

<!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.

b2f84ff91b0e1396.pngAdd 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):

pubspec.yaml

  url_launcher: ^6.0.0

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

b2f84ff91b0e1396.pngAdd the following import to main.dart:

lib/main.dart

import 'package:url_launcher/url_launcher.dart';

b2f84ff91b0e1396.pngUpdate the TextButton'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 71) uses Flutter's normal routing mechanism:

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

The new code for onPressed calls the url_launcher package:

lib/main.dart

onPressed: () => launch(
  '/privacy_policy.html',
  enableJavaScript: true,
  enableDomStorage: true,
),

b2f84ff91b0e1396.pngRun 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.

After your finish with your project, remember to clean up:

b2f84ff91b0e1396.png 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: