1. Giới thiệu
Trong lớp học lập trình này, bạn sẽ tạo một ứng dụng bộ đếm nhiều người chơi. Bạn sẽ tìm hiểu cách sử dụng Dart cho cả giao diện người dùng Flutter và phần phụ trợ Firebase.
Bạn cũng sẽ tìm hiểu cách chia sẻ các mô hình dữ liệu giữa ứng dụng và máy chủ của mình, nhờ đó không cần phải sao chép logic.
Kiến thức bạn sẽ học được
- Trích xuất logic nghiệp vụ dùng chung vào một gói Dart độc lập.
- Viết và triển khai Cloud Functions cho Firebase một cách tự nhiên bằng Dart.
- Tận dụng tính năng biên dịch Trước khi thực thi (AOT) của Dart để giảm số lượt khởi động nguội không máy chủ.
- Kiểm thử ngăn xếp của bạn trên thiết bị bằng Bộ công cụ mô phỏng Firebase.
2. Điều kiện tiên quyết
- Flutter SDK (phiên bản ổn định mới nhất).
- Giao diện dòng lệnh (CLI) của Firebase (bắt buộc phải là phiên bản 15.15.0 trở lên).
- Một trình soạn thảo mã, chẳng hạn như Antigravity, Visual Studio Code, IntelliJ hoặc Android Studio, đã cài đặt các trình bổ trợ Dart và Flutter.
- Hiểu biết cơ bản về Flutter và Firebase.
3. Tại sao nên sử dụng Dart cho phần phụ trợ?
Nhiều ứng dụng đám mây sử dụng Dart cho giao diện người dùng và một ngôn ngữ khác, chẳng hạn như TypeScript, Python hoặc Go, cho phần phụ trợ. Điều này đòi hỏi bạn phải duy trì 2 tập hợp mô hình dữ liệu riêng biệt. Khi giản đồ cơ sở dữ liệu thay đổi, bạn phải cập nhật cả hai cơ sở mã.
Lưu ý: Việc sử dụng Dart trên phần phụ trợ cho phép bạn kết hợp trải nghiệm người dùng thích ứng của Flutter trên ứng dụng với quy trình xác thực an toàn trên máy chủ mà không cần sao chép mã.
4. Tạo ứng dụng Flutter
Tạo một ứng dụng Flutter tiêu chuẩn:
flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run
Trong một ứng dụng Flutter tiêu chuẩn, lib/main.dart xử lý trạng thái bộ đếm cục bộ:
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
Phương pháp này phù hợp với trạng thái cục bộ, nhưng không mở rộng được cho ứng dụng nhiều người chơi, trong đó máy chủ phải đóng vai trò là nguồn đáng tin cậy. Để hỗ trợ nhiều người chơi, chúng ta sẽ chuyển logic này sang phần phụ trợ trong các bước sau.
5. Tạo gói dùng chung
Để tránh trùng lặp các mô hình trên giao diện người dùng và phụ trợ, hãy tạo một gói Dart dùng chung bên trong kho lưu trữ dự án của bạn. Cả ứng dụng Flutter và các hàm cho Firebase đều phụ thuộc vào gói này.
Từ gốc của dự án my_counter, hãy chạy các lệnh sau:
mkdir -p packages
cd packages
dart create -t package shared
Thêm phần phụ thuộc
Trong packages/shared/pubspec.yaml, hãy thêm các công cụ chuyển đổi tuần tự JSON:
dependencies:
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.9
json_serializable: ^6.8.0
Xác định các mô hình được chia sẻ
Tạo packages/shared/lib/src/models.dart. Tệp này xác định cấu trúc dữ liệu mà cả ứng dụng và máy chủ đều sử dụng.
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';
Trong packages/shared/lib/shared.dart, hãy xuất các mô hình của bạn:
library shared;
export 'src/models.dart';
Trong thư mục packages/shared, hãy chạy trình chạy bản dựng để tạo mã chuyển đổi tuần tự JSON:
dart run build_runner build
6. Thiết lập Cloud Functions cho Firebase
Cloud Functions cho Firebase là một khung không máy chủ, cho phép bạn tự động chạy mã phụ trợ mà không cần quản lý và mở rộng quy mô máy chủ của riêng mình. Dart là một lựa chọn phù hợp vì nó biên dịch Trước thời hạn (AOT) thành một tệp nhị phân, không yêu cầu môi trường thời gian chạy nặng như Node.js hoặc Java. Điều này giúp giảm đáng kể thời gian khởi động nguội cho các hàm của bạn.
Chuyển đến thư mục gốc của dự án rồi khởi động Cloud Functions cho Firebase:
cd ../..
firebase experiments:enable dartfunctions
firebase init functions
dart pub add google_cloud_firestore
- Khi được nhắc chọn ngôn ngữ, hãy chọn Dart.
Liên kết gói dùng chung
Trong functions/pubspec.yaml, hãy thêm đường dẫn tương đối vào gói dùng chung:
dependencies:
firebase_functions:
google_cloud_firestore:
shared:
path: ../packages/shared
7. Viết hàm
Để viết logic phụ trợ, hãy mở functions/bin/server.dart rồi thay thế nội dung bằng đoạn mã sau:
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. Kiểm thử cục bộ bằng Bộ công cụ mô phỏng của Firebase
Bạn có thể chạy cả giao diện người dùng và phần phụ trợ cục bộ mà không cần triển khai.
Từ thư mục gốc của dự án, hãy khởi động Bộ công cụ mô phỏng Firebase:
# Enable functions and firestore for the emulators
firebase init emulators
# Start the emulators and optionally open up the Admin UI
firebase emulators:start
Liên kết gói dùng chung
Trong pubspec.yaml, hãy thêm một đường dẫn tương đối vào gói dùng chung và thêm gói http:
dependencies:
http: ^1.6.0
shared:
path: ../packages/shared
Trong dự án Flutter, hãy mở lib/main.dart rồi thay thế nội dung của tệp này bằng đoạn mã sau. Đoạn mã giao diện người dùng này sử dụng cùng một lớp IncrementResponse như phần phụ trợ.
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),
),
);
}
}
Chạy ứng dụng Flutter. Khi bạn nhấp vào nút hành động nổi, ứng dụng sẽ gọi phần phụ trợ Dart cục bộ, truy xuất số lượt truy cập mới và cập nhật giao diện người dùng.
9. Triển khai lên Firebase
Để triển khai phần phụ trợ Dart, hãy chạy lệnh sau:
firebase deploy --only functions
Sau khi chạy lệnh, hãy sao chép URL và thay thế FIREBASE_FUNCTIONS_URL_HERE trong mã nguồn ứng dụng Flutter mà chúng ta đã thêm trước đó.
10. Khắc phục sự cố
firebase: command not found
Đảm bảo bạn đã cài đặt Firebase CLI và PATH đã được cập nhật. Bạn có thể cài đặt bằng npm: npm install -g firebase-tools.
Thiếu Dart trong mẫu hàm init
Để Dart xuất hiện dưới dạng danh sách các lựa chọn triển khai và tạo mã mẫu khi chạy firebase init functions, bạn cần đặt cờ thử nghiệm bằng cách chạy firebase experiments:enable dartfunctions.
Trình mô phỏng hàm không kết nối
Xác minh rằng bạn đang sử dụng localhost và cổng 5001. Nếu bạn đang kiểm thử trên Trình mô phỏng Android, thì thiết bị sẽ không phân giải localhost thành máy chủ của bạn. Cập nhật cấu hình trình mô phỏng trong main.dart để sử dụng 10.0.2.2.
Không tìm thấy gói dùng chung
Xác minh đường dẫn tương đối trong functions/pubspec.yaml. Nếu cấu trúc thư mục của bạn khác với cấu trúc trong lớp học lập trình, hãy điều chỉnh path: ../packages/shared để trỏ đến thư mục chính xác.
Tôi có cần dùng json_serializable không?
Mặc dù không bắt buộc, nhưng việc sử dụng json_serializable sẽ ngăn chặn các lỗi do viết phương thức fromJson và toJson theo cách thủ công. Điều này đảm bảo giao diện người dùng và phần phụ trợ của bạn có cùng định dạng dữ liệu.
11. Xin chúc mừng
Bạn đã tạo thành công một ứng dụng Dart full-stack. Bằng cách duy trì các mô hình dữ liệu trong một gói dùng chung, bạn đảm bảo rằng các phản hồi API và giao diện người dùng của ứng dụng luôn được đồng bộ hoá, bằng cách sử dụng một ngôn ngữ lập trình duy nhất trên toàn bộ ngăn xếp.