ساخت یک برنامه کامل Dart با توابع ابری برای Firebase

۱. مقدمه

در این آزمایشگاه کد، شما یک برنامه شمارنده چند نفره می‌سازید. یاد می‌گیرید که چگونه از دارت برای هر دو بخش فرانت‌اند فلاتر و بک‌اند فایربیس استفاده کنید.

همچنین یاد می‌گیرید که چگونه مدل‌های داده را بین برنامه و سرور خود به اشتراک بگذارید و نیاز به منطق تکراری را از بین ببرید.

آنچه یاد خواهید گرفت

  • منطق تجاری مشترک را در یک بسته Dart مستقل استخراج کنید.
  • توابع ابری را برای فایربیس به صورت بومی در دارت بنویسید و مستقر کنید.
  • از کامپایل Ahead-of-Time (AOT) دارت برای کاهش شروع‌های سرد بدون سرور استفاده کنید.
  • با استفاده از مجموعه شبیه‌ساز Firebase، پشته خود را به صورت محلی آزمایش کنید.

۲. پیش‌نیازها

۳. چرا از دارت برای بک‌اند استفاده کنیم؟

بسیاری از برنامه‌های ابری از Dart برای رابط کاربری frontend و زبان دیگری مانند TypeScript، Python یا Go برای backend استفاده می‌کنند. این امر مستلزم حفظ دو مجموعه جداگانه از مدل‌های داده است. هنگامی که یک طرحواره پایگاه داده تغییر می‌کند، باید هر دو پایگاه داده را به‌روزرسانی کنید.

نکته: استفاده از دارت در بک‌اند به شما امکان می‌دهد تجربه کاربری واکنش‌گرای فلاتر در کلاینت را با اعتبارسنجی امن در سرور، بدون تکرار کد، ترکیب کنید.

۴. اپلیکیشن فلاتر را ایجاد کنید

یک برنامه استاندارد Flutter ایجاد کنید:

flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run

در یک برنامه استاندارد فلاتر، lib/main.dart حالت شمارنده را به صورت محلی مدیریت می‌کند:

int _counter = 0;

void _incrementCounter() {
  setState(() {
    _counter++;
  });
}

این رویکرد برای حالت محلی کار می‌کند، اما برای یک برنامه چند نفره که در آن سرور باید به عنوان منبع حقیقت عمل کند، قابل استفاده نیست. برای پشتیبانی از چندین بازیکن، این منطق را در مراحل بعدی به backend منتقل خواهیم کرد.

۵. بسته اشتراکی را ایجاد کنید

برای جلوگیری از تکرار مدل‌ها در frontend و backend، یک پکیج 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

۶. تنظیم توابع ابری برای فایربیس

توابع ابری برای فایربیس یک چارچوب بدون سرور است که به شما امکان می‌دهد بدون نیاز به مدیریت و مقیاس‌بندی سرورهای خود، کد بک‌اند را به طور خودکار اجرا کنید. دارت گزینه بسیار مناسبی است زیرا Ahead-of-Time (AOT) را به یک فایل باینری کامپایل می‌کند و به یک محیط زمان اجرای سنگین مانند Node.js یا جاوا نیاز ندارد. این امر به طور قابل توجهی زمان شروع سرد برای توابع شما را کاهش می‌دهد.

به ریشه پروژه خود بروید و توابع ابری را برای فایربیس مقداردهی اولیه کنید:

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

۷. تابع را بنویسید

برای نوشتن منطق backend، 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'},
      );
    });

  });
}

۸. تست محلی با مجموعه شبیه‌ساز Firebase

شما می‌توانید هم frontend و هم backend را به صورت محلی و بدون نیاز به استقرار (deployment) اجرا کنید.

از ریشه پروژه خود، 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

در پروژه فلاتر خود، lib/main.dart را باز کنید و محتویات آن را با کد زیر جایگزین کنید. این کد frontend از همان کلاس IncrementResponse که در 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),
      ),
    );
  }
}

برنامه Flutter را اجرا کنید. وقتی روی دکمه اکشن شناور کلیک می‌کنید، برنامه backend محلی Dart را فراخوانی می‌کند، تعداد جدید را بازیابی می‌کند و رابط کاربری را به‌روزرسانی می‌کند.

۹. استقرار در فایربیس

برای استقرار backend دارت، دستور زیر را اجرا کنید:

firebase deploy --only functions

پس از اجرای دستور، URL را کپی کرده و FIREBASE_FUNCTIONS_URL_HERE در کد منبع برنامه flutter که قبلاً اضافه کردیم، جایگزین کنید.

۱۰. عیب‌یابی

firebase: command not found

مطمئن شوید که Firebase CLI نصب شده و PATH شما به‌روزرسانی شده است. می‌توانید آن را با استفاده از npm نصب کنید: npm install -g firebase-tools .

دارت در قالب‌های توابع init وجود ندارد

برای اینکه دارت هنگام اجرای firebase init functions لیستی از گزینه‌های استقرار و ایجاد کد الگو را نشان دهد، باید با اجرای firebase experiments:enable dartfunctions یک پرچم آزمایش تنظیم شود.

شبیه‌ساز توابع متصل نمی‌شود

تأیید کنید که از localhost و پورت 5001 استفاده می‌کنید. اگر روی یک شبیه‌ساز اندروید آزمایش می‌کنید، دستگاه localhost به دستگاه میزبان شما اختصاص نمی‌دهد. پیکربندی شبیه‌ساز را در main.dart به‌روزرسانی کنید تا 10.0.2.2 استفاده کند.

بسته مشترک یافت نشد

مسیر نسبی را در functions/pubspec.yaml بررسی کنید. اگر ساختار پوشه شما با codelab متفاوت است، path: ../packages/shared را طوری تنظیم کنید که به دایرکتوری صحیح اشاره کند.

آیا باید از json_serializable استفاده کنم؟

اگرچه کاملاً ضروری نیست، استفاده از json_serializable از خطاهای ناشی از نوشتن دستی متدهای fromJson و toJson جلوگیری می‌کند. این تضمین می‌کند که frontend و backend شما دقیقاً از یک قالب داده انتظار دارند.

۱۱. تبریک

شما با موفقیت یک برنامه‌ی کامل Dart ساختید. با نگهداری مدل‌های داده‌ی خود در یک بسته‌ی مشترک، تضمین می‌کنید که پاسخ‌های API و رابط کاربری کلاینت شما با استفاده از یک زبان برنامه‌نویسی واحد در کل پشته‌ی شما، همگام‌سازی می‌شوند.