تسجيل الدخول في Flutter
في هذا الدليل، سنقوم ببناء مسار تسجيل دخول في Flutter باستخدام مكتبة Bloc.

المواضيع الرئيسية
Section titled “المواضيع الرئيسية”- BlocProvider، ويدجت Flutter يقوم بتوفير bloc للأطفال (العناصر الفرعية) الخاصة به.
- إضافة الأحداث باستخدام context.read.
- تجنب إعادة البناء غير الضرورية باستخدام Equatable.
- RepositoryProvider، ويدجت Flutter يقوم بتوفير repository للأطفال الخاصة به.
- BlocListener، ويدجت Flutter يقوم بتنفيذ كود المستمع استجابة لتغيرات الحالة في الـ bloc.
- تحديث واجهة المستخدم بناءً على جزء من حالة الـ bloc باستخدام context.select.
إعداد المشروع
Section titled “إعداد المشروع”سنبدأ بإنشاء مشروع Flutter جديد تمامًا
flutter create flutter_loginبعد ذلك، يمكننا تثبيت جميع الاعتمادات الخاصة بنا
flutter pub getمستودع المصادقة (Authentication Repository)
Section titled “مستودع المصادقة (Authentication Repository)”أول شيء سنقوم به هو إنشاء حزمة authentication_repository والتي ستكون مسؤولة عن
إدارة مجال المصادقة.
سنبدأ بإنشاء مجلد packages/authentication_repository في جذر المشروع والذي
سيحتوي على جميع الحزم الداخلية.
على مستوى الهيكلية العامة، يجب أن يبدو هيكل الدليل كما يلي:
├── android├── ios├── lib├── packages│ └── authentication_repository└── testبعد ذلك، يمكننا إنشاء ملف pubspec.yaml لحزمة authentication_repository:
name: authentication_repositorydescription: Dart package which manages the authentication domain.publish_to: none
environment: sdk: ">=3.10.0 <4.0.0"بعد ذلك، نحتاج إلى تنفيذ الفئة AuthenticationRepository نفسها والتي ستكون في
packages/authentication_repository/lib/src/authentication_repository.dart.
import 'dart:async';
enum AuthenticationStatus { unknown, authenticated, unauthenticated }
class AuthenticationRepository { final _controller = StreamController<AuthenticationStatus>();
Stream<AuthenticationStatus> get status async* { await Future<void>.delayed(const Duration(seconds: 1)); yield AuthenticationStatus.unauthenticated; yield* _controller.stream; }
Future<void> logIn({ required String username, required String password, }) async { await Future.delayed( const Duration(milliseconds: 300), () => _controller.add(AuthenticationStatus.authenticated), ); }
void logOut() { _controller.add(AuthenticationStatus.unauthenticated); }
void dispose() => _controller.close();}توفر AuthenticationRepository تدفق Stream من تحديثات AuthenticationStatus
والذي سيتم استخدامه لإبلاغ التطبيق عندما يقوم المستخدم بتسجيل الدخول أو الخروج.
بالإضافة إلى ذلك، هناك طرق logIn و logOut مبسطة للشرح، لكنها يمكن بسهولة
توسيعها للمصادقة باستخدام FirebaseAuth مثلاً أو أي مزود مصادقة آخر.
أخيرًا، نحتاج إلى إنشاء الملف
packages/authentication_repository/lib/authentication_repository.dart والذي
سيحتوي على الصادرات العامة (public exports):
export 'src/authentication_repository.dart';هذا كل شيء بالنسبة لـ AuthenticationRepository، في الخطوة التالية سنعمل على
UserRepository.
مستودع المستخدم
Section titled “مستودع المستخدم”تمامًا كما فعلنا مع AuthenticationRepository، سنقوم بإنشاء حزمة
user_repository داخل مجلد packages.
├── android├── ios├── lib├── packages│ ├── authentication_repository│ └── user_repository└── testبعد ذلك، سنقوم بإنشاء ملف pubspec.yaml الخاص بـ user_repository:
name: user_repositorydescription: Dart package which manages the user domain.publish_to: none
environment: sdk: ">=3.10.0 <4.0.0"
dependencies: equatable: ^2.0.0 uuid: ^3.0.0حزمة user_repository ستكون مسؤولة عن نطاق المستخدم وستوفر واجهات برمجية (APIs)
للتفاعل مع المستخدم الحالي.
أول شيء سنحدده هو نموذج المستخدم في الملف
packages/user_repository/lib/src/models/user.dart:
import 'package:equatable/equatable.dart';
class User extends Equatable { const User(this.id);
final String id;
@override List<Object> get props => [id];
static const empty = User('-');}لأجل البساطة، يحتوي المستخدم على خاصية id فقط، لكن في التطبيق العملي قد تكون
هناك خصائص إضافية مثل firstName، lastName، avatarUrl وغيرها…
بعد ذلك، يمكننا إنشاء ملف models.dart داخل
packages/user_repository/lib/src/models ليقوم بتصدير كل النماذج، بحيث يمكننا
استخدام استيراد واحد لاستدعاء نماذج متعددة.
export 'user.dart';الآن بعد تعريف النماذج، يمكننا تنفيذ فئة UserRepository في
packages/user_repository/lib/src/user_repository.dart.
import 'dart:async';
import 'package:user_repository/src/models/models.dart';import 'package:uuid/uuid.dart';
class UserRepository { User? _user;
Future<User?> getUser() async { if (_user != null) return _user; return Future.delayed( const Duration(milliseconds: 300), () => _user = User(const Uuid().v4()), ); }}في هذا المثال البسيط، توفّر UserRepository دالة واحدة فقط هي getUser والتي
تسترجع المستخدم الحالي. نحن هنا نقوم بعمل تمثيل تجريبي (stubbing)، لكن في
التطبيق الفعلي ستكون هذه الدالة هي التي تستعلم المستخدم الحالي من الخادم
(backend).
لقد اقتربنا من الانتهاء من حزمة user_repository، والشيء الوحيد المتبقي هو
إنشاء ملف user_repository.dart في المسار packages/user_repository/lib والذي
يعرّف الصادرات العامة (public exports):
export 'src/models/models.dart';export 'src/user_repository.dart';الآن بعد أن أتممنا حزمتي authentication_repository و user_repository، يمكننا
الانتقال للتركيز على تطبيق Flutter.
تثبيت التبعيات
Section titled “تثبيت التبعيات”لنبدأ بتحديث ملف pubspec.yaml المُولد في جذر مشروعنا:
name: flutter_logindescription: A new Flutter project.version: 1.0.0+1publish_to: none
environment: sdk: ">=3.10.0 <4.0.0"
dependencies: authentication_repository: path: packages/authentication_repository bloc: ^9.0.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 formz: ^0.8.0 user_repository: path: packages/user_repository
dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0
flutter: uses-material-design: trueيمكننا تثبيت التبعيات عن طريق تشغيل الأمر:
flutter pub getAuthentication Bloc
Section titled “Authentication Bloc”سيتولى AuthenticationBloc مسؤولية الاستجابة لتغيرات حالة المصادقة (التي يعرضها
AuthenticationRepository) وسيصدر حالات يمكننا التفاعل معها في طبقة العرض.
تم تنفيذ AuthenticationBloc داخل مجلد lib/authentication لأننا نعتبر
المصادقة كميزة في طبقة التطبيق الخاصة بنا.
├── lib│ ├── app.dart│ ├── authentication│ │ ├── authentication.dart│ │ └── bloc│ │ ├── authentication_bloc.dart│ │ ├── authentication_event.dart│ │ └── authentication_state.dart│ ├── main.dartauthentication_event.dart
Section titled “authentication_event.dart”تمثل مثيلات AuthenticationEvent المدخلات إلى AuthenticationBloc، وسيتم
معالجتها لاستخدامها في إصدار مثيلات جديدة من AuthenticationState.
في هذا التطبيق، سيستجيب AuthenticationBloc لحدثين مختلفين:
AuthenticationSubscriptionRequested: الحدث الأولي الذي يُبلغ الـ bloc بالاشتراك في تدفقAuthenticationStatus.AuthenticationLogoutPressed: يُعلم الـ bloc بحدوث تسجيل خروج من قِبل المستخدم.
part of 'authentication_bloc.dart';
sealed class AuthenticationEvent { const AuthenticationEvent();}
final class AuthenticationSubscriptionRequested extends AuthenticationEvent {}
final class AuthenticationLogoutPressed extends AuthenticationEvent {}لننتقل الآن للنظر في AuthenticationState.
authentication_state.dart
Section titled “authentication_state.dart”تمثل مثيلات AuthenticationState نواتج AuthenticationBloc وسيتم استهلاكها من
قبل طبقة العرض.
لفئة AuthenticationState ثلاث مُنشئين مسمّين:
-
AuthenticationState.unknown(): الحالة الافتراضية التي تدل على أن الـ bloc لا يعرف بعد ما إذا كان المستخدم الحالي مصدقًا أم لا. -
AuthenticationState.authenticated(): الحالة التي تشير إلى أن المستخدم حالياً مصدق عليه. -
AuthenticationState.unauthenticated(): الحالة التي تدل على أن المستخدم حالياً غير مصدق عليه.
part of 'authentication_bloc.dart';
class AuthenticationState extends Equatable { const AuthenticationState._({ this.status = AuthenticationStatus.unknown, this.user = User.empty, });
const AuthenticationState.unknown() : this._();
const AuthenticationState.authenticated(User user) : this._(status: AuthenticationStatus.authenticated, user: user);
const AuthenticationState.unauthenticated() : this._(status: AuthenticationStatus.unauthenticated);
final AuthenticationStatus status; final User user;
@override List<Object> get props => [status, user];}بعد أن اطلعنا على تنفيذ AuthenticationEvent وAuthenticationState، دعونا نلقي
نظرة على AuthenticationBloc.
authentication_bloc.dart
Section titled “authentication_bloc.dart”يدير AuthenticationBloc حالة المصادقة في التطبيق، والتي تُستخدم لاتخاذ قرارات
مثل بدء المستخدم في صفحة تسجيل الدخول أو الصفحة الرئيسية.
import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart';import 'package:bloc/bloc.dart';import 'package:equatable/equatable.dart';import 'package:user_repository/user_repository.dart';
part 'authentication_event.dart';part 'authentication_state.dart';
class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> { AuthenticationBloc({ required AuthenticationRepository authenticationRepository, required UserRepository userRepository, }) : _authenticationRepository = authenticationRepository, _userRepository = userRepository, super(const AuthenticationState.unknown()) { on<AuthenticationSubscriptionRequested>(_onSubscriptionRequested); on<AuthenticationLogoutPressed>(_onLogoutPressed); }
final AuthenticationRepository _authenticationRepository; final UserRepository _userRepository;
Future<void> _onSubscriptionRequested( AuthenticationSubscriptionRequested event, Emitter<AuthenticationState> emit, ) { return emit.onEach( _authenticationRepository.status, onData: (status) async { switch (status) { case AuthenticationStatus.unauthenticated: return emit(const AuthenticationState.unauthenticated()); case AuthenticationStatus.authenticated: final user = await _tryGetUser(); return emit( user != null ? AuthenticationState.authenticated(user) : const AuthenticationState.unauthenticated(), ); case AuthenticationStatus.unknown: return emit(const AuthenticationState.unknown()); } }, onError: addError, ); }
void _onLogoutPressed( AuthenticationLogoutPressed event, Emitter<AuthenticationState> emit, ) { _authenticationRepository.logOut(); }
Future<User?> _tryGetUser() async { try { final user = await _userRepository.getUser(); return user; } catch (_) { return null; } }}يعتمد AuthenticationBloc على كل من AuthenticationRepository
وUserRepository، ويحدد الحالة الابتدائية كـ AuthenticationState.unknown().
في مُنشئ الـ bloc، يتم ربط فئات الأحداث المشتقة من AuthenticationEvent
بمعالجيها المناسبين.
في معالج الحدث _onSubscriptionRequested، يستخدم AuthenticationBloc
emit.onEach للاشتراك في تدفق status الخاص بـ AuthenticationRepository
وإصدار حالة استجابةً لكل حالة من AuthenticationStatus.
emit.onEach يقوم بإنشاء اشتراك داخلي في التدفق ويتولى إلغاءه تلقائيًا عند
إغلاق AuthenticationBloc أو تدفق status.
إذا أصدر تدفق status خطأً، فإن addError يمرر الخطأ مع stackTrace لأي
BlocObserver مستمع.
عندما يصدر تدفق status الحالة AuthenticationStatus.unknown أو
unauthenticated، يتم إصدار الحالة المطابقة في AuthenticationState.
عندما يُصدر التدفق AuthenticationStatus.authenticated، يقوم
AuthenticationBloc باستعلام بيانات المستخدم عبر UserRepository.
main.dart
Section titled “main.dart”بعد ذلك، يمكننا استبدال ملف main.dart الافتراضي بالنص التالي:
import 'package:flutter/widgets.dart';import 'package:flutter_login/app.dart';
void main() => runApp(const App());التطبيق
Section titled “التطبيق”app.dart يحتوي على ويدجت الجذر App الخاص بالتطبيق بأكمله.
import 'package:authentication_repository/authentication_repository.dart';import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_login/authentication/authentication.dart';import 'package:flutter_login/home/home.dart';import 'package:flutter_login/login/login.dart';import 'package:flutter_login/splash/splash.dart';import 'package:user_repository/user_repository.dart';
class App extends StatelessWidget { const App({super.key});
@override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (_) => AuthenticationRepository(), dispose: (repository) => repository.dispose(), ), RepositoryProvider(create: (_) => UserRepository()), ], child: BlocProvider( lazy: false, create: (context) => AuthenticationBloc( authenticationRepository: context.read<AuthenticationRepository>(), userRepository: context.read<UserRepository>(), )..add(AuthenticationSubscriptionRequested()), child: const AppView(), ), ); }}
class AppView extends StatefulWidget { const AppView({super.key});
@override State<AppView> createState() => _AppViewState();}
class _AppViewState extends State<AppView> { final _navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => _navigatorKey.currentState!;
@override Widget build(BuildContext context) { return MaterialApp( navigatorKey: _navigatorKey, builder: (context, child) { return BlocListener<AuthenticationBloc, AuthenticationState>( listener: (context, state) { switch (state.status) { case AuthenticationStatus.authenticated: _navigator.pushAndRemoveUntil<void>( HomePage.route(), (route) => false, ); case AuthenticationStatus.unauthenticated: _navigator.pushAndRemoveUntil<void>( LoginPage.route(), (route) => false, ); case AuthenticationStatus.unknown: break; } }, child: child, ); }, onGenerateRoute: (_) => SplashPage.route(), ); }}افتراضيًا، BlocProvider يكون كسولًا (lazy) ولا يستدعي create إلا عند أول
وصول إلى الـ Bloc. بما أن AuthenticationBloc يجب أن يشترك دائمًا في تيار
AuthenticationStatus فورًا (عبر الحدث AuthenticationSubscriptionRequested)،
يمكننا تجاوز هذا السلوك صراحةً عن طريق ضبط lazy: false.
AppView هو StatefulWidget لأنه يحتفظ بـ GlobalKey الذي يُستخدم للوصول إلى
حالة الـ Navigator. بشكل افتراضي، يقوم AppView بعرض SplashPage (التي
سنراها لاحقًا) ويستخدم BlocListener للتنقل بين الصفحات المختلفة بناءً على
التغيرات في حالة AuthenticationState.
شاشة البداية
Section titled “شاشة البداية”ميزة شاشة البداية ستتكون من عرض بسيط يُعرض فور إطلاق التطبيق بينما يحدد التطبيق ما إذا كان المستخدم مصادقًا عليه.
lib└── splash ├── splash.dart └── view └── splash_page.dartimport 'package:flutter/material.dart';
class SplashPage extends StatelessWidget { const SplashPage({super.key});
static Route<void> route() { return MaterialPageRoute<void>(builder: (_) => const SplashPage()); }
@override Widget build(BuildContext context) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); }}تسجيل الدخول
Section titled “تسجيل الدخول”يحتوي مسار تسجيل الدخول على LoginPage و LoginForm و LoginBloc، ويسمح
للمستخدمين بإدخال اسم المستخدم وكلمة المرور لتسجيل الدخول إلى التطبيق.
├── lib│ ├── login│ │ ├── bloc│ │ │ ├── login_bloc.dart│ │ │ ├── login_event.dart│ │ │ └── login_state.dart│ │ ├── login.dart│ │ ├── models│ │ │ ├── models.dart│ │ │ ├── password.dart│ │ │ └── username.dart│ │ └── view│ │ ├── login_form.dart│ │ ├── login_page.dart│ │ └── view.dartنماذج تسجيل الدخول
Section titled “نماذج تسجيل الدخول”نستخدم package:formz لإنشاء نماذج قابلة
لإعادة الاستخدام وموحدة لـ username وpassword.
اسم المستخدم
Section titled “اسم المستخدم”import 'package:formz/formz.dart';
enum UsernameValidationError { empty }
class Username extends FormzInput<String, UsernameValidationError> { const Username.pure() : super.pure(''); const Username.dirty([super.value = '']) : super.dirty();
@override UsernameValidationError? validator(String value) { if (value.isEmpty) return UsernameValidationError.empty; return null; }}لأجل البساطة، نحن نتحقق فقط من أن اسم المستخدم ليس فارغًا، ولكن في التطبيق العملي يمكنك فرض قواعد استخدام الأحرف الخاصة، الطول، وغيرها…
كلمة المرور
Section titled “كلمة المرور”import 'package:formz/formz.dart';
enum PasswordValidationError { empty }
class Password extends FormzInput<String, PasswordValidationError> { const Password.pure() : super.pure(''); const Password.dirty([super.value = '']) : super.dirty();
@override PasswordValidationError? validator(String value) { if (value.isEmpty) return PasswordValidationError.empty; return null; }}مرة أخرى، نحن نُجري فحصًا بسيطًا للتأكد من أن كلمة المرور ليست فارغة.
ملف التجميع للنماذج (Models Barrel)
Section titled “ملف التجميع للنماذج (Models Barrel)”كما في السابق، هناك ملف models.dart لتسهيل استيراد نماذج Username و
Password عبر استيراد واحد فقط.
export 'password.dart';export 'username.dart';الـ Login Bloc
Section titled “الـ Login Bloc”يقوم الـ LoginBloc بإدارة حالة LoginForm ويتولى التحقق من صحة إدخالات اسم
المستخدم وكلمة المرور بالإضافة إلى حالة النموذج.
login_event.dart
Section titled “login_event.dart”في هذا التطبيق، هناك ثلاثة أنواع مختلفة من LoginEvent:
LoginUsernameChanged: يُخطر الـ bloc بأنه تم تعديل اسم المستخدم.LoginPasswordChanged: يُخطر الـ bloc بأنه تم تعديل كلمة المرور.LoginSubmitted: يُخطر الـ bloc بأنه تم تقديم النموذج.
part of 'login_bloc.dart';
sealed class LoginEvent extends Equatable { const LoginEvent();
@override List<Object> get props => [];}
final class LoginUsernameChanged extends LoginEvent { const LoginUsernameChanged(this.username);
final String username;
@override List<Object> get props => [username];}
final class LoginPasswordChanged extends LoginEvent { const LoginPasswordChanged(this.password);
final String password;
@override List<Object> get props => [password];}
final class LoginSubmitted extends LoginEvent { const LoginSubmitted();}login_state.dart
Section titled “login_state.dart”يحتوي الـ LoginState على حالة النموذج بالإضافة إلى حالات إدخال اسم المستخدم
وكلمة المرور.
part of 'login_bloc.dart';
final class LoginState extends Equatable { const LoginState({ this.status = FormzSubmissionStatus.initial, this.username = const Username.pure(), this.password = const Password.pure(), this.isValid = false, });
final FormzSubmissionStatus status; final Username username; final Password password; final bool isValid;
LoginState copyWith({ FormzSubmissionStatus? status, Username? username, Password? password, bool? isValid, }) { return LoginState( status: status ?? this.status, username: username ?? this.username, password: password ?? this.password, isValid: isValid ?? this.isValid, ); }
@override List<Object> get props => [status, username, password];}login_bloc.dart
Section titled “login_bloc.dart”يتولى LoginBloc التفاعل مع تفاعلات المستخدم داخل الـ LoginForm والتعامل مع
التحقق من صحة النموذج وتقديمه.
import 'package:authentication_repository/authentication_repository.dart';import 'package:bloc/bloc.dart';import 'package:equatable/equatable.dart';import 'package:flutter_login/login/login.dart';import 'package:formz/formz.dart';
part 'login_event.dart';part 'login_state.dart';
class LoginBloc extends Bloc<LoginEvent, LoginState> { LoginBloc({ required AuthenticationRepository authenticationRepository, }) : _authenticationRepository = authenticationRepository, super(const LoginState()) { on<LoginUsernameChanged>(_onUsernameChanged); on<LoginPasswordChanged>(_onPasswordChanged); on<LoginSubmitted>(_onSubmitted); }
final AuthenticationRepository _authenticationRepository;
void _onUsernameChanged( LoginUsernameChanged event, Emitter<LoginState> emit, ) { final username = Username.dirty(event.username); emit( state.copyWith( username: username, isValid: Formz.validate([state.password, username]), ), ); }
void _onPasswordChanged( LoginPasswordChanged event, Emitter<LoginState> emit, ) { final password = Password.dirty(event.password); emit( state.copyWith( password: password, isValid: Formz.validate([password, state.username]), ), ); }
Future<void> _onSubmitted( LoginSubmitted event, Emitter<LoginState> emit, ) async { if (state.isValid) { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { await _authenticationRepository.logIn( username: state.username.value, password: state.password.value, ); emit(state.copyWith(status: FormzSubmissionStatus.success)); } catch (_) { emit(state.copyWith(status: FormzSubmissionStatus.failure)); } } }}يعتمد الـ LoginBloc على AuthenticationRepository لأنه عند تقديم النموذج،
يقوم باستدعاء logIn. الحالة الابتدائية للـ bloc هي pure، مما يعني أن
الإدخالات والنموذج لم يتم لمسهم أو التفاعل معهم بعد.
عندما يتغير اسم المستخدم أو كلمة المرور، يقوم الـ bloc بإنشاء نسخة “متسخة”
(dirty) من نموذج Username أو Password ويُحدّث حالة النموذج عبر واجهة برمجة
التطبيقات Formz.validate.
عند إضافة حدث LoginSubmitted، إذا كانت حالة النموذج الحالية صالحة، يقوم الـ
bloc باستدعاء logIn ويُحدّث الحالة بناءً على نتيجة الطلب.
بعد ذلك، سنلقي نظرة على LoginPage و LoginForm.
صفحة تسجيل الدخول
Section titled “صفحة تسجيل الدخول”تتولى LoginPage مسؤولية توفير الـ Route بالإضافة إلى إنشاء وتوفير الـ
LoginBloc لـ LoginForm.
import 'package:authentication_repository/authentication_repository.dart';import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_login/login/login.dart';
class LoginPage extends StatelessWidget { const LoginPage({super.key});
static Route<void> route() { return MaterialPageRoute<void>(builder: (_) => const LoginPage()); }
@override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(12), child: BlocProvider( create: (context) => LoginBloc( authenticationRepository: context.read<AuthenticationRepository>(), ), child: const LoginForm(), ), ), ); }}نموذج تسجيل الدخول
Section titled “نموذج تسجيل الدخول”يتولى LoginForm إخطار الـ LoginBloc بأحداث المستخدم ويستجيب أيضًا للتغيرات
في الحالة باستخدام BlocBuilder و BlocListener.
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_login/login/login.dart';import 'package:formz/formz.dart';
class LoginForm extends StatelessWidget { const LoginForm({super.key});
@override Widget build(BuildContext context) { return BlocListener<LoginBloc, LoginState>( listener: (context, state) { if (state.status.isFailure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( const SnackBar(content: Text('Authentication Failure')), ); } }, child: Align( alignment: const Alignment(0, -1 / 3), child: Column( mainAxisSize: MainAxisSize.min, children: [ _UsernameInput(), const Padding(padding: EdgeInsets.all(12)), _PasswordInput(), const Padding(padding: EdgeInsets.all(12)), _LoginButton(), ], ), ), ); }}
class _UsernameInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (LoginBloc bloc) => bloc.state.username.displayError, );
return TextField( key: const Key('loginForm_usernameInput_textField'), onChanged: (username) { context.read<LoginBloc>().add(LoginUsernameChanged(username)); }, decoration: InputDecoration( labelText: 'username', errorText: displayError != null ? 'invalid username' : null, ), ); }}
class _PasswordInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (LoginBloc bloc) => bloc.state.password.displayError, );
return TextField( key: const Key('loginForm_passwordInput_textField'), onChanged: (password) { context.read<LoginBloc>().add(LoginPasswordChanged(password)); }, obscureText: true, decoration: InputDecoration( labelText: 'password', errorText: displayError != null ? 'invalid password' : null, ), ); }}
class _LoginButton extends StatelessWidget { @override Widget build(BuildContext context) { final isInProgressOrSuccess = context.select( (LoginBloc bloc) => bloc.state.status.isInProgressOrSuccess, );
if (isInProgressOrSuccess) return const CircularProgressIndicator();
final isValid = context.select((LoginBloc bloc) => bloc.state.isValid);
return ElevatedButton( key: const Key('loginForm_continue_raisedButton'), onPressed: isValid ? () => context.read<LoginBloc>().add(const LoginSubmitted()) : null, child: const Text('Login'), ); }}يُستخدم BlocListener لعرض SnackBar في حال فشل تقديم بيانات تسجيل الدخول.
بالإضافة إلى ذلك، يُستخدم context.select للوصول بكفاءة إلى أجزاء محددة من
LoginState لكل ويدجت، مما يمنع عمليات البناء غير الضرورية. تُستخدم دالة
onChanged لإخطار الـ LoginBloc بالتغييرات التي تطرأ على اسم المستخدم أو كلمة
المرور.
ويدجت _LoginButton يتم تفعيله فقط إذا كانت حالة النموذج صالحة، ويُعرض مؤشر
تحميل دائري CircularProgressIndicator مكانه أثناء تقديم النموذج.
الصفحة الرئيسية
Section titled “الصفحة الرئيسية”عند نجاح طلب logIn، سيتغير حالة AuthenticationBloc إلى authenticated وسيتم
توجيه المستخدم إلى صفحة HomePage حيث نعرض معرف المستخدم (id) بالإضافة إلى زر
لتسجيل الخروج.
├── lib│ ├── home│ │ ├── home.dart│ │ └── view│ │ └── home_page.dartصفحة الرئيسية
Section titled “صفحة الرئيسية”يمكن لصفحة HomePage الوصول إلى معرف المستخدم الحالي عبر
context.select((AuthenticationBloc bloc) => bloc.state.user.id) وعرضه باستخدام
ويدجت Text. بالإضافة إلى ذلك، عند الضغط على زر تسجيل الخروج يتم إضافة حدث
AuthenticationLogoutPressed إلى الـ AuthenticationBloc.
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_login/authentication/authentication.dart';
class HomePage extends StatelessWidget { const HomePage({super.key});
static Route<void> route() { return MaterialPageRoute<void>(builder: (_) => const HomePage()); }
@override Widget build(BuildContext context) { return const Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [_UserId(), _LogoutButton()], ), ), ); }}
class _LogoutButton extends StatelessWidget { const _LogoutButton();
@override Widget build(BuildContext context) { return ElevatedButton( child: const Text('Logout'), onPressed: () { context.read<AuthenticationBloc>().add(AuthenticationLogoutPressed()); }, ); }}
class _UserId extends StatelessWidget { const _UserId();
@override Widget build(BuildContext context) { final userId = context.select( (AuthenticationBloc bloc) => bloc.state.user.id, );
return Text('UserID: $userId'); }}في هذه المرحلة لدينا تنفيذ قوي لمسار تسجيل الدخول وقد قمنا بفصل طبقة العرض عن طبقة منطق الأعمال باستخدام Bloc.
يمكن العثور على الشيفرة المصدرية الكاملة لهذا المثال (بما في ذلك اختبارات الوحدة
والويدجت)
هنا.