ساختن برنامه Dart با پشته کامل با «توابع ابری ویژه Firebase»

۱. مقدمه

در این codelab، برنامه شمارنده چندنفره می‌سازید. یاد می‌گیرید که چگونه از Dart برای هم پیش‌نمای Flutter و هم زیرینه Firebase استفاده کنید.

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

می‌توانید کد منبع کامل این codelab را در مخزن نمونه‌های «توابع ابری ویژه Firebase» مشاهده کنید.

آنچه خواهید آموخت

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

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

  • کیت توسعه نرم‌افزار Flutter (آخرین نسخه پایدار).
  • Firebase CLI (نسخه ۱۵.۱۵.۰ یا بالاتر الزامی است).
  • ویرایشگر کد، مانند Antigravity، Visual Studio Code،‏ IntelliJ، یا Android Studio، با نصب افزونه‌های Dart و Flutter.
  • آشنایی اولیه با Flutter و Firebase.

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

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

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

۴. ایجاد برنامه 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++;
  });
}

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

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

برای جلوگیری از تکرار مدل‌ها در پیش‌زمینه و پس‌زمینه، یک بسته مشترک Dart در مخزن پروژه خود ایجاد کنید. هم برنامه Flutter و هم توابع Firebase به این بسته وابسته هستند.

از ریشه پروژه my_counter، فرمان‌های زیر را اجرا کنید:

mkdir -p packages
cd packages
dart create -t package shared

افزودن وابستگی‌ها

در packages/shared/pubspec.yaml، ابزارهای سریال‌سازی JSON را اضافه کنید:

dependencies:
  json_annotation: ^4.11.0

dev_dependencies:
  build_runner: ^2.13.1
  json_serializable: ^6.13.1

تعریف کردن مدل‌های مشترک

‫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

‫۶. راه‌اندازی Cloud Functions ویژه Firebase

«توابع ابری ویژه 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_admin_sdk: ^0.5.6
  firebase_functions: ^0.8.0
  google_cloud_firestore: ^0.5.5
  shared:
    path: ../packages/shared

‫۷. نوشتن تابع

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

  });
}

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

می‌توانید هم پیش‌زمینه و هم پس‌زمینه را بدون استقرار به‌صورت محلی اجرا کنید.

از ریشه پروژه، «مجموعه شبیه‌ساز Firebase» را شروع کنید:

# 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 تماس می‌گیرد، تعداد جدید را بازیابی می‌کند، و واسط کاربر را به‌روز می‌کند.

‫۹. استقرار در Firebase

در این codelab، بااستفاده از «مجموعه شبیه‌ساز محلی Firebase» توانستید توابع را بدون پروژه Firebase یا حساب صورت‌حساب امتحان کنید. اگر می‌خواهید از توابع خود در محیط واقعی (مانند محیط تولید) استفاده کنید، باید پروژه Firebase و صورت‌حساب راه‌اندازی کنید.

ایجاد پروژه Firebase

  1. بااستفاده از «حساب Google» خود به سیستم کنسول Firebase وارد شوید.
  2. برای ایجاد پروژه جدید، روی دکمه کلیک کنید و سپس نام پروژه را وارد کنید.
  3. روی ادامه کلیک کنید.
  4. درصورت درخواست، شرایط Firebase را مرور و تأیید کنید، و سپس روی ادامه کلیک کنید.
  5. (اختیاری) «دستیار هوش مصنوعی» را در کنسول Firebase فعال کنید (به آن «Gemini در Firebase» گفته می‌شود).
  6. برای این کدآزمایی، به Google Analytics نیاز ندارید، بنابراین گزینه Google Analytics را خاموش کنید.
  7. روی ایجاد پروژه کلیک کنید، منتظر بمانید تا پروژه شما آماده شود، و سپس روی ادامه دادن کلیک کنید.

ارتقا دادن طرح قیمت‌گذاری Firebase

برای استفاده از خدمات Firebase در این تمرین گام‌به‌گام، پروژه Firebase شما باید در طرح قیمت‌گذاری براساس مصرف (Blaze) باشد، که به این معنی است که به حساب Cloud Billing پیوند داده شده است.

  • حساب «صورت‌حساب Cloud» به روش پرداخت، مانند کارت اعتباری، نیاز دارد.
  • درطول تبلیغات ویژه یا اگر این codelab را به‌عنوان بخشی از یک رویداد انجام می‌دهید، ممکن است اعتبارات Google Cloud دردسترس باشد.
  • اگر با Firebase و Google Cloud آشنایی ندارید، بررسی کنید که آیا واجدشرایط ۳۰۰ دلار اعتبار و حساب «صورت‌حساب Cloud» دوره آزمایشی رایگان هستید یا نه.

برای ارتقا دادن پروژه به طرح Blaze، این مراحل را دنبال کنید:

  1. در کنسول Firebase، ارتقا دادن طرح را انتخاب کنید.
  2. طرح Blaze را انتخاب کنید. دستورالعمل‌های روی صفحه را برای پیوند دادن حساب Cloud Billing به پروژه‌تان دنبال کنید.
    • اگر برای این کدآزمایی از اعتبارهای Google Cloud استفاده می‌کنید، احتمالاً نام حساب صورت‌حساب Google Cloud Platform Trial Billing Account یا My Billing Account است.
    • اگر لازم بود به‌عنوان بخشی از این ارتقا، حساب «صدور صورت‌حساب Cloud» ایجاد کنید، ممکن است لازم باشد برای تکمیل ارتقا به جریان ارتقا در کنسول Firebase برگردید.

استقرار در پروژه Firebase

برای استقرار زیرینه Dart، فرمان زیر را بااستفاده از Firebase CLI اجرا کنید:

firebase use <PROJECT_ID>
firebase deploy --only functions

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

‫۱۰. عیب‌یابی

firebase: command not found

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

‫Dart در الگوهای تابع init وجود ندارد

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

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

تأیید کنید که از localhost و درگاه 5001 استفاده می‌کنید. اگر در «شبیه‌ساز Android» آزمایش می‌کنید، دستگاه localhost را به ماشین میزبان شما حل نمی‌کند. برای استفاده از 10.0.2.2، پیکربندی شبیه‌ساز را در main.dart به‌روز کنید.

بسته هم‌رسانی‌شده پیدا نشد

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

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

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

‫۱۱. تبریک می‌گوییم

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

برای مشاهده کد کامل نهایی این پروژه، نمونه کدآزمایشی در مخزن firebase/functions-samples را ببینید.