إدارة حالة المصادقة في Firebase والثبات
إدارة حالة المصادقة في Firebase والثبات
بعد تسجيل دخول المستخدم عبر Firebase Authentication، يجب أن يعرف تطبيقك بحالة تسجيل الدخول هذه في كل مكان — عبر كل شاشة — وأن تستمر بعد إعادة تشغيل التطبيق. يحقق Firebase ذلك عبر آليتين متكاملتين: تدفق حالة المصادقة (authStateChanges()) ورمز هوية (ID token) يُخزَّن تلقائياً ويُحدَّث بصمت. فهم الاثنتين معاً يُتيح لك بناء بوابات مسار محكمة وتفاعلية دون الحاجة إلى أي استطلاع دوري للخادم.
تدفق حالة المصادقة: authStateChanges()
FirebaseAuth.instance.authStateChanges() يُعيد Stream<User?> يُصدر حدثاً جديداً في كل مرة يتغير فيها المستخدم المسجَّل دخوله. الأحداث المحتملة هي:
- تشغيل التطبيق — يُصدر كائن
Userالمحفوظ إذا كان المستخدم مسجَّلاً مسبقاً، أوnullإذا لم يكن كذلك. - تسجيل الدخول — يُصدر كائن
Userالجديد فوراً بعد نجاح استدعاء تسجيل الدخول. - تسجيل الخروج — يُصدر
nullعند استدعاءFirebaseAuth.instance.signOut(). - إلغاء الرمز أو حذف الحساب — يُصدر
nullإذا أبطل الخادم الجلسة.
idTokenChanges() الذي يُصدر حدثاً عند كل تحديث لرمز الهوية (مرة كل ساعة تقريباً). استخدم authStateChanges() لبوابات المسار؛ واستخدم idTokenChanges() فقط حين تحتاج التفاعل مع تحديثات الرمز تحديداً، كإعادة تعيين رمز Bearer جديد في عميل HTTP.بوابة المسار مع StreamBuilder
StreamBuilder هو الودجت المثالي في Flutter لاستهلاك تدفق وإعادة البناء بصورة تفاعلية. إحاطة قرار التوجيه الرئيسي داخل StreamBuilder يستمع إلى authStateChanges() تُعطيك بوابة تصريحية واحدة: المستخدمون غير المصادَق عليهم يرون دائماً تدفق المصادقة؛ والمصادَق عليهم يرون التطبيق دائماً.
بوابة المسار مع StreamBuilder — main.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class AppRouter extends StatelessWidget {
const AppRouter({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<User?> snapshot) {
// أثناء تحقق Firebase من التخزين المحلي، اعرض شاشة تحميل
if (snapshot.connectionState == ConnectionState.waiting) {
return const MaterialApp(
home: Scaffold(
body: Center(child: CircularProgressIndicator()),
),
);
}
final User? user = snapshot.data;
if (user == null) {
// لا يوجد مستخدم مصادَق — اعرض تسجيل الدخول
return const MaterialApp(home: LoginScreen());
}
// مصادَق عليه — اعرض التطبيق الرئيسي
return const MaterialApp(home: HomeScreen());
},
);
}
}
StreamBuilder فوق MaterialApp (أو ضمِّنه في الودجت الرئيسي) حتى يُستبدل كامل مكدس التنقل عند تغير حالة المصادقة. هذا يمنع بقاء شاشات قديمة في مكدس الملاح بعد تسجيل الخروج.التحديث التلقائي للرمز والثبات
تنتهي صلاحية رموز هوية Firebase بعد ساعة واحدة. يُجدِّد FlutterFire SDK هذه الرموز بصمت في الخلفية قبل انتهاء صلاحيتها — لا تحتاج أبداً لاستدعاء طريقة تحديث يدوياً. السلوكيات الرئيسية هي:
- يُخزِّن SDK بيانات الاعتماد في تخزين النظام الآمن (Keychain في iOS/macOS، وEncryptedSharedPreferences في Android، وIndexedDB في الويب).
- عند تشغيل التطبيق مرة أخرى، يقرأ SDK من الذاكرة المحلية ويُصدر كائن
UserعبرauthStateChanges()قبل اكتمال أي استدعاء شبكي. - ثم يُجري تحديثاً في الخلفية للتحقق من صلاحية الرمز والحصول على رمز جديد.
- إذا فشل التحديث (رمز مُلغى، حساب محذوف، انقطاع الشبكة لفترة طويلة)، يُصدر التدفق
nullويُسجَّل خروج المستخدم.
إجبار تحديث الرمز وقراءة المطالبات
Future<void> refreshAndReadToken() async {
final User? user = FirebaseAuth.instance.currentUser;
if (user == null) return;
// إجبار تحديث رمز الهوية (يتجاوز تخزين الساعة)
final String idToken = await user.getIdToken(true);
// قراءة نتيجة الرمز المفكوك للحصول على المطالبات المخصصة
final IdTokenResult tokenResult = await user.getIdTokenResult(true);
final Map<String, dynamic>? claims = tokenResult.claims;
final bool isAdmin = claims?['admin'] == true;
debugPrint('تم تحديث الرمز. هل مسؤول: \$isAdmin');
}
// الاستماع إلى تغيرات الرمز (يُطلق عند كل تحديث تلقائي أيضاً)
void listenToTokenChanges() {
FirebaseAuth.instance.idTokenChanges().listen((User? user) {
if (user != null) {
debugPrint('تغيَّر الرمز لـ: \${user.email}');
}
});
}
التعامل الصحيح مع حالة الانتظار
عند اشتراك StreamBuilder لأول مرة، تكون حالة الاتصال ConnectionState.waiting وقيمة snapshot.data هي null. هذه النافذة القصيرة ليست مساوية لـ"لا يوجد مستخدم مسجَّل" — فالـSDK لم يقرأ من التخزين المحلي بعد. عدم التعامل مع هذه الحالة بشكل صحيح يتسبب في وميض شاشة تسجيل الدخول عند كل تشغيل للمستخدمين المصادَق عليهم. تحقق دائماً من snapshot.connectionState قبل قراءة snapshot.data.
snapshot.data == null حين تكون snapshot.connectionState == ConnectionState.waiting على أنها "تسجيل خروج". اعرض دائماً مؤشر تحميل خلال مرحلة الانتظار. تجاهل هذا الفحص يُسبب إعادة توجيه غير مرغوب فيها إلى شاشة تسجيل الدخول عند كل بدء تشغيل بارد.أوضاع الثبات (الويب)
في Flutter Web، يمكنك التحكم في طريقة حفظ حالة المصادقة باستخدام FirebaseAuth.instance.setPersistence(). الأوضاع الثلاثة هي:
- Persistence.LOCAL (الافتراضي) — الحالة تبقى بعد إعادة تشغيل المتصفح؛ مخزَّنة في IndexedDB.
- Persistence.SESSION — الحالة تبقى بعد إعادة تحميل الصفحة لكن لا تنتقل إلى تبويبات جديدة أو إعادة تشغيل المتصفح.
- Persistence.NONE — الحالة في الذاكرة فقط؛ تُفقَد عند إغلاق الصفحة. مفيد للحواسيب العامة أو المشتركة.
LOCAL ولا يمكن تغييره عبر الـSDK — يتولى نظام التشغيل إدارة تخزين بيانات الاعتماد. واجهة برمجة setPersistence() ميزة خاصة بالويب فحسب.ملخص
يُوفِّر authStateChanges() تدفقاً تفاعلياً يُشغِّل بوابات المسار في تطبيقات Flutter. إحاطة منطق التوجيه في StreamBuilder يستمع لهذا التدفق يُعطيك إعادة توجيه تصريحية وتلقائية بناءً على حالة المصادقة. يُثبِّت Firebase بيانات الاعتماد أصلياً، ويُحدِّث رموز الهوية تلقائياً كل ساعة، ويُصدر null إذا أصبحت الجلسة غير صالحة — كل ذلك دون أي إدارة يدوية للرمز من جانبك. التفصيل الحرج الوحيد هو التعامل الصحيح مع مرحلة الانتظار الأولية ConnectionState.waiting لتجنب وميض شاشة تسجيل الدخول عند كل تشغيل.