تكامل Firebase

Cloud Functions — الدوال القابلة للاستدعاء من Flutter

16 دقيقة الدرس 10 من 13

Cloud Functions — الدوال القابلة للاستدعاء من Flutter

تتيح لك Firebase Cloud Functions تشغيل منطق آمن على جانب الخادم دون إدارة البنية التحتية. دوال HTTPS القابلة للاستدعاء (Callable) هي نوع خاص يمكن استدعاؤه مباشرةً من تطبيق Flutter باستخدام SDK مكتوبة بأنواع محددة — فهي تتولى تلقائياً سياق المصادقة والتسلسل ونشر الأخطاء البنيوية. في هذا الدرس ستنشر دالة Node.js قابلة للاستدعاء وتستدعيها من Dart.

ما الذي يجعل الدوال القابلة للاستدعاء مختلفة؟

على عكس دوال HTTPS العادية التي تُطلَق عبر رابط URL، تتواصل الدوال القابلة للاستدعاء عبر Firebase Functions SDK. يمنحك هذا عدة مزايا:

  • يُضمّن SDK تلقائياً رمز المعرّف (ID token) للمستخدم المسجل دخوله، لذا يكون context.auth متاحاً دائماً على الخادم.
  • الأخطاء المُرمية كـ HttpsError على الخادم تظهر كـ FirebaseFunctionsException مكتوبة بنوع محدد على العميل.
  • لا حاجة لترميز JSON يدوياً — تمرر Map وتتلقى Map.
  • تعمل بسلاسة مع Firebase Local Emulator Suite للتطوير المحلي.
ملاحظة: تتطلب الدوال القابلة للاستدعاء حزمة Flutter firebase_functions و Node.js 18+ (أو 20+) على جانب الخادم. يُنشر وقت تشغيل الخادم عبر Firebase CLI.

الخطوة 1 — كتابة دالة Node.js القابلة للاستدعاء

داخل ملف functions/index.js (أو src/index.ts) لمشروع Firebase، عرّف الدالة القابلة للاستدعاء باستخدام onCall:

functions/index.js — الدالة القابلة للاستدعاء على جانب الخادم

// Node.js 18+ / Firebase Functions v2
const { onCall, HttpsError } = require('firebase-functions/v2/https');
const { getMessaging } = require('firebase-admin/messaging');
const admin = require('firebase-admin');

admin.initializeApp();

exports.sendWelcomeNotification = onCall(async (request) => {
  // 1. فرض المصادقة
  if (!request.auth) {
    throw new HttpsError(
      'unauthenticated',
      'You must be signed in to call this function.'
    );
  }

  const { deviceToken, userName } = request.data;

  // 2. التحقق من صحة المدخلات
  if (!deviceToken || typeof deviceToken !== 'string') {
    throw new HttpsError('invalid-argument', 'deviceToken is required.');
  }

  // 3. تنفيذ المنطق على جانب الخادم (إرسال إشعار FCM)
  const message = {
    token: deviceToken,
    notification: {
      title: `Welcome, ${userName ?? 'Friend'}!`,
      body: 'Thanks for joining. Explore the app now.',
    },
    data: { screen: 'home' },
  };

  const messageId = await getMessaging().send(message);

  // 4. إرجاع استجابة مكتوبة بنوع محدد
  return { success: true, messageId };
});

انشر الدالة باستخدام Firebase CLI:

الطرفية — النشر على Firebase

# من جذر مشروع Firebase
firebase deploy --only functions:sendWelcomeNotification

الخطوة 2 — إضافة اعتماد Flutter

أضف firebase_functions إلى pubspec.yaml:

pubspec.yaml

dependencies:
  firebase_core: ^3.6.0
  firebase_auth: ^5.3.0
  firebase_functions: ^5.1.0

الخطوة 3 — استدعاء الدالة القابلة للاستدعاء من Flutter

استخدم FirebaseFunctions.instance.httpsCallable() للحصول على مرجع، ثم استدعه بخريطة بيانات. استخدم دائماً await داخل try/catch للتعامل مع FirebaseFunctionsException:

lib/services/functions_service.dart

import 'package:cloud_functions/cloud_functions.dart';

class FunctionsService {
  final FirebaseFunctions _functions = FirebaseFunctions.instance;

  /// تستدعي Cloud Function باسم [sendWelcomeNotification].
  /// تُرجع messageId عند النجاح أو ترمي [FirebaseFunctionsException].
  Future<String> sendWelcomeNotification({
    required String deviceToken,
    required String userName,
  }) async {
    final callable = _functions.httpsCallable('sendWelcomeNotification');

    try {
      final HttpsCallableResult result = await callable.call<Map<String, dynamic>>({
        'deviceToken': deviceToken,
        'userName': userName,
      });

      final data = Map<String, dynamic>.from(result.data as Map);
      return data['messageId'] as String;
    } on FirebaseFunctionsException catch (e) {
      // خطأ مكتوب بنوع محدد من HttpsError المرمية على الخادم
      throw Exception('[${e.code}] ${e.message}');
    }
  }
}

الخطوة 4 — استدعاء الخدمة من ودجت

lib/screens/home_screen.dart — تشغيل الدالة

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  bool _loading = false;
  String? _result;

  Future<void> _triggerNotification() async {
    setState(() { _loading = true; _result = null; });

    try {
      final service = FunctionsService();
      final messageId = await service.sendWelcomeNotification(
        deviceToken: 'DEVICE_FCM_TOKEN_HERE',
        userName: 'Edrees',
      );
      setState(() { _result = 'Sent! Message ID: $messageId'; });
    } catch (e) {
      setState(() { _result = 'Error: $e'; });
    } finally {
      setState(() { _loading = false; });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Cloud Functions Demo')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ElevatedButton(
              onPressed: _loading ? null : _triggerNotification,
              child: _loading
                  ? const CircularProgressIndicator()
                  : const Text('Send Welcome Notification'),
            ),
            if (_result != null) ...[
              const SizedBox(height: 16),
              Text(_result!, textAlign: TextAlign.center),
            ],
          ],
        ),
      ),
    );
  }
}

استخدام المحاكي المحلي

أثناء التطوير، وجّه SDK إلى محاكي Functions المحلي بدلاً من بيئة الإنتاج:

main.dart — الاتصال بالمحاكي

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // فقط في وضع التصحيح / المحاكي
  if (kDebugMode) {
    FirebaseFunctions.instance.useFunctionsEmulator('localhost', 5001);
  }

  runApp(const MyApp());
}
نصيحة: شغّل firebase emulators:start --only functions واضبط useFunctionsEmulator حتى تتمكن من التكرار دون نشر. التغييرات على كود Node.js تصبح سارية بعد إعادة تشغيل المحاكي.

مرجع معالجة الأخطاء

يرمي الخادم HttpsError بسلسلة رمز حالة. على العميل، يُعيّن FirebaseFunctionsException.code إلى نفس السلسلة:

  • unauthenticated — المستدعي غير مسجل الدخول
  • permission-denied — المستدعي يفتقر إلى الدور/المطالبة المطلوبة
  • invalid-argument — بيانات مدخلة مشوّهة
  • not-found — المورد المطلوب غير موجود
  • internal — استثناء غير معالَج على جانب الخادم
تحذير: لا ترمِ JavaScript Error خاماً من دالة قابلة للاستدعاء — ستظهر كـ internal بدون رسالة. استخدم دائماً new HttpsError(code, message) لمنح العميل معلومات خطأ قابلة للتنفيذ.

الملخص

تربط الدوال القابلة للاستدعاء Flutter بوقت تشغيل جانب الخادم في Firebase بشكل نظيف: يتولى SDK حقن رمز المصادقة والتسلسل ونشر الأخطاء المكتوبة بأنواع محددة. انشر بـ firebase deploy --only functions، استدعِ بـ httpsCallable().call(data)، وامسك FirebaseFunctionsException للتعامل البنيوي مع الأخطاء. استخدم المحاكي المحلي أثناء التطوير للتكرار بسرعة دون تكبّد تكاليف استدعاء Cloud Functions.