1. Pengantar
Dalam codelab ini, Anda akan membuat aplikasi penghitung multiplayer. Anda akan mempelajari cara menggunakan Dart untuk frontend Flutter dan backend Firebase.
Anda juga akan mempelajari cara membagikan model data antara aplikasi dan server, sehingga tidak perlu menduplikasi logika.
Yang akan Anda pelajari
- Ekstrak logika bisnis bersama ke dalam paket Dart mandiri.
- Menulis dan men-deploy Cloud Functions for Firebase secara native di Dart.
- Manfaatkan kompilasi Ahead-of-Time (AOT) Dart untuk mengurangi cold start serverless.
- Uji stack Anda secara lokal menggunakan Firebase Emulator Suite.
2. Prasyarat
- Flutter SDK (versi stabil terbaru).
- Firebase CLI (diperlukan v15.15.0 atau yang lebih baru).
- Editor kode, seperti Antigravity, Visual Studio Code, IntelliJ, atau Android Studio, dengan plugin Dart dan Flutter yang sudah diinstal.
- Pemahaman dasar tentang Flutter dan Firebase.
3. Mengapa menggunakan Dart untuk backend?
Banyak aplikasi cloud menggunakan Dart untuk UI frontend dan bahasa lain, seperti TypeScript, Python, atau Go, untuk backend. Hal ini mengharuskan Anda mempertahankan dua set model data yang terpisah. Saat skema database berubah, Anda harus memperbarui kedua codebase.
Catatan: Dengan menggunakan Dart di backend, Anda dapat menggabungkan pengalaman pengguna responsif Flutter di klien dengan validasi aman di server, tanpa menduplikasi kode.
4. Membuat aplikasi Flutter
Buat aplikasi Flutter standar:
flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run
Di aplikasi Flutter standar, lib/main.dart menangani status penghitung secara lokal:
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
Pendekatan ini berfungsi untuk status lokal, tetapi tidak dapat diskalakan ke aplikasi multiplayer di mana server harus bertindak sebagai sumber kebenaran. Untuk mendukung beberapa pemain, kita akan memindahkan logika ini ke backend pada langkah-langkah berikut.
5. Membuat paket bersama
Untuk menghindari duplikasi model di frontend dan backend, buat paket Dart bersama di dalam repositori project Anda. Aplikasi Flutter dan fungsi untuk Firebase bergantung pada paket ini.
Dari root project my_counter, jalankan perintah berikut:
mkdir -p packages
cd packages
dart create -t package shared
Menambahkan dependensi
Di packages/shared/pubspec.yaml, tambahkan alat serialisasi JSON:
dependencies:
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.9
json_serializable: ^6.8.0
Menentukan model bersama Anda
Buat packages/shared/lib/src/models.dart. File ini menentukan struktur data yang digunakan oleh aplikasi dan server.
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';
Di packages/shared/lib/shared.dart, ekspor model Anda:
library shared;
export 'src/models.dart';
Di direktori packages/shared, jalankan runner build untuk membuat kode serialisasi JSON:
dart run build_runner build
6. Menyiapkan Cloud Functions for Firebase
Cloud Functions for Firebase adalah framework tanpa server yang memungkinkan Anda menjalankan kode backend secara otomatis tanpa harus mengelola dan menskalakan server Anda sendiri. Dart sangat cocok karena dikompilasi Ahead-of-Time (AOT) ke biner, tidak memerlukan lingkungan runtime yang berat seperti Node.js atau Java. Hal ini secara signifikan mengurangi waktu cold start untuk fungsi Anda.
Buka root project Anda dan lakukan inisialisasi Cloud Functions for Firebase:
cd ../..
firebase experiments:enable dartfunctions
firebase init functions
dart pub add google_cloud_firestore
- Saat diminta memilih bahasa, pilih Dart.
Menautkan paket bersama
Di functions/pubspec.yaml, tambahkan jalur relatif ke paket bersama:
dependencies:
firebase_functions:
google_cloud_firestore:
shared:
path: ../packages/shared
7. Tulis fungsi
Untuk menulis logika backend, buka functions/bin/server.dart dan ganti kontennya dengan kode berikut:
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. Menguji secara lokal dengan Firebase Emulator Suite
Anda dapat menjalankan frontend dan backend secara lokal tanpa men-deploy.
Dari root project Anda, mulai 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
Menautkan paket bersama
Di pubspec.yaml, tambahkan jalur relatif ke paket bersama dan tambahkan paket http:
dependencies:
http: ^1.6.0
shared:
path: ../packages/shared
Di project Flutter Anda, buka lib/main.dart dan ganti isinya dengan kode berikut. Kode frontend ini menggunakan class IncrementResponse yang sama dengan backend.
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),
),
);
}
}
Jalankan aplikasi Flutter. Saat Anda mengklik tombol tindakan mengambang, aplikasi akan memanggil backend Dart lokal, mengambil jumlah baru, dan memperbarui UI.
9. Men-deploy ke Firebase
Untuk men-deploy backend Dart, jalankan perintah berikut:
firebase deploy --only functions
Setelah perintah berjalan, salin URL dan ganti FIREBASE_FUNCTIONS_URL_HERE dalam kode sumber aplikasi Flutter yang kita tambahkan sebelumnya.
10. Pemecahan masalah
firebase: command not found
Pastikan Firebase CLI telah diinstal dan PATH Anda telah diupdate. Anda dapat menginstalnya menggunakan npm: npm install -g firebase-tools.
Dart tidak ada dalam template fungsi init
Agar Dart ditampilkan sebagai daftar opsi untuk men-deploy dan membuat kode template saat menjalankan firebase init functions, tanda eksperimen harus ditetapkan dengan menjalankan firebase experiments:enable dartfunctions.
Emulator fungsi tidak terhubung
Pastikan Anda menggunakan localhost dan port 5001. Jika Anda melakukan pengujian di Emulator Android, perangkat tidak menyelesaikan localhost ke mesin host Anda. Perbarui konfigurasi emulator di main.dart untuk menggunakan 10.0.2.2.
Paket bersama tidak ditemukan
Verifikasi jalur relatif di functions/pubspec.yaml. Jika struktur folder Anda berbeda dengan codelab, sesuaikan path: ../packages/shared agar mengarah ke direktori yang benar.
Apakah saya perlu menggunakan json_serializable?
Meskipun tidak benar-benar diperlukan, penggunaan json_serializable mencegah error yang disebabkan oleh penulisan metode fromJson dan toJson secara manual. Hal ini memastikan frontend dan backend Anda mengharapkan format data yang sama persis.
11. Selamat
Anda berhasil membangun aplikasi Dart full-stack. Dengan mempertahankan model data dalam paket bersama, Anda memastikan bahwa respons API dan UI klien tetap tersinkron, menggunakan satu bahasa pemrograman di seluruh stack.