如果你熟悉面向对象编程的思想,这个 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 工程的根目录下,有 iosandroid 两个文件夹,这两个文件夹下放置各自平台下的配置文件。

配置 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 IdentifieriOS 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 的值,选择 Typestring,然后以 DanaValue 进行输入;
  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 iOSAPK file for Android 这两篇教程了解到如何对各个平台做应用打包的操作。

附录

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

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

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

评价一下

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

1 2 3 4 5