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?
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
- 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.
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:
- Launch DevTools from Android Studio or IntelliJ.
- Launch DevTools from VS Code.
- Launch DevTools from the command line.
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:
Log in to Firebase to authenticate using
firebase login
.
Initialize a Firebase project using
firebase init.
Use the following values:
- Which Firebase features? Hosting
- Project setup: Create a new project
- What project name? my-flutter-app (for example)
- 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? y
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:
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:
{
"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:
- 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
.
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:
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?
- Try one of the other Flutter codelabs.
- Learn more about the power of Dart Devtools.
Continue learning about Flutter:
- flutter.dev: The documentation site for the Flutter project
- The Flutter Cookbook
- The Flutter API reference documentation
- Additional Flutter sample apps, with source code