1. 简介
在此 Codelab 中,您将构建一个多人计数器应用。您将学习如何将 Dart 用于 Flutter 前端和 Firebase 后端。
您还将了解如何在应用和服务器之间共享数据模型,从而无需重复编写逻辑。
学习内容
- 将共享业务逻辑提取到独立的 Dart 软件包中。
- 使用 Dart 原生编写和部署 Cloud Functions for Firebase。
- 利用 Dart 的预先 (AOT) 编译来减少无服务器冷启动。
- 使用 Firebase Emulator Suite 在本地测试您的堆栈。
2. 前提条件
- Flutter SDK(最新稳定版)。
- Firebase CLI(需要 v15.15.0 或更高版本)。
- 一个代码编辑器,例如 Antigravity、Visual Studio Code、IntelliJ 或 Android Studio,并已安装 Dart 和 Flutter 插件。
- 基本熟悉 Flutter 和 Firebase。
3. 为何使用 Dart 作为后端?
许多云应用使用 Dart 作为前端界面语言,并使用 TypeScript、Python 或 Go 等其他语言作为后端语言。这需要维护两组单独的数据模型。当数据库架构发生更改时,您必须更新这两个代码库。
注意:在后端使用 Dart 可让您将 Flutter 在客户端的响应式用户体验与服务器上的安全验证相结合,而无需重复编写代码。
4. 创建 Flutter 应用
创建标准 Flutter 应用:
flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run
在标准 Flutter 应用中,lib/main.dart 在本地处理计数器状态:
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
这种方法适用于本地状态,但无法扩展到服务器必须充当可靠数据源的多人游戏应用。为了支持多位玩家,我们将按照以下步骤将此逻辑移至后端。
5. 创建共享软件包
为避免在前端和后端重复创建模型,请在项目代码库中创建一个共享 Dart 软件包。Flutter 应用和 Firebase 函数都依赖于此软件包。
在 my_counter 项目的根目录下,运行以下命令:
mkdir -p packages
cd packages
dart create -t package shared
添加依赖项
在 packages/shared/pubspec.yaml 中,添加 JSON 序列化工具:
dependencies:
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.9
json_serializable: ^6.8.0
定义共享模型
创建 packages/shared/lib/src/models.dart。此文件定义了应用和服务器所使用的数据结构。
import 'package:json_annotation/json_annotation.dart';
part 'models.g.dart';
@JsonSerializable()
class IncrementResponse {
final bool success;
final String? message;
final int? newCount;
const IncrementResponse({required this.success, this.message, this.newCount});
factory IncrementResponse.fromJson(Map<String, dynamic> json) =>
_$IncrementResponseFromJson(json);
Map<String, dynamic> toJson() => _$IncrementResponseToJson(this);
}
// Store the function name as a constant to ensure consistency between client and server.
const incrementCallable = 'increment';
在 packages/shared/lib/shared.dart 中,导出模型:
library shared;
export 'src/models.dart';
在 packages/shared 目录中,运行 build runner 以生成 JSON 序列化代码:
dart run build_runner build
6. 设置 Cloud Functions for Firebase
Cloud Functions for Firebase 是一种无服务器框架,您可以利用它自动运行后端代码,而无需管理和扩展自己的服务器。Dart 非常适合,因为它会预先 (AOT) 编译为二进制文件,不需要像 Node.js 或 Java 那样庞大的运行时环境。这可显著缩短函数的冷启动时间。
前往项目根目录,然后初始化 Cloud Functions for Firebase:
cd ../..
firebase experiments:enable dartfunctions
firebase init functions
dart pub add google_cloud_firestore
- 当系统提示您选择语言时,请选择 Dart。
关联共享软件包
在 functions/pubspec.yaml 中,添加共享软件包的相对路径:
dependencies:
firebase_functions:
google_cloud_firestore:
shared:
path: ../packages/shared
7. 编写函数
如需编写后端逻辑,请打开 functions/bin/server.dart 并将内容替换为以下代码:
import 'dart:convert';
import 'package:firebase_functions/firebase_functions.dart';
import 'package:google_cloud_firestore/google_cloud_firestore.dart'
show FieldValue;
import 'package:shared/shared.dart';
void main(List<String> args) async {
await fireUp(args, (firebase) {
// Listen for calls to the http request and name defined in the shared package.
firebase.https.onRequest(name: incrementCallable, (request) async {
// In a production app, verify the user with request.auth?.uid here.
print('Incrementing counter on the server...');
// Get firestore database instance
final firestore = firebase.adminApp.firestore();
// Get a reference to the counter document
final counterDoc = firestore.collection('counters').doc('global');
// Get the current snapshot for the count data
final snapshot = await counterDoc.get();
// Increment response we will send back
IncrementResponse incrementResponse;
// Check for the current count and if the snapshot exists
if (snapshot.data() case {'count': int value} when snapshot.exists) {
if (request.method == 'GET') {
// Get the current result
incrementResponse = IncrementResponse(
success: true,
message: 'Read-only sync complete',
newCount: value,
);
} else if (request.method == 'POST') {
// Increment count by one
final step = request.url.queryParameters['step'] as int? ?? 1;
await counterDoc.update({'count': FieldValue.increment(step)});
incrementResponse = IncrementResponse(
success: true,
message: 'Atomic increment complete',
newCount: value + step,
);
} else {
throw FailedPreconditionError(
'only GET and POST requests are allowed',
);
}
} else {
// Create a new document with a count of 1
await counterDoc.set({'count': 1});
incrementResponse = const IncrementResponse(
success: true,
message: 'Cloud-sync complete',
newCount: 1,
);
}
// Return the response as JSON
return Response(
200,
body: jsonEncode(incrementResponse.toJson()),
headers: {'Content-Type': 'application/json'},
);
});
});
}
8. 使用 Firebase Emulator Suite 在本地进行测试
您可以在本地运行前端和后端,而无需进行部署。
从项目根目录中启动 Firebase Emulator Suite:
# Enable functions and firestore for the emulators
firebase init emulators
# Start the emulators and optionally open up the Admin UI
firebase emulators:start
关联共享软件包
在 pubspec.yaml 中,添加共享软件包的相对路径并添加 http 软件包:
dependencies:
http: ^1.6.0
shared:
path: /packages/shared
在 Flutter 项目中,打开 lib/main.dart 并将其内容替换为以下代码。此前端代码使用与后端相同的 IncrementResponse 类。
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shared/shared.dart';
/// Get from emulator output when running or when deploying:
/// ✔ functions[us-central1-increment]: http function initialized
/// (http://127.0.0.1:5001/demo-no-project/us-central1/increment).
const incrementUrl = 'FIREBASE_FUNCTIONS_URL_HERE';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue),
home: const CounterPage(),
);
}
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
bool _loading = false;
@override
void initState() {
super.initState();
// Fetch the current count
_increment(readOnly: true).ignore();
}
Future<void> _increment({bool readOnly = false}) async {
setState(() => _loading = true);
try {
// Call the Dart function.
final uri = Uri.parse(incrementUrl);
final response = readOnly ? await http.get(uri) : await http.post(uri);
// Parse the response back into the shared Dart object.
final responseData = jsonDecode(response.body);
final incrementResponse = IncrementResponse.fromJson(responseData);
if (incrementResponse.success) {
setState(() => _count = incrementResponse.newCount ?? _count);
}
} catch (e) {
print("Error calling function: $e");
} finally {
setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Multiplayer Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_count',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _loading ? null : _increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
运行 Flutter 应用。当您点击悬浮操作按钮时,应用会调用本地 Dart 后端,检索新计数并更新界面。
9. 部署到 Firebase
在此 Codelab 中,您能够使用 Firebase Local Emulator Suite 在没有 Firebase 项目或结算账号的情况下试用函数。如果您想在实际环境(例如生产环境)中使用函数,则需要设置 Firebase 项目和结算。
创建 Firebase 项目
- 使用您的 Google 账号登录 Firebase 控制台。
- 点击相应按钮以创建新项目,然后输入项目名称。
- 点击继续。
- 如果看到相关提示,请查看并接受 Firebase 条款,然后点击继续。
- (可选)在 Firebase 控制台中启用 AI 辅助功能(称为“Gemini in Firebase”)。
- 在此 Codelab 中,您不需要使用 Google Analytics,因此请关闭 Google Analytics 选项。
- 点击创建项目,等待项目完成预配,然后点击继续。
升级您的 Firebase 定价方案
如需使用本 Codelab 中的 Firebase 服务,您的 Firebase 项目必须采用随用随付 (Blaze) 定价方案,这意味着该项目与一个 Cloud Billing 账号相关联。
- Cloud Billing 账号要求提供付款方式,例如信用卡。
- 在特殊促销活动期间,或者如果您是在活动中完成此 Codelab,则可能会有 Google Cloud 赠金可用。
- 如果您刚开始接触 Firebase 和 Google Cloud,请确认您是否有资格获得 $300 赠金和免费试用 Cloud Billing 账号。
如需将项目升级到 Blaze 方案,请按以下步骤操作:
- 在 Firebase 控制台中,选择升级您的方案。
- 选择 Blaze 方案。按照屏幕上的说明将 Cloud Billing 账号关联到您的项目。
- 如果您在此 Codelab 中使用 Google Cloud 赠金,则结算账号可能名为
Google Cloud Platform Trial Billing Account或My Billing Account。 - 如果您需要在此升级过程中创建 Cloud Billing 账号,则可能需要返回 Firebase 控制台中的升级流程以完成升级。
- 如果您在此 Codelab 中使用 Google Cloud 赠金,则结算账号可能名为
部署到 Firebase 项目
如需部署 Dart 后端,请使用 Firebase CLI 运行以下命令:
firebase use <PROJECT_ID>
firebase deploy --only functions
命令运行完毕后,复制该网址并替换我们之前添加的 Flutter 应用源代码中的 FIREBASE_FUNCTIONS_URL_HERE。
10. 问题排查
firebase: command not found
确保已安装 Firebase CLI,并且 PATH 已更新。您可以使用 npm 安装它:npm install -g firebase-tools。
初始函数模板中缺少 Dart
为了让 Dart 显示为可用于部署的选项列表,并在运行 firebase init functions 时创建模板代码,需要通过运行 firebase experiments:enable dartfunctions 来设置实验标志。
函数模拟器未连接
验证您是否使用了 localhost 和端口 5001。如果您在 Android 模拟器上进行测试,设备不会将 localhost 解析为宿主机。更新 main.dart 中的模拟器配置以使用 10.0.2.2。
找不到共享的软件包
验证 functions/pubspec.yaml 中的相对路径。如果您的文件夹结构与 Codelab 不同,请调整 path: ../packages/shared 以指向正确的目录。
我是否需要使用 json_serializable?
虽然不是严格要求,但使用 json_serializable 可防止因手动编写 fromJson 和 toJson 方法而导致的错误。它可确保前端和后端预期的数据格式完全相同。
11. 恭喜
您已成功构建一个全栈 Dart 应用。通过在共享软件包中维护数据模型,您可以确保 API 响应和客户端界面保持同步,并在整个堆栈中使用单一编程语言。