ক্লাউড ফাংশনস ফর ফায়ারবেস ব্যবহার করে একটি ফুল-স্ট্যাক ডার্ট অ্যাপ তৈরি করুন

১. ভূমিকা

এই কোডল্যাবে আপনি একটি মাল্টিপ্লেয়ার কাউন্টার অ্যাপ তৈরি করবেন। আপনি শিখবেন কীভাবে ফ্লাটার ফ্রন্টএন্ড এবং ফায়ারবেস ব্যাকএন্ড উভয়ের জন্য ডার্ট ব্যবহার করতে হয়।

আপনি আরও শিখবেন কীভাবে আপনার অ্যাপ এবং সার্ভারের মধ্যে ডেটা মডেল শেয়ার করতে হয়, যার ফলে লজিকের পুনরাবৃত্তির প্রয়োজন হয় না।

আপনি যা শিখবেন

  • শেয়ার করা ব্যবসায়িক লজিককে একটি স্বতন্ত্র ডার্ট প্যাকেজে স্থানান্তর করুন।
  • ডার্ট-এ নেটিভভাবে ফায়ারবেসের জন্য ক্লাউড ফাংশন লিখুন এবং স্থাপন করুন।
  • সার্ভারলেস কোল্ড স্টার্ট কমাতে ডার্টের অ্যাহেড-অফ-টাইম (AOT) কম্পাইলেশন ব্যবহার করুন।
  • Firebase Emulator Suite ব্যবহার করে আপনার স্ট্যাকটি স্থানীয়ভাবে পরীক্ষা করুন।

২. পূর্বশর্তসমূহ

  • ফ্লাটার এসডিকে (সর্বশেষ স্থিতিশীল সংস্করণ)।
  • ফায়ারবেস সিএলআই (v15.15.0 বা উচ্চতর সংস্করণ প্রয়োজন)।
  • একটি কোড এডিটর, যেমন অ্যান্টিগ্র্যাভিটি , ভিজ্যুয়াল স্টুডিও কোড, ইন্টেলিজ বা অ্যান্ড্রয়েড স্টুডিও, যেখানে ডার্ট এবং ফ্লাটার প্লাগইন ইনস্টল করা আছে।
  • ফ্লাটার এবং ফায়ারবেস সম্পর্কে প্রাথমিক ধারণা।

৩. ব্যাকএন্ডের জন্য ডার্ট কেন ব্যবহার করা হয়?

অনেক ক্লাউড অ্যাপ্লিকেশন ফ্রন্টএন্ড UI-এর জন্য ডার্ট (Dart) এবং ব্যাকএন্ডের জন্য টাইপস্ক্রিপ্ট (TypeScript), পাইথন (Python) বা গো (Go)-এর মতো অন্য কোনো ভাষা ব্যবহার করে। এর জন্য দুটি পৃথক ডেটা মডেল সেট রক্ষণাবেক্ষণ করতে হয়। যখন কোনো ডাটাবেস স্কিমা পরিবর্তিত হয়, তখন আপনাকে উভয় কোডবেসই আপডেট করতে হয়।

দ্রষ্টব্য: ব্যাকএন্ডে ডার্ট ব্যবহার করলে, কোডের পুনরাবৃত্তি ছাড়াই ক্লায়েন্টে ফ্লাটারের রেসপন্সিভ ইউজার এক্সপেরিয়েন্সের সাথে সার্ভারে সুরক্ষিত ভ্যালিডেশনকে একত্রিত করা যায়।

৪. ফ্লাটার অ্যাপটি তৈরি করুন

একটি স্ট্যান্ডার্ড ফ্লাটার অ্যাপ তৈরি করুন:

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++;
  });
}

এই পদ্ধতিটি লোকাল স্টেটের জন্য কাজ করলেও, মাল্টিপ্লেয়ার অ্যাপ্লিকেশনের ক্ষেত্রে এটি উপযুক্ত নয়, যেখানে সার্ভারকেই তথ্যের মূল উৎস হিসেবে কাজ করতে হয়। একাধিক প্লেয়ারকে সাপোর্ট করার জন্য, আমরা পরবর্তী ধাপগুলোতে এই লজিকটি ব্যাকএন্ডে সরিয়ে নেব।

৫. শেয়ার করা প্যাকেজটি তৈরি করুন

ফ্রন্টএন্ড এবং ব্যাকএন্ডে মডেলের পুনরাবৃত্তি এড়াতে, আপনার প্রজেক্ট রিপোজিটরির ভিতরে একটি শেয়ার্ড ডার্ট প্যাকেজ তৈরি করুন। ফ্লাটার অ্যাপ এবং ফায়ারবেসের ফাংশন উভয়ই এই প্যাকেজের উপর নির্ভরশীল।

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

৬. ফায়ারবেসের জন্য ক্লাউড ফাংশন সেট আপ করুন

ক্লাউড ফাংশনস ফর ফায়ারবেস হলো একটি সার্ভারলেস ফ্রেমওয়ার্ক যা আপনাকে নিজের সার্ভার পরিচালনা ও স্কেল করার ঝামেলা ছাড়াই স্বয়ংক্রিয়ভাবে ব্যাকএন্ড কোড চালাতে দেয়। ডার্ট এর জন্য একটি চমৎকার বিকল্প, কারণ এটি অ্যাহেড-অফ-টাইম (AOT) পদ্ধতিতে বাইনারিতে কম্পাইল হয় এবং এর জন্য নোড.জেএস বা জাভার মতো ভারী রানটাইম এনভায়রনমেন্টের প্রয়োজন হয় না। এটি আপনার ফাংশনগুলোর কোল্ড স্টার্ট টাইম উল্লেখযোগ্যভাবে কমিয়ে দেয়।

আপনার প্রজেক্ট রুটে যান এবং 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

৭. ফাংশনটি লিখুন।

ব্যাকএন্ড লজিক লেখার জন্য, 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 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 খুলুন এবং এর ভেতরের লেখাগুলো নিচের কোড দিয়ে প্রতিস্থাপন করুন। এই ফ্রন্টএন্ড কোডটি ব্যাকএন্ডের মতোই একই 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),
      ),
    );
  }
}

ফ্লাটার অ্যাপটি চালান। আপনি যখন ফ্লোটিং অ্যাকশন বাটনটিতে ক্লিক করবেন, অ্যাপটি লোকাল ডার্ট ব্যাকএন্ডকে কল করে নতুন সংখ্যাটি সংগ্রহ করবে এবং UI আপডেট করবে।

৯. ফায়ারবেসে স্থাপন করুন

আপনার ডার্ট ব্যাকএন্ড ডেপ্লয় করতে, নিম্নলিখিত কমান্ডটি চালান:

firebase deploy --only functions

কমান্ডটি চালানোর পর, URL-টি কপি করুন এবং পূর্বে যোগ করা ফ্লাটার অ্যাপের সোর্স কোডে FIREBASE_FUNCTIONS_URL_HERE জায়গায় সেটি প্রতিস্থাপন করুন।

১০. সমস্যা সমাধান

firebase: command not found

নিশ্চিত করুন যে Firebase CLI ইনস্টল করা আছে এবং আপনার PATH আপডেট করা আছে। আপনি npm ব্যবহার করে এটি ইনস্টল করতে পারেন: npm install -g firebase-tools

init ফাংশন টেমপ্লেট থেকে ডার্ট অনুপস্থিত

firebase init functions চালানোর সময় ডার্টকে ডিপ্লয় করার জন্য বিকল্পগুলির একটি তালিকা হিসাবে দেখাতে এবং টেমপ্লেট কোড তৈরি করতে firebase experiments:enable dartfunctions কমান্ডটি চালিয়ে একটি এক্সপেরিমেন্ট ফ্ল্যাগ সেট করতে হবে।

ফাংশন এমুলেটরটি সংযোগ স্থাপন করতে পারছে না।

নিশ্চিত করুন যে আপনি localhost এবং পোর্ট 5001 ব্যবহার করছেন। আপনি যদি একটি অ্যান্ড্রয়েড এমুলেটরে পরীক্ষা করেন, তবে ডিভাইসটি আপনার হোস্ট মেশিনে localhost রিজলভ করে না। 10.0.2.2 ব্যবহার করার জন্য main.dart এ এমুলেটর কনফিগারেশন আপডেট করুন।

শেয়ার করা প্যাকেজটি খুঁজে পাওয়া যায়নি।

functions/pubspec.yaml এ রিলেটিভ পাথটি যাচাই করুন। যদি আপনার ফোল্ডার কাঠামো কোডল্যাবের থেকে ভিন্ন হয়, তাহলে সঠিক ডিরেক্টরি নির্দেশ করার জন্য path: ../packages/shared পরিবর্তন করুন।

আমার কি json_serializable ব্যবহার করার প্রয়োজন আছে?

যদিও এটি কঠোরভাবে বাধ্যতামূলক নয়, json_serializable ব্যবহার করলে fromJson এবং toJson মেথডগুলো ম্যানুয়ালি লেখার কারণে সৃষ্ট ত্রুটি প্রতিরোধ করা যায়। এটি নিশ্চিত করে যে আপনার ফ্রন্টএন্ড এবং ব্যাকএন্ড হুবহু একই ডেটা ফরম্যাট প্রত্যাশা করে।

১১. অভিনন্দন

আপনি সফলভাবে একটি ফুল-স্ট্যাক ডার্ট অ্যাপ্লিকেশন তৈরি করেছেন। আপনার ডেটা মডেলগুলোকে একটি শেয়ার্ড প্যাকেজে রাখার মাধ্যমে, আপনি আপনার সম্পূর্ণ স্ট্যাক জুড়ে একটিমাত্র প্রোগ্রামিং ভাষা ব্যবহার করে এপিআই রেসপন্স এবং ক্লায়েন্ট ইউআই-এর সিঙ্ক্রোনাইজড থাকা নিশ্চিত করেন।