在 Flutter Web 应用中使用插件

Flutter 是 Google 的界面工具包,它让开发者可以使用同一个代码库分别为移动设备、网页和桌面设备构建精致的原生编译应用。在此 Codelab 中,您将完成一个为 GitHub 代码库报告星数的应用。您将使用 Dart DevTools 执行一些简单的调试。您将学习如何在 Firebase 上托管您的应用。最后,您将使用 Flutter 插件启动应用和打开托管的隐私权政策。

学习内容

  • 如何在 Web 应用中使用 Flutter 插件
  • 软件包和插件之间的区别
  • 如何使用 Dart DevTools 调试 Web 应用
  • 如何在 Firebase 上托管应用

前提条件:此 Codelab 假定您具备一些 Flutter 基础知识。如果您是 Flutter 新手,不妨先在网页上编写您的第一个 Flutter 应用

您想通过此 Codelab 学习什么?

我不熟悉这个主题,想深入了解一下。 我对这个主题有所了解,但我想重温一下。 我在寻找示例代码以用到我的项目中。 我在寻找有关特定内容的说明。

插件(也称为“插件软件包”)是一个专用 Dart 软件包,其中包含一个以 Dart 代码编写的 API 以及一个或多个针对特定平台的实现。您可以针对 Android(使用 Kotlin 或 Java)、iOS(使用 Swift 或 Objective-C)、web(使用 Dart)、macOS(使用 Dart)编写插件软件包,也可以针对任意平台组合编写(事实上,Flutter 支持联合插件,这样软件包的不同版本便可分别支持不同的平台)。

软件包是一个 Dart 库,可用于扩展或简化应用的功能。如前所述,插件是软件包的一种类型。如需详细了解软件包和插件,请参阅 Flutter 插件或 Dart 软件包

您需要使用 3 款软件来完成这个实验:Flutter SDK一款编辑器Chrome 浏览器。您可以使用自己偏好的编辑器,例如 Android Studio 或 IntelliJ(已安装 Flutter 和 Dart 插件),或者 Visual Studio Code(包含 Dart Code 和 Flutter 扩展程序)。您将使用 Chrome 上的 Dart DevTools 来调试您的代码。

对于此 Codelab,我们提供了很多起始代码,以便您可以快速进入感兴趣的部分。

b2f84ff91b0e1396.png创建简单的模板化 Flutter 应用。

创建一个名为 star_counter 的 Flutter 项目并迁移到 null 安全,如下所示。

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

b2f84ff91b0e1396.png 更新 pubspec.yaml 文件。更新项目顶部的 pubspec.yaml 文件:

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 获取更新后的依赖项。点击 IDE 中的 Pub get 按钮,或在命令行中,从项目顶部运行 flutter pub get

b2f84ff91b0e1396.png 替换 lib/main.dart 的内容。删除 lib/main.dart 中的所有代码,这些代码创建的是一个具有 Material 主题、且会统计按钮按下次数的应用。然后添加以下代码,以创建一个尚未完成的、为 GitHub 代码库统计星数的应用:

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.png运行应用。在 Chrome 上运行该应用。如果您使用的是 IDE,请先从设备下拉菜单中选择 Chrome。如果您使用的是命令行,则从软件包顶部运行 flutter run -d chrome(如果 flutter devices 显示您已配置网络但没有其他已连接的设备,则 flutter run 命令默认为 Chrome)。

Chrome 会启动,然后您应该会看到如下页面:

97cb2368f34eb03c.png

在文本字段中输入一些文字,然后按回车键。您输入的文字会显示在窗口底部。

接下来,您可以修改该应用,让其显示该代码库的星数,而不是显示以“google/flutter.widgets”格式输入的文本。

b2f84ff91b0e1396.pnglib 中,新建一个名为 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 观察结果

  • 该星数计数器会使用 github Dart 软件包在 GitHub 中查询代码库获得的星数。
  • 您可以在 pub.dev 上找到软件包和插件。
  • 您还可以浏览和搜索针对特定平台的软件包。如果您从着陆页中选择 FLUTTER,那么在下一页中则选择 WEB。系统会显示网页上运行的所有软件包。您可以浏览软件包的页面,也可以使用搜索栏缩小结果范围。
  • Flutter 社区致力于为 pub.dev 提供软件包和插件。当您查看 github 软件包的页面时,会发现它适用于几乎所有 Dart 或 Flutter 应用,包括 web应用。
  • 您可能会特别关注标记为 Flutter Favorites 的软件包。Flutter Favorites 程序可识别符合特定条件(例如功能完整性和良好的运行时行为)的软件包。
  • 稍后,您可以将 pub.dev 中的插件添加到此示例中。

b2f84ff91b0e1396.png将下面的 import 添加到 main.dart

lib/main.dart

import 'star_counter.dart';

b2f84ff91b0e1396.png 使用新的 GitHubStarCounter 微件。

main.dart 中,将 Text 微件(第 60-62 行)替换为用以定义 GitHubStarCounterWidget 的 3 个新行:

lib/main.dart

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

b2f84ff91b0e1396.png运行应用。

热重启该应用,方法是在 IDE 中再次点击运行按钮(不要先停止应用),在 IDE 中点击热重启按钮 293160db29e53878.png,或在 IDE 中输入 r。此操作会更新应用,但不会刷新浏览器。

该窗口的外观与以前相同。输入一个现有代码库,例如建议的代码库:flutter/flutter。文本字段下方会报告星数,例如:

78a5f531b1acfd58.png

准备好进行调试练习了吗?在正在运行的应用中,输入一个不存在的代码库,例如 foo/bar。该微件会卡住,一直显示“正在加载…”。您现在就可以解决这个问题。

b2f84ff91b0e1396.png 启动 Dart DevTools。

您可能比较熟悉 Chrome DevTools,但是要调试 Flutter 应用,您需要使用 Dart DevTools。Dart DevTools 专用于调试和剖析 Dart 和 Flutter 应用。您可以通过多种方式启动 Dart DevTools,具体取决于您的工作流程。以下页面提供了有关如何安装和启动 DevTools 的说明:

b2f84ff91b0e1396.png 调出调试程序。

Dart DevTools 启动时,您看到的初始浏览页面可能会有所不同,具体取决于其启动方式。点击 Debugger 标签页 3d8c8053deda4caa.png,调出调试程序。

b2f84ff91b0e1396.png 调出 star_counter.dart 源代码。

Libraries 文本字段的左下方,输入 star_counter。双击结果列表中的 package:star_counter/star_counter.dart 条目,在“File”视图中打开该文件。

b2f84ff91b0e1396.png 设置断点。

在源代码中找到以下行:var repo = await github.repositories。它应该在第 52 行。点击行号的左侧,此时会出现一个圆圈,表示您设置了断点。断点也会显示在左侧的 Breakpoints 列表中。选中右上角的 Break on exceptions 复选框。界面应如下所示:

eeec16d42e7012ba.png

b2f84ff91b0e1396.png 运行应用。

输入一个不存在的代码库,然后按回车键。在错误窗格的代码窗格下方,您会看到 github 软件包抛出“repository not found”异常:

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 捕获错误。

star_counter.dart 中,找到以下代码(第 52-58 行):

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

将该代码替换为使用 try-catch 块的代码,以便更妥当地捕获错误和输出消息:

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 热重启应用。

在 DevTools 中,系统会更新源代码以反映更改。再次输入一个不存在的代码库。您应该会看到以下内容:

f1b3847ee101a85b.png

f5077295022a18df.png您发现了一些特别内容!

在此步骤中,您将向应用添加隐私权政策页面。首先,您需要在 Dart 代码中嵌入隐私权政策文本。

b2f84ff91b0e1396.png 添加一个 lib/privacy_policy.dart 文件。在 lib 目录中,将一个 privacy_policy.dart 文件添加到您的项目中:

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.png将下面的 import 添加到 main.dart

lib/main.dart

import 'privacy_policy.dart';

b2f84ff91b0e1396.png为隐私权政策添加新的路由(页面)。

在第 17 行之后,为隐私权政策页面添加路由:

lib/main.dart

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

b2f84ff91b0e1396.png添加用以显示隐私权政策的按钮。

_HomePageStatebuild() 方法中,将 TextButton 添加到 Column 的底部,即第 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.png运行应用。

热重启应用。现在,屏幕底部会显示隐私权政策链接:

ae990c7f6e0918e5.png

b2f84ff91b0e1396.png点击隐私权政策按钮。

请注意,隐私权政策显示时,网址会变成 /privacypolicy

c233a1dea9abfaec.png

b2f84ff91b0e1396.png返回。

使用浏览器的返回按钮返回到首页。您可以免费获取此行为。

托管页面的优点是,您可以在不发布应用的新版本的情况下更改该页面。

在项目根目录下的命令行中,按照以下说明操作:

b2f84ff91b0e1396.png 安装 Firebase CLI

b2f84ff91b0e1396.png 使用 firebase login 登录 Firebase 以进行身份验证

b2f84ff91b0e1396.png 使用 firebase init. 初始化 Firebase 项目

请使用以下值:

  • 哪些 Firebase 功能?托管
  • 项目设置:创建新项目
  • 项目名称是什么?[yourname]-my-flutter-app(必须是唯一的)
  • 如何调用您的项目?按回车键可接受默认值(与上一个问题中使用的名称相同)。
  • 公共目录是什么?build/web(这很重要)。
  • 是否配置为单页应用?
  • 是否设置使用 GitHub 自动构建和部署?

在命令行中,运行 firebase init 后,您会看到如下内容:

55135b9eda3c41ef.png

完成 init 命令后,以下文件会添加到您的项目中:

  • firebase.json,配置文件
  • .firebaserc,其中包含您的项目数据

确保 firebase.json 中的 public 字段指定了 build/web,例如:

firebase.json

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

b2f84ff91b0e1396.png构建应用的发布版本。

配置 IDE,使用以下方式之一构建应用的发布版本:

  • 在 Android Studio 或 IntelliJ 中,在 Run > Edit Configuration 对话框的 Additional parameters 字段中指定 --release。然后,运行您的应用。
  • 在命令行中运行 flutter build web --release

通过检查项目的 build/web 目录来确认此步骤是否有效。此目录应包含多个文件,其中包括 index.html

b2f84ff91b0e1396.png部署应用。

在命令行中,从项目顶部运行 firebase deploy,以部署公共 build/web 目录的内容。系统会显示托管该目录的网址,https://project-id>.web.app.

在浏览器中,转到 https://<project-id>.web.apphttps://<project-id>.web.app/#/privacypolicy,查看当前所使用的隐私权政策版本。

接下来,您将使用 Firebase 将隐私权政策托管为 HTML 页面,而不是将其嵌入 Dart 代码。

b2f84ff91b0e1396.png删除 privacy_policy.dart.

从项目的 lib 目录中移除该文件。

b2f84ff91b0e1396.png更新 main.dart.

lib/main.dart 中,移除底部的导入语句 import privacy_policy.dartPrivacyPolicyPage 微件。

b2f84ff91b0e1396.png添加 privacy_policy.html.

将此文件放在项目的 web 目录中。

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>

接下来,您可以使用 url_launcher 插件在新标签页中打开隐私权政策。

Flutter Web 应用是单页应用 (SPA)。因此,当我们在网页示例中使用标准路由机制时,系统会在同一网页中打开隐私权政策。另一方面,网址启动器插件会在浏览器中打开一个新标签页,启动应用的另一个副本,并将应用路由到托管页面。

b2f84ff91b0e1396.png添加依赖项。

pubspec.yaml 文件中,添加以下依赖项(请注意,空格在 YAML 文件中很重要,因此,请确保该代码行以两个空格开头):

pubspec.yaml

  url_launcher: ^6.0.0

b2f84ff91b0e1396.png 获取新依赖项。

停止应用,因为添加依赖项需要完全重启应用。点击 IDE 中的 Pub get 按钮,或在命令行中,从项目顶部运行 flutter pub get

b2f84ff91b0e1396.png将下面的 import 添加到 main.dart

lib/main.dart

import 'package:url_launcher/url_launcher.dart';

b2f84ff91b0e1396.png更新 TextButton 的处理程序。

main.dart 中,替换用户按下隐私权政策按钮时调用的代码。原始代码(在第 71 行)使用 Flutter 的正常路由机制:

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

onPressed 的新代码会调用 url_launcher 软件包:

lib/main.dart

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

b2f84ff91b0e1396.png运行应用。

点击隐私权政策按钮,在新标签页中打开相应文件。使用 url_launcher 软件包的主要优势是,隐私权政策页面(当其托管以后)可以在网页和移动平台上正常运行。另一个好处是,您无需重新编译应用即可修改托管的隐私权政策页面。

项目完成后,请别忘了进行清理工作:

b2f84ff91b0e1396.png 删除您的 Firebase 项目

恭喜!您已成功完成 GitHub Star Counter 应用!您还大致了解了 Dart DevTools,您可以使用这些工具调试和剖析所有 Dart 和 Flutter 应用,而不仅仅是 Web 应用。

接下来做什么?

继续了解 Flutter: