1. Wprowadzenie
W ramach tego ćwiczenia utworzysz aplikację do liczenia dla wielu graczy. Dowiesz się, jak używać języka Dart zarówno w przypadku frontendu Flutter, jak i backendu Firebase.
Dowiesz się też, jak udostępniać modele danych między aplikacją a serwerem, co eliminuje konieczność duplikowania logiki.
Pełny kod źródłowy tego ćwiczenia znajdziesz w repozytorium z przykładami Cloud Functions dla Firebase.
Czego się nauczysz
- Wyodrębnij wspólną logikę biznesową do osobnego pakietu Dart.
- Pisanie i wdrażanie funkcji w Cloud Functions dla Firebase w Dart.
- Skorzystaj z kompilacji AOT (Ahead-of-Time) w Dart, aby ograniczyć uruchomienia „na zimno” w przypadku funkcji bezserwerowych.
- Testuj stos lokalnie za pomocą Pakietu emulatorów Firebase.
2. Wymagania wstępne
- Pakiet SDK Flutter (najnowsza stabilna wersja).
- Wiersz poleceń Firebase (wymagana jest wersja 15.15.0 lub nowsza).
- Edytor kodu, np. Antigravity, Visual Studio Code, IntelliJ lub Android Studio, z zainstalowanymi wtyczkami Dart i Flutter.
- Podstawowa znajomość Fluttera i Firebase.
3. Dlaczego warto używać Darta na potrzeby backendu?
Wiele aplikacji w chmurze używa języka Dart do tworzenia interfejsu użytkownika, a innego języka, np. TypeScript, Python lub Go, do tworzenia backendu. Wymaga to utrzymywania 2 oddzielnych zestawów modeli danych. Gdy schemat bazy danych ulegnie zmianie, musisz zaktualizować obie bazy kodu.
Uwaga: używanie języka Dart na backendzie umożliwia połączenie responsywności interfejsu użytkownika Fluttera na kliencie z bezpieczną weryfikacją na serwerze bez duplikowania kodu.
4. Tworzenie aplikacji we Flutterze
Utwórz standardową aplikację we Flutterze:
flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run
W standardowej aplikacji Flutter stan licznika jest obsługiwany lokalnie przez element lib/main.dart:
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
To podejście sprawdza się w przypadku stanu lokalnego, ale nie jest skalowalne w aplikacji wieloosobowej, w której serwer musi być źródłem informacji. Aby obsługiwać wielu graczy, w kolejnych krokach przeniesiemy tę logikę na backend.
5. Tworzenie pakietu udostępnionego
Aby uniknąć duplikowania modeli w interfejsie i na backendzie, utwórz w repozytorium projektu wspólny pakiet Dart. Z tego pakietu korzystają zarówno aplikacja Flutter, jak i funkcje Firebase.
W katalogu głównym projektu my_counter uruchom te polecenia:
mkdir -p packages
cd packages
dart create -t package shared
Dodawanie zależności
W sekcji packages/shared/pubspec.yaml dodaj narzędzia do serializacji JSON:
dependencies:
json_annotation: ^4.11.0
dev_dependencies:
build_runner: ^2.13.1
json_serializable: ^6.13.1
Określanie modeli udostępnionych
Utwórz packages/shared/lib/src/models.dart. Ten plik definiuje strukturę danych używaną zarówno przez aplikację, jak i serwer.
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';
Wyeksportuj modele w usłudze packages/shared/lib/shared.dart:
library shared;
export 'src/models.dart';
W katalogu packages/shared uruchom narzędzie do kompilacji, aby wygenerować kod serializacji JSON:
dart run build_runner build
6. Konfigurowanie Cloud Functions dla Firebase
Cloud Functions dla Firebase to bezserwerowa platforma, która umożliwia automatyczne uruchamianie kodu backendu bez konieczności zarządzania własnymi serwerami i skalowania ich. Dart doskonale się do tego nadaje, ponieważ kompiluje się z wyprzedzeniem (AOT) do postaci binarnej i nie wymaga rozbudowanego środowiska wykonawczego, takiego jak Node.js czy Java. Znacznie skraca to czas uruchamiania „na zimno” funkcji.
Otwórz katalog główny projektu i zainicjuj Cloud Functions dla Firebase:
cd ../..
firebase experiments:enable dartfunctions
firebase init functions
dart pub add google_cloud_firestore
- Gdy pojawi się prośba o wybranie języka, kliknij Dart.
Połącz udostępniony pakiet
W functions/pubspec.yaml dodaj ścieżkę względną do pakietu udostępnionego:
dependencies:
firebase_admin_sdk: ^0.5.6
firebase_functions: ^0.8.0
google_cloud_firestore: ^0.5.5
shared:
path: ../packages/shared
7. Pisanie funkcji
Aby napisać logikę backendu, otwórz plik functions/bin/server.dart i zastąp jego zawartość tym kodem:
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() {
runFunctions((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 =
int.tryParse(request.url.queryParameters['step'] ?? '') ?? 1;
await counterDoc.update({'count': FieldValue.increment(step)});
incrementResponse = IncrementResponse(
success: true,
message: 'Atomic increment complete',
newCount: value + step,
);
} else {
throw HttpResponseException.badRequest(
message: '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. Testowanie lokalne za pomocą Pakietu emulatorów Firebase
Możesz uruchomić zarówno frontend, jak i backend lokalnie bez wdrażania.
W katalogu głównym projektu uruchom Pakiet emulatorów Firebase:
# Enable functions and firestore for the emulators
firebase init emulators
# Start the emulators and optionally open up the Admin UI
firebase emulators:start
Połącz udostępniony pakiet
W sekcji pubspec.yaml dodaj ścieżkę względną do pakietu udostępnionego i dodaj pakiet http:
dependencies:
http: ^1.6.0
shared:
path: packages/shared
W projekcie Flutter otwórz plik lib/main.dart i zastąp jego zawartość poniższym kodem. Ten kod frontendu używa tej samej klasy IncrementResponse co 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),
),
);
}
}
Uruchom aplikację Flutter. Gdy klikniesz pływający przycisk działania, aplikacja wywoła lokalny backend Dart, pobierze nową liczbę i zaktualizuje interfejs.
9. Wdrażanie w Firebase
W tym ćwiczeniu możesz wypróbować Funkcje bez projektu w Firebase ani konta rozliczeniowego, korzystając z Pakietu emulatorów lokalnych Firebase. Jeśli chcesz używać funkcji w prawdziwym środowisku (np. środowisku produkcyjnym), musisz skonfigurować projekt w Firebase i płatności.
Tworzenie projektu w Firebase
- Zaloguj się w konsoli Firebase, korzystając ze swojego konta Google.
- Kliknij przycisk, aby utworzyć nowy projekt, a następnie wpisz jego nazwę.
- Kliknij Dalej.
- Jeśli pojawi się odpowiedni komunikat, przeczytaj i zaakceptuj warunki korzystania z Firebase, a potem kliknij Dalej.
- (Opcjonalnie) Włącz w konsoli Firebase pomoc AI (nazywaną „Gemini w Firebase”).
- W tym ćwiczeniu nie potrzebujesz Google Analytics, więc wyłącz opcję Google Analytics.
- Kliknij Utwórz projekt, poczekaj, aż projekt zostanie udostępniony, a następnie kliknij Dalej.
Przechodzenie na wyższy pakiet cenowy Firebase
Aby korzystać z usług Firebase w tym ćwiczeniu, musisz mieć projekt w Firebase w abonamencie z płatnością według wykorzystania (Blaze), co oznacza, że musi on być połączony z kontem rozliczeniowym Cloud.
- Konto rozliczeniowe Cloud wymaga formy płatności, np. karty kredytowej.
- Podczas specjalnych promocji lub jeśli wykonujesz to ćwiczenie w ramach wydarzenia, mogą być dostępne środki w Google Cloud.
- Jeśli dopiero zaczynasz korzystać z Firebase i Google Cloud, sprawdź, czy kwalifikujesz się do otrzymania środków w wysokości 300 USD i konta rozliczeniowego Cloud w ramach bezpłatnego okresu próbnego.
Aby przejść na abonament Blaze, wykonaj te czynności:
- W konsoli Firebase wybierz przejście na wyższy abonament.
- Wybierz pakiet Blaze. Postępuj zgodnie z instrukcjami wyświetlanymi na ekranie, aby połączyć konto rozliczeniowe Cloud z projektem.
- Jeśli w tym laboratorium korzystasz z kredytów Google Cloud, konto rozliczeniowe prawdopodobnie będzie się nazywać
Google Cloud Platform Trial Billing AccountlubMy Billing Account. - Jeśli w ramach tego uaktualnienia konieczne było utworzenie konta rozliczeniowego Cloud, może być potrzebny powrót do procesu uaktualniania w konsoli Firebase, aby dokończyć uaktualnienie.
- Jeśli w tym laboratorium korzystasz z kredytów Google Cloud, konto rozliczeniowe prawdopodobnie będzie się nazywać
Wdrażanie w projekcie w Firebase
Aby wdrożyć backend w Dart, uruchom to polecenie za pomocą interfejsu wiersza poleceń Firebase:
firebase use <PROJECT_ID>
firebase deploy --only functions
Po uruchomieniu polecenia skopiuj adres URL i zastąp nim znak FIREBASE_FUNCTIONS_URL_HERE w kodzie źródłowym aplikacji Flutter, który został dodany wcześniej.
10. Rozwiązywanie problemów
firebase: command not found
Sprawdź, czy wiersz poleceń Firebase jest zainstalowany i czy PATH jest zaktualizowany. Możesz go zainstalować za pomocą npm: npm install -g firebase-tools.
Brak języka Dart w szablonach funkcji inicjujących
Aby Dart wyświetlał się jako lista opcji wdrażania i tworzenia kodu szablonu podczas uruchamiania firebase init functions, należy ustawić flagę eksperymentu, uruchamiając firebase experiments:enable dartfunctions.
Emulator funkcji nie łączy się
Sprawdź, czy używasz localhost i portu 5001. Jeśli testujesz na Android Emulatorze, urządzenie nie rozpoznaje localhost jako komputera hosta. Zaktualizuj konfigurację emulatora w main.dart, aby używać 10.0.2.2.
Nie znaleziono udostępnionego pakietu
Sprawdź ścieżkę względną w functions/pubspec.yaml. Jeśli struktura folderów różni się od struktury w tym ćwiczeniu, dostosuj path: ../packages/shared, aby wskazywał prawidłowy katalog.
Czy muszę korzystać z usługi json_serializable?
Chociaż nie jest to bezwzględnie wymagane, używanie json_serializable zapobiega błędom spowodowanym ręcznym pisaniem metod fromJson i toJson. Dzięki temu masz pewność, że interfejs i backend oczekują dokładnie tego samego formatu danych.
11. Gratulacje
Udało Ci się utworzyć aplikację Dart typu full-stack. Utrzymując modele danych w pakiecie udostępnionym, masz pewność, że odpowiedzi interfejsu API i interfejs klienta pozostaną zsynchronizowane, a w całym stosie będzie używany jeden język programowania.
Aby wyświetlić pełny kod tego projektu, zapoznaj się z przykładowym ćwiczeniem w repozytorium firebase/functions-samples.