تخطَّ إلى المحتوى

تسجيل الدخول في Flutter

intermediate

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

demo

  • BlocProvider، ويدجت Flutter يقوم بتوفير bloc للأطفال (العناصر الفرعية) الخاصة به.
  • إضافة الأحداث باستخدام context.read.
  • تجنب إعادة البناء غير الضرورية باستخدام Equatable.
  • RepositoryProvider، ويدجت Flutter يقوم بتوفير repository للأطفال الخاصة به.
  • BlocListener، ويدجت Flutter يقوم بتنفيذ كود المستمع استجابة لتغيرات الحالة في الـ bloc.
  • تحديث واجهة المستخدم بناءً على جزء من حالة الـ bloc باستخدام context.select.

سنبدأ بإنشاء مشروع Flutter جديد تمامًا

Terminal window
flutter create flutter_login

بعد ذلك، يمكننا تثبيت جميع الاعتمادات الخاصة بنا

Terminal window
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:

packages/authentication_repository/pubspec.yaml
name: authentication_repository
description: 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.

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):

packages/authentication_repository/lib/authentication_repository.dart
export 'src/authentication_repository.dart';

هذا كل شيء بالنسبة لـ AuthenticationRepository، في الخطوة التالية سنعمل على UserRepository.

تمامًا كما فعلنا مع AuthenticationRepository، سنقوم بإنشاء حزمة user_repository داخل مجلد packages.

├── android
├── ios
├── lib
├── packages
│ ├── authentication_repository
│ └── user_repository
└── test

بعد ذلك، سنقوم بإنشاء ملف pubspec.yaml الخاص بـ user_repository:

packages/user_repository/pubspec.yaml
name: user_repository
description: 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:

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 ليقوم بتصدير كل النماذج، بحيث يمكننا استخدام استيراد واحد لاستدعاء نماذج متعددة.

packages/user_repository/lib/src/models/models.dart
export 'user.dart';

الآن بعد تعريف النماذج، يمكننا تنفيذ فئة UserRepository في packages/user_repository/lib/src/user_repository.dart.

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):

packages/user_repository/lib/user_repository.dart
export 'src/models/models.dart';
export 'src/user_repository.dart';

الآن بعد أن أتممنا حزمتي authentication_repository و user_repository، يمكننا الانتقال للتركيز على تطبيق Flutter.

لنبدأ بتحديث ملف pubspec.yaml المُولد في جذر مشروعنا:

pubspec.yaml
name: flutter_login
description: A new Flutter project.
version: 1.0.0+1
publish_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

يمكننا تثبيت التبعيات عن طريق تشغيل الأمر:

Terminal window
flutter pub get

سيتولى AuthenticationBloc مسؤولية الاستجابة لتغيرات حالة المصادقة (التي يعرضها AuthenticationRepository) وسيصدر حالات يمكننا التفاعل معها في طبقة العرض.

تم تنفيذ AuthenticationBloc داخل مجلد lib/authentication لأننا نعتبر المصادقة كميزة في طبقة التطبيق الخاصة بنا.

├── lib
│ ├── app.dart
│ ├── authentication
│ │ ├── authentication.dart
│ │ └── bloc
│ │ ├── authentication_bloc.dart
│ │ ├── authentication_event.dart
│ │ └── authentication_state.dart
│ ├── main.dart

تمثل مثيلات AuthenticationEvent المدخلات إلى AuthenticationBloc، وسيتم معالجتها لاستخدامها في إصدار مثيلات جديدة من AuthenticationState.

في هذا التطبيق، سيستجيب AuthenticationBloc لحدثين مختلفين:

  • AuthenticationSubscriptionRequested: الحدث الأولي الذي يُبلغ الـ bloc بالاشتراك في تدفق AuthenticationStatus.
  • AuthenticationLogoutPressed: يُعلم الـ bloc بحدوث تسجيل خروج من قِبل المستخدم.
lib/authentication/bloc/authentication_event.dart
part of 'authentication_bloc.dart';
sealed class AuthenticationEvent {
const AuthenticationEvent();
}
final class AuthenticationSubscriptionRequested extends AuthenticationEvent {}
final class AuthenticationLogoutPressed extends AuthenticationEvent {}

لننتقل الآن للنظر في AuthenticationState.

تمثل مثيلات AuthenticationState نواتج AuthenticationBloc وسيتم استهلاكها من قبل طبقة العرض.

لفئة AuthenticationState ثلاث مُنشئين مسمّين:

  • AuthenticationState.unknown(): الحالة الافتراضية التي تدل على أن الـ bloc لا يعرف بعد ما إذا كان المستخدم الحالي مصدقًا أم لا.

  • AuthenticationState.authenticated(): الحالة التي تشير إلى أن المستخدم حالياً مصدق عليه.

  • AuthenticationState.unauthenticated(): الحالة التي تدل على أن المستخدم حالياً غير مصدق عليه.

lib/authentication/bloc/authentication_state.dart
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.

يدير AuthenticationBloc حالة المصادقة في التطبيق، والتي تُستخدم لاتخاذ قرارات مثل بدء المستخدم في صفحة تسجيل الدخول أو الصفحة الرئيسية.

lib/authentication/bloc/authentication_bloc.dart
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 الافتراضي بالنص التالي:

lib/main.dart
import 'package:flutter/widgets.dart';
import 'package:flutter_login/app.dart';
void main() => runApp(const App());

app.dart يحتوي على ويدجت الجذر App الخاص بالتطبيق بأكمله.

lib/app.dart
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.

ميزة شاشة البداية ستتكون من عرض بسيط يُعرض فور إطلاق التطبيق بينما يحدد التطبيق ما إذا كان المستخدم مصادقًا عليه.

lib
└── splash
├── splash.dart
└── view
└── splash_page.dart
lib/splash/view/splash_page.dart
import '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()),
);
}
}

يحتوي مسار تسجيل الدخول على 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

نستخدم package:formz لإنشاء نماذج قابلة لإعادة الاستخدام وموحدة لـ username وpassword.

lib/login/models/username.dart
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;
}
}

لأجل البساطة، نحن نتحقق فقط من أن اسم المستخدم ليس فارغًا، ولكن في التطبيق العملي يمكنك فرض قواعد استخدام الأحرف الخاصة، الطول، وغيرها…

lib/login/models/password.dart
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 عبر استيراد واحد فقط.

lib/login/models/models.dart
export 'password.dart';
export 'username.dart';

يقوم الـ LoginBloc بإدارة حالة LoginForm ويتولى التحقق من صحة إدخالات اسم المستخدم وكلمة المرور بالإضافة إلى حالة النموذج.

في هذا التطبيق، هناك ثلاثة أنواع مختلفة من LoginEvent:

  • LoginUsernameChanged: يُخطر الـ bloc بأنه تم تعديل اسم المستخدم.
  • LoginPasswordChanged: يُخطر الـ bloc بأنه تم تعديل كلمة المرور.
  • LoginSubmitted: يُخطر الـ bloc بأنه تم تقديم النموذج.
lib/login/bloc/login_event.dart
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();
}

يحتوي الـ LoginState على حالة النموذج بالإضافة إلى حالات إدخال اسم المستخدم وكلمة المرور.

lib/login/bloc/login_state.dart
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];
}

يتولى LoginBloc التفاعل مع تفاعلات المستخدم داخل الـ LoginForm والتعامل مع التحقق من صحة النموذج وتقديمه.

lib/login/bloc/login_bloc.dart
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.

تتولى LoginPage مسؤولية توفير الـ Route بالإضافة إلى إنشاء وتوفير الـ LoginBloc لـ LoginForm.

lib/login/view/login_page.dart
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(),
),
),
);
}
}

يتولى LoginForm إخطار الـ LoginBloc بأحداث المستخدم ويستجيب أيضًا للتغيرات في الحالة باستخدام BlocBuilder و BlocListener.

lib/login/view/login_form.dart
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 مكانه أثناء تقديم النموذج.

عند نجاح طلب logIn، سيتغير حالة AuthenticationBloc إلى authenticated وسيتم توجيه المستخدم إلى صفحة HomePage حيث نعرض معرف المستخدم (id) بالإضافة إلى زر لتسجيل الخروج.

├── lib
│ ├── home
│ │ ├── home.dart
│ │ └── view
│ │ └── home_page.dart

يمكن لصفحة HomePage الوصول إلى معرف المستخدم الحالي عبر
context.select((AuthenticationBloc bloc) => bloc.state.user.id) وعرضه باستخدام ويدجت Text. بالإضافة إلى ذلك، عند الضغط على زر تسجيل الخروج يتم إضافة حدث AuthenticationLogoutPressed إلى الـ AuthenticationBloc.

lib/home/view/home_page.dart
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.

يمكن العثور على الشيفرة المصدرية الكاملة لهذا المثال (بما في ذلك اختبارات الوحدة والويدجت)
هنا.