如果你熟悉面向对象编程的思想,这个 Codelab 对你来说很简单。你不需要有 Dart、移动应用开发或者 Firebase 的使用经验,不过如果先完成关于 Flutter 的介绍的 Codelab 将对你有一些帮助。
Flutter 和 Firebase 携手助你以极快的时间来构建移动应用。Flutter 是 Google 推出的便携 UI 工具包,帮助你在 Android 和 iOS 平台开发原生体验的精美应用,而 Firebase 进一步让移动端应用具有访问后端服务的能力,包括鉴权、存储、数据库以及无服务器托管的服务。
在这个 Codelab 中,你将学习如果使用 Firebase 来创建 Flutter 应用。该应用可以让父母的亲戚朋友可以对他们喜欢的名字投票,然后父母根据投票结果来为他们的孩子起名字。这个应用使用 Cloud Firestore 作为数据库,每个用户的行为(比如标记一个名字的选项)都会以事务的形式更新数据库中的数据。
下图是这个应用 在 iOS 和 Android 上 最终显示的样子。哈哈,你没理解错,使用 Flutter 创建一个应用,就可以让这一套代码 同时在 iOS 和 Android 上 跑起来。
这是一个展示如何创建应用的小短片,它展示了你完成这个 Codelab 所需要做的几个步骤。
你需要安装两部分来完成本次实验,Flutter 的 SDK 和编辑器(editor),这个 Codelab 里,我们以 Android Studio 作为编辑器(editor),但你可以用个人更顺手的编辑器。
你可以通过如下任何设备完成本 codelab:
myapp
改为 baby_names
。创建新应用(项目)的名字或者方法可能会根据你选择的编辑器不同而略有差异;pubspec.yaml
,添加 cloud_firestore
依赖包并保存;dependencies:
flutter:
sdk: flutter
cloud_firestore: ^0.8.2 # 新增了这一行
flutter package get
如果运行命令报错了,请确定 dependencies
的缩进同上述代码是否一致,要用两个空格而不是一个 tab。lib/main.dart
文件,它包含了新建一个 Flutter 应用所包含的默认代码;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>";
}
flutter run
的路径一致。你将会看到如下界面:
这个应用只是一个展示,点击其中某一项名字只会将其打印到输出框中。
下一步是将这个应用连接到 Cloud Firestore 上,在这之前,你可以看一下 main.dart
文件中的代码,了解一下它是如何进行组织的。
在创建完 Firebase 项目后,如果需要关联多个应用使用这个 Firebase 项目,你只需要:
如果你创建的是跨 iOS 和 Android 平台的 Flutter 应用,你需要在同一个 Firebase 项目中分别注册 iOS 和 Android 版本,如果你只打算支持一个平台,只需要为那个平台进行配置文件的注册就可以了。
在 Flutter 工程的根目录下,有 ios
和 android
两个文件夹,这两个文件夹下放置各自平台下的配置文件。
配置 iOS |
open ios/Runner.xcworkspace
来打开 Xocde;GoogleService-Info.plist
文件;GoogleService-Info.plist
(就是你刚刚下载的文件)文件拖到 Runner 的子目录下;这样,你就完成了对于 iOS 平台下的 Flutter 应用的配置。
设置 Android |
你将会看到如下图所示的对话框:
android/app/src/main/AndroidManifest.xml
;manifest
中,找到 package
属性中的值,它代表的是 Android 的包名(类似于 com.yourcompany.yourproject
这样的)复制这个值;google-services.json
文件;google-services.json
(就是你刚刚下载的文件)放入到 android/app
目录中;google-services.json
文件;android/app/build.gradle
文件,然后将下列这一行粘贴到文本中:apply plugin: 'com.google.gms.google-services'
android/build.gradle
文件,然后在里面的 buildscript
标签下,新增一个依赖:buildscript {
repositories {
// ...
}
dependencies {
// ...
classpath 'com.google.gms:google-services:3.2.1' // new
}
}
至此,你就完成了对于 Android 平台下的 Flutter 应用的配置。
现在你的 Flutter 工程应该可以连接并使用 Firebase 了。
Flutter 以及插件帮助你完成同一个功能在不同平台(Android 和 iOS)的代码实现。
传统的方式是通过调用不同的 Flutter 产品的 API 库来使用 Firebase(比如数据库、授权、分析、存储)。Flutter 官方团队维护了一个名为 FlutterFire 的插件库(Packages),你可以通过这一系列库在你的 Flutter 应用里使用 Firebase 服务。你可以在 FlutterFire Github 页面中进行查看。
创建 Cloud Firestore 数据库
Firebase-Flutter 已经设置完成,接下来可以写应用功能了。
我们将会从初始化 Cloud Firestore 并赋予其一些初始值开始。
数据库将会有一个数据集命名为 "baby",该数据集中还会存储应用中使用到的 name 和 vote。
baby
,然后点击 Next。danas
;name
的值,选择 Type 为 string
,然后以 Dana
为 Value 进行输入;votes
的值,将 Type 设置为 number
类型,然后设置初始值为 0;在数据集中增加多个文件之后,你的数据库看起来跟下图类似:
我们的应用现在已经成功连接 Cloud Firestore 了,是时候来获取数据集(baby
),并用它来替换我们使用的 dummySnapshot
了。
在 Dart 中,你需要通过调用 Firestore.instance
来获取到对 Cloud Firestore 的引用,通过调用 Firestore.instance.collection('baby').snapshots() 返回一个 snapshot
stream
来进一步获取到想要的数据集。
通过 SteamBuilder
widget 来将请求的数据显示到 Flutter 界面中。
lib/main.dart
文件,找到 _buildBody
方法。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);
},
);
}
�
��它尝试将一个 DocumentSnapshot
列表传递给一个方法,但是这个方法并不期望拿到这个值。找到 _buildList
然后将其更换为下列代码:Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
...
现在,它获取到了一个包含 DocumentSnapshot
类型的列表,而不是 Map
。
_buildListItem
方法仍然认为它拿到的是 Map
类型,在这个方法的开头更改如下所示代码:Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot(data);
更改之后,获取到的值就是一个 DocumentSnapshot
类型,而不是 Map
,然后使用 Record.fromSnapshot() 构��
�器来创建 Record;
lib/main.dart
文件的顶部移除 dummySnapshot
,它不再被需要了;flutter run
的路径一致。大概一秒之后,你的应用界面就变成了下面这个样子:
你现在直接读取到了刚刚创建的数据库数据。
如果你想要的话,你可以到 Firebase console 中去更新数据库数据。你的应用将会对你所做的更改即时刷新(毕竟 Cloud Firestore 是一个实时数据库)。
接下来我们将加入投票功能~
onTap: () => print(record).
这一行,将其更改为如下代码:onTap: () => record.reference.updateData({'votes': record.votes + 1})
这行代码表示通过数据库引用来逐步递增投票数;
投票功能以及界面更新目前已经搞定了。
它是如何工作的呢?当一个用户点击列表中的某一项时,应用会通过对该项的数据引用来告诉 Cloud Firestore 更新数据。相应地,这也会让 Cloud Firestore 来通知别的监听者来去更新他们的值。当应用通过 StreamBuilder
来进行监听时,就会实时对新的数据进行更新。
只在一台设备上运行你可能会很难发现,不过咱们目前但代码是可能会出现"资源竞态(Race Condition)"的问题的。假设两个用户在完全精确相同的时间对同一个名字进行投票,虽然某个名字得到了两票,但是实际上 votes 字段中的值也只会加 1。这是因为两台设备的应用会同时读取当前 votes 的最新值,然后对其加 1,再将同样的值赋值到数据库中。对于那两个用户来说,他们不会觉得出了什么问题,因为他们都对 votes 做了加 1 的操作。测试时是很难对这个问题进行复现的,因为需要在一个非常小的时间窗口内完成对这个错误的触发。
votes 这个值是一个共享的资源,任何情况下对一个共享的资源做更新操作(尤其这个更新还依赖于它当前的值)都会存在"资源竞态"的问题。所以,当你对数据库中的值做更新操作时,你应该使用事务对它进行更新。
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});
}),
投票的交互代码现在变得有些复杂,因为需要避免"资源竞态"的出现。
这如何工作的呢?通过将"读操作"和"写操作"放到一个事务中去,也就相当于你告诉 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 啦,恭喜恭喜!
想对你的朋友分享这个应用吗?你可以通过 IPA file for iOS 和 APK file for Android 这两篇教程了解到如何对各个平台做应用打包的操作。
想了解更多 Firebase 的信息,可以来这里:
如果你继续学习 Flutter framework 相关的知识,下列资源应该会帮到你:
以下是有关获取 Flutter 最新资讯的方法: