Cloud Functions for Firebase की मदद से, फ़ुल-स्टैक Dart ऐप्लिकेशन बनाना

1. परिचय

इस कोडलैब में, एक से ज़्यादा खिलाड़ियों के लिए काउंटर ऐप्लिकेशन बनाया गया है. इसमें, Flutter फ़्रंटएंड और Firebase बैकएंड, दोनों के लिए Dart का इस्तेमाल करने का तरीका बताया गया है.

आपको यह भी पता चलता है कि अपने ऐप्लिकेशन और सर्वर के बीच डेटा मॉडल कैसे शेयर किए जाएं, ताकि लॉजिक को डुप्लीकेट करने की ज़रूरत न पड़े.

आपको क्या सीखने को मिलेगा

  • शेयर किए गए बिज़नेस लॉजिक को एक स्टैंडअलोन डार्ट पैकेज में एक्सट्रैक्ट करें.
  • Dart में, Cloud Functions for Firebase को नेटिव तौर पर लिखें और डिप्लॉय करें.
  • सर्वरलेस कोल्ड स्टार्ट को कम करने के लिए, Dart के Ahead-of-Time (AOT) कंपाइलेशन का इस्तेमाल करें.
  • Firebase Emulator Suite का इस्तेमाल करके, अपने स्टैक को स्थानीय तौर पर टेस्ट करें.

2. ज़रूरी शर्तें

  • Flutter SDK (नया स्टेबल वर्शन).
  • Firebase CLI (v15.15.0 या इसके बाद का वर्शन ज़रूरी है).
  • Dart और Flutter प्लगिन इंस्टॉल किया गया कोई कोड एडिटर, जैसे कि Antigravity, Visual Studio Code, IntelliJ या Android Studio.
  • 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. शेयर किया जाने वाला पैकेज बनाना

फ़्रंटएंड और बैकएंड पर मॉडल को डुप्लीकेट होने से रोकने के लिए, अपने प्रोजेक्ट रिपॉज़िटरी में एक शेयर किया गया डार्ट पैकेज बनाएं. 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';

JSON सीरियलाइज़ेशन कोड जनरेट करने के लिए, packages/shared डायरेक्ट्री में बिल्ड रनर चलाएं:

dart run build_runner build

6. Firebase के लिए Cloud Functions सेट अप करना

Cloud Functions for Firebase एक सर्वरलेस फ़्रेमवर्क है. इसकी मदद से, बैकएंड कोड अपने-आप चलता है. इसके लिए, आपको अपने सर्वर को मैनेज और स्केल करने की ज़रूरत नहीं होती. Dart एक बेहतरीन विकल्प है, क्योंकि यह बाइनरी में कंपाइल होने से पहले (एओटी) काम करता है. इसके लिए, Node.js या Java जैसे बड़े रनटाइम एनवायरमेंट की ज़रूरत नहीं होती. इससे आपके फ़ंक्शन के कोल्ड स्टार्ट होने में लगने वाला समय काफ़ी कम हो जाता है.

अपने प्रोजेक्ट के रूट पर जाएं और Firebase के लिए Cloud Functions को शुरू करें:

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

कमांड चलने के बाद, यूआरएल को कॉपी करें और उसे फ़्लटर ऐप्लिकेशन के उस सोर्स कोड में FIREBASE_FUNCTIONS_URL_HERE की जगह पर बदलें जिसे हमने पहले जोड़ा था.

10. समस्या का हल

firebase: command not found

पक्का करें कि Firebase CLI इंस्टॉल हो और आपका PATH अपडेट हो. इसे npm का इस्तेमाल करके इंस्टॉल किया जा सकता है: npm install -g firebase-tools.

init फ़ंक्शन के टेंप्लेट में Dart मौजूद नहीं है

Dart को डिप्लॉय करने के लिए विकल्पों की सूची के तौर पर दिखाने के लिए, firebase init functions को चलाने पर टेंप्लेट कोड बनाना होगा. इसके लिए, firebase init functions को चलाकर एक्सपेरिमेंट फ़्लैग सेट करना होगा.firebase experiments:enable dartfunctions

फ़ंक्शन एम्युलेटर कनेक्ट नहीं हो रहा है

पुष्टि करें कि आपने localhost और पोर्ट 5001 का इस्तेमाल किया हो. अगर Android Emulator पर टेस्टिंग की जा रही है, तो डिवाइस localhost को आपकी होस्ट मशीन से नहीं जोड़ता है. main.dart में एम्युलेटर कॉन्फ़िगरेशन अपडेट करें, ताकि 10.0.2.2 का इस्तेमाल किया जा सके.

शेयर किया गया पैकेज नहीं मिला

functions/pubspec.yaml में रिलेटिव पाथ की पुष्टि करें. अगर आपका फ़ोल्डर स्ट्रक्चर, कोडलैब से अलग है, तो सही डायरेक्ट्री पर ले जाने के लिए path: ../packages/shared में बदलाव करें.

क्या मुझे json_serializable का इस्तेमाल करना होगा?

json_serializable का इस्तेमाल करना ज़रूरी नहीं है. हालांकि, इससे fromJson और toJson तरीकों को मैन्युअल तरीके से लिखने की वजह से होने वाली गड़बड़ियों को रोका जा सकता है. इससे यह पक्का होता है कि आपके फ़्रंटएंड और बैकएंड को एक ही डेटा फ़ॉर्मैट की ज़रूरत है.

11. बधाई हो

आपने एक फ़ुल-स्टैक डार्ट ऐप्लिकेशन बना लिया है. शेयर किए गए पैकेज में अपने डेटा मॉडल को बनाए रखने से, यह पक्का किया जा सकता है कि आपके एपीआई जवाब और क्लाइंट यूज़र इंटरफ़ेस (यूआई) सिंक रहें. इसके लिए, आपको अपने पूरे स्टैक में एक ही प्रोग्रामिंग भाषा का इस्तेमाल करना होगा.