المصادقة: التسجيل وتسجيل الدخول واستمرارية الجلسة
المصادقة: التسجيل وتسجيل الدخول واستمرارية الجلسة
يتطلب تطبيق Flutter الإنتاجي في الغالب طبقة مصادقة آمنة وموثوقة. في هذا الدرس ستقوم بتوصيل Firebase Authentication (بريد إلكتروني وكلمة مرور) بمشروعك التطبيقي باستخدام بنية repository + use-case نظيفة، وستحافظ على الجلسة المصادقة حتى يبقى المستخدمون مسجلين دخولهم عبر إعادة تشغيل التطبيق، وستعرض رسائل خطأ ذات معنى في واجهة المستخدم.
نظرة عامة على البنية
بدلاً من استدعاء أساليب Firebase SDK مباشرةً من الودجات، نفصل المخاوف إلى ثلاث طبقات:
- AuthRepository — يجرد مصدر البيانات (Firebase). يمكن استبداله أو محاكاته في الاختبارات.
- حالات الاستخدام (Use Cases) — أغلفة منطق أعمال رفيعة:
SignUpUseCase،SignInUseCase،SignOutUseCase،GetCurrentUserUseCase. - AuthNotifier (Riverpod) — حامل الحالة الذي يستدعي حالات الاستخدام ويكشف
AuthStateلشجرة الودجات.
إعداد AuthRepository
عرّف واجهة مجردة وتطبيق Firebase ملموس:
auth_repository.dart
import 'package:firebase_auth/firebase_auth.dart';
// العقد المجرد — الودجات تعتمد على هذا، وليس على الفئة الملموسة
abstract class AuthRepository {
Stream<User?> get authStateChanges;
Future<User> signUp({required String email, required String password});
Future<User> signIn({required String email, required String password});
Future<void> signOut();
User? get currentUser;
}
// التطبيق الملموس لـ Firebase
class FirebaseAuthRepository implements AuthRepository {
final FirebaseAuth _auth;
FirebaseAuthRepository({FirebaseAuth? auth})
: _auth = auth ?? FirebaseAuth.instance;
@override
Stream<User?> get authStateChanges => _auth.authStateChanges();
@override
User? get currentUser => _auth.currentUser;
@override
Future<User> signUp({
required String email,
required String password,
}) async {
final credential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
return credential.user!;
}
@override
Future<User> signIn({
required String email,
required String password,
}) async {
final credential = await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
return credential.user!;
}
@override
Future<void> signOut() => _auth.signOut();
}
FirebaseAuth عبر المنشئ (مع قيمة افتراضية هي FirebaseAuth.instance) يجعل اختبار الوحدة سهلاً — مرر نموذجاً مزيفاً في الاختبارات دون لمس كود الإنتاج.حالات الاستخدام
كل حالة استخدام هي فئة ذات مسؤولية واحدة مع طريقة call() واحدة. تترجم استثناءات Firebase الخام إلى أخطاء نطاق مألوفة:
sign_in_use_case.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'auth_repository.dart';
class SignInUseCase {
final AuthRepository _repo;
SignInUseCase(this._repo);
Future<User> call({
required String email,
required String password,
}) async {
try {
return await _repo.signIn(email: email, password: password);
} on FirebaseAuthException catch (e) {
// تعيين أكواد FirebaseAuthException إلى رسائل مقروءة
throw _mapError(e.code);
}
}
String _mapError(String code) {
switch (code) {
case 'user-not-found':
return 'لم يُعثر على حساب لعنوان البريد الإلكتروني هذا.';
case 'wrong-password':
return 'كلمة المرور غير صحيحة. يرجى المحاولة مرة أخرى.';
case 'invalid-email':
return 'عنوان البريد الإلكتروني غير صالح.';
case 'user-disabled':
return 'تم تعطيل هذا الحساب.';
case 'too-many-requests':
return 'محاولات كثيرة جداً. يرجى الانتظار والمحاولة مجدداً.';
default:
return 'فشل تسجيل الدخول. يرجى المحاولة مجدداً.';
}
}
}
استمرارية الجلسة
يحافظ Firebase SDK على رمز المصادقة في التخزين الآمن للجهاز افتراضياً على الأجهزة المحمولة. عند تشغيل التطبيق، تحتاج فقط للتحقق من authStateChanges() — إذا وُجد رمز وكان لا يزال صالحاً، يبث Firebase كائن User فوراً قبل اكتمال أي اتصال بالشبكة. اربط هذا بـ StreamProvider من Riverpod:
auth_providers.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'auth_repository.dart';
import 'firebase_auth_repository.dart';
// توفير المستودع (تجاوز في الاختبارات)
final authRepositoryProvider = Provider<AuthRepository>(
(ref) => FirebaseAuthRepository(),
);
// استمرارية الجلسة القائمة على البث: يبث User? عند كل تغيير في المصادقة
final authStateProvider = StreamProvider<User?>((ref) {
return ref.watch(authRepositoryProvider).authStateChanges;
});
// Notifier لإجراءات التسجيل وتسجيل الدخول والخروج
class AuthNotifier extends AsyncNotifier<User?> {
late AuthRepository _repo;
@override
Future<User?> build() async {
_repo = ref.watch(authRepositoryProvider);
return _repo.currentUser;
}
Future<void> signIn(String email, String password) async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => SignInUseCase(_repo)(email: email, password: password),
);
}
Future<void> signUp(String email, String password) async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => SignUpUseCase(_repo)(email: email, password: password),
);
}
Future<void> signOut() async {
state = const AsyncLoading();
await _repo.signOut();
state = const AsyncData(null);
}
}
final authNotifierProvider =
AsyncNotifierProvider<AuthNotifier, User?>(AuthNotifier.new);
التوجيه بناءً على حالة المصادقة
استمع إلى authStateProvider في جهاز التوجيه الخاص بك (مثلاً go_router) لإعادة توجيه المستخدمين غير المصادقين إلى شاشة تسجيل الدخول وتجاوزها للمصادقين:
- استخدم استدعاء
redirectفيGoRouterيقرأref.watch(authStateProvider). - عندما يبث البث
null، أعد التوجيه إلى/login. - عندما يبث
User، أعد التوجيه إلى/home. - أثناء تحميل البث (
AsyncLoading)، اعرض شاشة البداية وأعدnullلإيقاف التنقل.
معالجة الأخطاء في واجهة المستخدم
اعرض الأخطاء باستخدام AsyncValue.when على حالة notifier. يحمل AsyncError رسالة الاستثناء المعيّن من حالة الاستخدام، جاهزة للعرض في SnackBar أو نص النموذج المضمّن:
- استخدم
ref.listen(authNotifierProvider, ...)للتفاعل مع تغييرات الحالة دون إعادة بناء الودجت بالكامل. - عند
AsyncError، اعرضScaffoldMessenger.of(context).showSnackBar(...)معerror.toString(). - احتفظ بحقول النموذج مملوءة عند الخطأ حتى يتمكن المستخدمون من تصحيح الخطأ فقط.
- عطّل زر الإرسال أثناء
AsyncLoadingلمنع التقديم المزدوج.
الملخص
لديك الآن تدفق مصادقة كامل وجاهز للإنتاج. يعزل المستودع Firebase عن منطق الأعمال. تترجم حالات الاستخدام استثناءات SDK إلى رسائل مقروءة للمستخدم. يُغذّي StreamProvider استمرارية الرمز المدمجة في Firebase تلقائياً إلى شجرة الودجات. ويحرس توجيه go_router كل مسار دون تكرار عمليات التحقق من المصادقة في شاشات فردية. تتوسع هذه البنية بنظافة عند إضافة تسجيل الدخول الاجتماعي أو القياسات الحيوية أو خادم خلفي مختلف لاحقاً.