1. Введение
В этом практическом занятии вы создадите приложение-счетчик для многопользовательской игры. Вы научитесь использовать Dart как для фронтенда на Flutter, так и для бэкенда на Firebase.
Вы также узнаете, как обмениваться моделями данных между вашим приложением и сервером, что исключает необходимость дублирования логики.
Что вы узнаете
- Вынесите общую бизнес-логику в отдельный пакет Dart.
- Разрабатывайте и развертывайте облачные функции для Firebase на языке Dart.
- Используйте компиляцию Ahead-of-Time (AOT) в Dart, чтобы сократить количество холодных запусков бессерверных приложений.
- Протестируйте свою систему локально, используя Firebase Emulator Suite.
2. Предварительные требования
- Flutter SDK (последняя стабильная версия).
- Для работы требуется Firebase CLI (версия 15.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 запустите средство сборки, чтобы сгенерировать код сериализации JSON:
dart run build_runner build
6. Настройка облачных функций для 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
Для развертывания бэкенда на Dart выполните следующую команду:
firebase deploy --only functions
После выполнения команды скопируйте URL-адрес и замените им FIREBASE_FUNCTIONS_URL_HERE в исходном коде приложения Flutter, который мы добавили ранее.
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 . Если структура ваших папок отличается от той, что используется в практическом задании, измените path: ../packages/shared чтобы он указывал на правильный каталог.
Нужно ли мне использовать json_serializable ?
Хотя это и не является строго обязательным, использование json_serializable предотвращает ошибки, возникающие при ручном написании методов fromJson и toJson . Это гарантирует, что ваш фронтенд и бэкенд ожидают один и тот же формат данных.
11. Поздравляем!
Вы успешно создали полнофункциональное приложение на Dart. Поддерживая модели данных в общем пакете, вы обеспечиваете синхронизацию ответов API и пользовательского интерфейса клиента, используя единый язык программирования для всего вашего стека.