如果你熟悉面向对象编程的思想,这个 Codelab 对你来说很简单。你不需要有 Dart、移动应用开发或者 Firebase 的使用经验,不过如果先完成关于 Flutter 的介绍的 Codelab 将对你有一些帮助。

Flutter 和 Firebase 携手助你以极快的时间来构建移动应用。Flutter 是 Google 推出的便携 UI 工具包,帮助你在 Android 和 iOS 平台开发原生体验的精美应用,而 Firebase 进一步让移动端应用具有访问后端服务的能力,包括鉴权、存储、数据库以及无服务器托管的服务。

在这个 Codelab 中,你将学习如果使用 Firebase 来创建 Flutter 应用。该应用可以让父母的亲戚朋友可以对他们喜欢的名字投票,然后父母根据投票结果来为他们的孩子起名字。这个应用使用 Cloud Firestore 作为数据库,每个用户的行为(比如标记一个名字的选项)都会以事务的形式更新数据库中的数据。

下图是这个应用 在 iOS 和 Android 上 最终显示的样子。哈哈,你没理解错,使用 Flutter 创建一个应用,就可以让这一套代码 同时在 iOS 和 Android 上 跑起来。

这是一个展示如何创建应用的小短片,它展示了你完成这个 Codelab 所需要做的几个步骤。

你的移动应用开发经验有多少?

从未接触过移动应用开发 只以 Web 页面构建过移动端 只写过 Android 应用 只写过 iOS 应用 iOS 和 Android 应用都写过 iOS、Android、Web 应用都写过

你需要安装两部分来完成本次实验,Flutter 的 SDK 和编辑器(editor),这个 Codelab 里,我们以 Android Studio 作为编辑器(editor),但你可以用个人更顺手的编辑器。

你可以通过如下任何设备完成本 codelab:

  1. 按照这篇教程来新建一个 Flutter 应用(也可能叫"新建项目"),并将命名由 myapp 改为 baby_names。创建新应用(项目)的名字或者方法可能会根据你选择的编辑器不同而略有差异;
  1. 确保你的应用并未在模拟器或者真机上运行;
  2. 在 IDE 或者编辑器中打开文件 pubspec.yaml,添加 cloud_firestore 依赖包并保存;
dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^0.8.2     # 新增了这一行
  1. 在 IDE 中(或者是在处于应用目录下的命令行中)运行 flutter package get 如果运行命令报错了,请确定 dependencies 的缩进同上述代码是否一致,要用两个空格而不是一个 tab。
  1. 以这篇文章作为教程,帮你设置在模拟器或者真机上运行应用。
    Flutter 构建一个应用大概需要一分钟时间,好消息是,这是你在这个 Codelab 中最后一次等待编译,其余所做的更改将以 stateful hot reload 的形式更新。
    当你的项目完成构建之后,你将会看到下面这样的界面:

  1. 使用 IDE 或者编辑器打开 lib/main.dart 文件,它包含了新建一个 Flutter 应用所包含的默认代码;
  2. 删除所有 main.dart 中的代码,然后输入下面的代码:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

final dummySnapshot = [
 {"name": "Filip", "votes": 15},
 {"name": "Abraham", "votes": 14},
 {"name": "Richard", "votes": 11},
 {"name": "Ike", "votes": 10},
 {"name": "Justin", "votes": 1},
];

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Baby Names',
     home: MyHomePage(),
   );
 }
}

class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() {
   return _MyHomePageState();
 }
}

class _MyHomePageState extends State<MyHomePage> {
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Baby Name Votes')),
     body: _buildBody(context),
   );
 }

 Widget _buildBody(BuildContext context) {
   // TODO: get actual snapshot from Cloud Firestore
   return _buildList(context, dummySnapshot);
 }

 Widget _buildList(BuildContext context, List<Map> snapshot) {
   return ListView(
     padding: const EdgeInsets.only(top: 20.0),
     children: snapshot.map((data) => _buildListItem(context, data)).toList(),
   );
 }

 Widget _buildListItem(BuildContext context, Map data) {
   final record = Record.fromMap(data);

   return Padding(
     key: ValueKey(record.name),
     padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
     child: Container(
       decoration: BoxDecoration(
         border: Border.all(color: Colors.grey),
         borderRadius: BorderRadius.circular(5.0),
       ),
       child: ListTile(
         title: Text(record.name),
         trailing: Text(record.votes.toString()),
         onTap: () => print(record),
       ),
     ),
   );
 }
}

class Record {
 final String name;
 final int votes;
 final DocumentReference reference;

 Record.fromMap(Map<String, dynamic> map, {this.reference})
     : assert(map['name'] != null),
       assert(map['votes'] != null),
       name = map['name'],
       votes = map['votes'];

 Record.fromSnapshot(DocumentSnapshot snapshot)
     : this.fromMap(snapshot.data, reference: snapshot.reference);

 @override
 String toString() => "Record<$name:$votes>";
}
  1. 保存文件,然后使用 hot-reload 更新应用:

你将会看到如下界面:


这个应用只是一个展示,点击其中某一项名字只会将其打印到输出框中。

下一步是将这个应用连接到 Cloud Firestore 上,在这之前,你可以看一下 main.dart 文件中的代码,了解一下它是如何进行组织的。

  1. 如果你有一个 Firebase 的账号了,直接登陆,如果你没有账号,你得先注册一个,对于这个 Codelab 来说选择免费模式完全足够了;
  2. 在 Firebase console 中,点击 Add project,新建一个 Firebase 项目;
  3. 如图所示,为你的 Firebase 项目中输入一个项目名(比如 "baby names app db"),然后设置 country/region 等,点击 Create Project;
  4. 等大概一分钟,你的 Firebase 项目就已经好了,然后点击 Continue。

在创建完 Firebase 项目后,如果需要关联多个应用使用这个 Firebase 项目,你只需要:

如果你创建的是跨 iOS 和 Android 平台的 Flutter 应用,你需要在同一个 Firebase 项目中分别注册 iOS 和 Android 版本,如果你只打算支持一个平台,只需要为那个平台进行配置文件的注册就可以了。

在 Flutter 工程的根目录下,有 ios 和 android 两个文件夹,这两个文件夹下放置各自平台下的配置文件。

配置 iOS

  1. 在 Firebase console 中,选择左边栏中的 Project Overview,然后在 "Get started by adding Firebase to your app" 下点击 iOS 按钮,你将看到如下图所示的对话框:

  1. 特别提醒的是 iOS bundle ID,你可以通过下面的步骤来获取该值;
  1. 在命令行工具中跳转到 Flutter 应用的根目录下;
  2. 输入命令 open ios/Runner.xcworkspace 来打开 Xocde;
  1. 在 Xcode 中,点击左边最顶层的 Runner,然后在右边选择 General 栏,如下图所示,然后复制 Bundle Identifier 中的值;

  1. 回到 Firebase 对话框中,粘贴 Bundle Identifier 到 iOS bundle ID 一栏中,然后点击 Register App;
  1. 在 Firebase 中按照里面的步骤来下载 GoogleService-Info.plist 文件;
  2. 回到 Xocde,在 Runner 下有个子目录也叫 Runner,如上图所示;
  3. 将 GoogleService-Info.plist (就是你刚刚下载的文件)文件拖到 Runner 的子目录下;
  4. 在 Xcode 弹出的对话框中,点击 Finish;
  5. 回到 Firebase console 中,点击 Next,然后跳过剩余的步骤,回到 Firebase console 的首页。

这样,你就完成了对于 iOS 平台下的 Flutter 应用的配置。

设置 Android

  1. 在 Firebase Console 中,选择左边栏中的 Project Overview,然后选择 "Get started by adding Firebase to your app" 的 Android 按钮。

你将会看到如下图所示的对话框:

  1. 这里需要重点看的是 Android package name,你将通过下面两个步骤获取到该值;
  1. 在 Flutter 项目目录中,打开文件 android/app/src/main/AndroidManifest.xml;
  2. 在 manifest 中,找到 package 属性中的值,它代表的是 Android 的包名(类似于 com.yourcompany.yourproject 这样的)复制这个值;
  3. 在 Firebase 对话框中,粘贴刚刚的包名到 Android package name 一栏中;
  4. (可选)如果你打算使用 Google Sign In 或者 Firebase Dynamic Links (这些不在本 Codelab 介绍范围内),你还要提供 Debug signing certificate SHA-1 的值,你可以通过 Authenticating Your Client 里面的方法来获取到 debug certificate fingerprint 的值,然后粘贴到那一栏里;
  5. 点击 Register App;
  6. 在 Firebase 中按照里面的步骤下载 google-services.json 文件;
  7. 回到 Flutter 应用目录,将 google-services.json(就是你刚刚下载的文件)放入到 android/app 目录中;
  8. 会打破 Firebase console 中,跳过剩下的步骤回到首页;
  9. 最后,你还需要通过 Google Services Gradle 插件来读取 google-services.json 文件;
  10. 在 IDE 或者编辑器中,打开 android/app/build.gradle 文件,然后将下列这一行粘贴到文本中:
apply plugin: 'com.google.gms.google-services'
  1. 打开 android/build.gradle 文件,然后在里面的 buildscript 标签下,新增一个依赖:
buildscript {
   repositories {
       // ...
   }

   dependencies {
       // ...
       classpath 'com.google.gms:google-services:3.2.1'   // new
   }
}

至此,你就完成了对于 Android 平台下的 Flutter 应用的配置。

FlutterFire 插件

现在你的 Flutter 工程应该可以连接并使用 Firebase 了。

Flutter 以及插件帮助你完成同一个功能在不同平台(Android 和 iOS)的代码实现。

传统的方式是通过调用不同的 Flutter 产品的 API 库来使用 Firebase(比如数据库、授权、分析、存储)。Flutter 官方团队维护了一个名为 FlutterFire 的插件库(Packages),你可以通过这一系列库在你的 Flutter 应用里使用 Firebase 服务。你可以在 FlutterFire Github 页面中进行查看。

创建 Cloud Firestore 数据库

Firebase-Flutter 已经设置完成,接下来可以写应用功能了。

我们将会从初始化 Cloud Firestore 并赋予其一些初始值开始。

  1. 打开 Firebase console,然后选择你之前创建的 Firebase 项目;
  2. 在左侧的 Develop 一栏中选择 Database:
  3. 在 Cloud Firestore 中选择 Create database;
  4. 在 Security rules for Cloud Firestore 对话框中,选择 Start in test mode,然后选择 Enable;

数据库将会有一个数据集命名为 "baby",该数据集中还会存储应用中使用到的 name 和 vote。

  1. 点击 Add Collection,设置名字为 baby,然后点击 Next。
    现在你可以在数据集中新增文件了,每个文件都有 Document ID,同时我们还需要增加 name 和 votes 字段(如下图所示);
  2. 输入名字,全部小写,在这个例子中我们使用 danas;
  1. 对已有的 Field 一栏中,输入 name 的值,选择 Type 为 string,然后以 Dana 为 Value 进行输入;
  2. 点击 Add Field 图标,创建第二个输入框,用来输入 votes 的值,将 Type 设置为 number 类型,然后设置初始值为 0;
  3. 点击 Save;
  4. 点击 Add Document 来增加更多的名字。

在数据集中增加多个文件之后,你的数据库看起来跟下图类似:

我们的应用现在已经成功连接 Cloud Firestore 了,是时候来获取数据集(baby),并用它来替换我们使用的 dummySnapshot 了。

在 Dart 中,你需要通过调用 Firestore.instance 来获取到对 Cloud Firestore 的引用,通过调用 Firestore.instance.collection('baby').snapshots() 返回一个 snapshot stream 来进一步获取到想要的数据集。

通过 SteamBuilder widget 来将请求的数据显示到 Flutter 界面中。

  1. 在 IDE 或者编辑器中,打开 lib/main.dart 文件,找到 _buildBody 方法。
  2. 用下列代码替换整个方法:
Widget _buildBody(BuildContext context) {
 return StreamBuilder<QuerySnapshot>(
   stream: Firestore.instance.collection('baby').snapshots(),
   builder: (context, snapshot) {
     if (!snapshot.hasData) return LinearProgressIndicator();

     return _buildList(context, snapshot.data.documents);
   },
 );
}
  1. 你刚刚复制粘贴的代码有一个类型错误���它尝试将一个 DocumentSnapshot 列表传递给一个方法,但是这个方法并不期望拿到这个值。找到 _buildList 然后将其更换为下列代码:
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
  ... 

现在,它获取到了一个包含 DocumentSnapshot 类型的列表,而不是 Map。

  1. 快结束了哈,_buildListItem 方法仍然认为它拿到的是 Map 类型,在这个方法的开头更改如下所示代码:
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
 final record = Record.fromSnapshot(data);

更改之后,获取到的值就是一个 DocumentSnapshot 类型,而不是 Map,然后使用 Record.fromSnapshot() 构���器来创建 Record;

  1. (可选)在 lib/main.dart 文件的顶部移除 dummySnapshot,它不再被需要了;
  2. 保存文件,然后使用 hot-reload 更新 app:

大概一秒之后,你的应用界面就变成了下面这个样子:

你现在直接读取到了刚刚创建的数据库数据。

如果你想要的话,你可以到 Firebase console 中去更新数据库数据。你的应用将会对你所做的更改即时刷新(毕竟 Cloud Firestore 是一个实时数据库)。

接下来我们将加入投票功能~

  1. 在 lib/main.dart 文件中,找到 onTap: () => print(record). 这一行,将其更改为如下代码:
onTap: () => record.reference.updateData({'votes': record.votes + 1})

这行代码表示通过数据库引用来逐步递增投票数;

  1. 保存文件,然后使用 hot-reload 更新 app;

投票功能以及界面更新目前已经搞定了。

它是如何工作的呢?当一个用户点击列表中的某一项时,应用会通过对该项的数据引用来告诉 Cloud Firestore 更新数据。相应地,这也会让 Cloud Firestore 来通知别的监听者来去更新他们的值。当应用通过 StreamBuilder 来进行监听时,就会实时对新的数据进行更新。

只在一台设备上运行你可能会很难发现,不过咱们目前但代码是可能会出现"资源竞态(Race Condition)"的问题的。假设两个用户在完全精确相同的时间对同一个名字进行投票,虽然某个名字得到了两票,但是实际上 votes 字段中的值也只会加 1。这是因为两台设备的应用会同时读取当前 votes 的最新值,然后对其加 1,再将同样的值赋值到数据库中。对于那两个用户来说,他们不会觉得出了什么问题,因为他们都对 votes 做了加 1 的操作。测试时是很难对这个问题进行复现的,因为需要在一个非常小的时间窗口内完成对这个错误的触发。

votes 这个值是一个共享的资源,任何情况下对一个共享的资源做更新操作(尤其这个更新还依赖于它当前的值)都会存在"资源竞态"的问题。所以,当你对数据库中的值做更新操作时,你应该使用事务对它进行更新。

  1. 在 lib/main.dart 文件中,找到 onTap: () => record.reference.updateData({'votes': record.votes + 1}) 一行,替换为下列代码:
onTap: () => Firestore.instance.runTransaction((transaction) async {
     final freshSnapshot = await transaction.get(record.reference);
     final fresh = Record.fromSnapshot(freshSnapshot);

     await transaction
         .update(record.reference, {'votes': fresh.votes + 1});
   }),
  1. 保存文件,然后使用 hot-reload 更新 app。

投票的交互代码现在变得有些复杂,因为需要避免"资源竞态"的出现。

这如何工作的呢?通过将"读操作"和"写操作"放到一个事务中去,也就相当于你告诉 Cloud Firestore 在事务进行过程中,当且仅当没有别的数据更新操作发生时,才对事务做提交操作。如果有两个用户对同一个名字进行投票,但这两个用户的行为并不是同时发生的,那么每次都提交这个事务。但是如果在 transaction.get(...) 和 transaction.update(...) 调用之间发生了 votes 数据的更改,那么当前的事务并不会被提交,而是进行重试,如果重试 5 次还是失败,那么这个事务就会失败。

这是 lib/main.dart 文件的最终版本:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Baby Names',
     home: MyHomePage(),
   );
 }
}

class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() {
   return _MyHomePageState();
 }
}

class _MyHomePageState extends State<MyHomePage> {
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Baby Name Votes')),
     body: _buildBody(context),
   );
 }

 Widget _buildBody(BuildContext context) {
   return StreamBuilder<QuerySnapshot>(
     stream: Firestore.instance.collection('baby').snapshots(),
     builder: (context, snapshot) {
       if (!snapshot.hasData) return LinearProgressIndicator();

       return _buildList(context, snapshot.data.documents);
     },
   );
 }

 Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
   return ListView(
     padding: const EdgeInsets.only(top: 20.0),
     children: snapshot.map((data) => _buildListItem(context, data)).toList(),
   );
 }

 Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
   final record = Record.fromSnapshot(data);

   return Padding(
     key: ValueKey(record.name),
     padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
     child: Container(
       decoration: BoxDecoration(
         border: Border.all(color: Colors.grey),
         borderRadius: BorderRadius.circular(5.0),
       ),
       child: ListTile(
         title: Text(record.name),
         trailing: Text(record.votes.toString()),
         onTap: () => Firestore.instance.runTransaction((transaction) async {
               final freshSnapshot = await transaction.get(record.reference);
               final fresh = Record.fromSnapshot(freshSnapshot);

               await transaction
                   .update(record.reference, {'votes': fresh.votes + 1});
             }),
       ),
     ),
   );
 }
}

class Record {
 final String name;
 final int votes;
 final DocumentReference reference;

 Record.fromMap(Map<String, dynamic> map, {this.reference})
     : assert(map['name'] != null),
       assert(map['votes'] != null),
       name = map['name'],
       votes = map['votes'];

 Record.fromSnapshot(DocumentSnapshot snapshot)
     : this.fromMap(snapshot.data, reference: snapshot.reference);

 @override
 String toString() => "Record<$name:$votes>";
}

恭喜!

(理论上)你应该学会如何在 Flutter 应用中使用 Firebase 啦,恭喜恭喜!

新技能 get 清单

想对你的朋友分享这个应用吗?你可以通过 IPA file for iOS 和 APK file for Android 这两篇教程了解到如何对各个平台做应用打包的操作。

附录

想了解更多 Firebase 的信息,可以来这里:

如果你继续学习 Flutter framework 相关的知识,下列资源应该会帮到你:

以下是有关获取 Flutter 最新资讯的方法:

评价一下

你有多大意愿将 Flutter 推荐给身边的人?(1 代表非常不愿意,5 代表非常愿意)

1 2 3 4 5