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

دليل الانتقال (Migration Guide)

يجب أن يستخدم blocTest واجهات bloc الأساسية كلما أمكن ذلك لزيادة المرونة وقابلية إعادة الاستخدام. سابقاً، لم يكن هذا ممكناً لأن BlocBase كان يطبق StateStreamableSource وهو ما لم يكن كافياً لـ blocTest بسبب الاعتماد الداخلي على API الـ emit.

سابقاً، لم يكن من الممكن ترجمة التطبيقات إلى wasm عند استخدام hydrated_bloc. في الإصدار v10.0.0، تمت إعادة هيكلة الحزمة للسماح بالترجمة إلى wasm.

v9.x.x

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
);
runApp(App());
}

v10.x.x

void main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorageDirectory.web
: HydratedStorageDirectory((await getTemporaryDirectory()).path),
);
runApp(const App());
}

❗🧹 إزالة واجهات برمجة التطبيقات (APIs) المهجورة

Section titled “❗🧹 إزالة واجهات برمجة التطبيقات (APIs) المهجورة”
  • تمت إزالة BlocOverrides لصالح Bloc.observer و Bloc.transformer.

❗✨ تقديم واجهة EmittableStateStreamableSource الجديدة

Section titled “❗✨ تقديم واجهة EmittableStateStreamableSource الجديدة”

كانت package:bloc_test سابقاً مرتبطة بشكل وثيق بـ BlocBase. تم تقديم واجهة EmittableStateStreamableSource للسماح بفصل blocTest عن التطبيق الفعلي لـ BlocBase.

✨ إعادة تقديم API الـ HydratedBloc.storage

Section titled “✨ إعادة تقديم API الـ HydratedBloc.storage”

راجع الأسباب الكامنة وراء إعادة تقديم واجهات Bloc.observer و Bloc.transformer.

v8.x.x

Future<void> main() async {
final storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
);
HydratedBlocOverrides.runZoned(
() => runApp(App()),
storage: storage,
);
}

v9.0.0

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
);
runApp(App());
}

✨ إعادة تقديم واجهات برمجة التطبيقات Bloc.observer و Bloc.transformer

Section titled “✨ إعادة تقديم واجهات برمجة التطبيقات Bloc.observer و Bloc.transformer”

تم تقديم API الـ BlocOverrides في الإصدار v8.0.0 في محاولة لدعم نطاق (scoping) إعدادات الـ bloc المحددة مثل BlocObserver و EventTransformer و HydratedStorage. في تطبيقات Dart الصرفة، عملت التغييرات بشكل جيد؛ ومع ذلك، في تطبيقات Flutter، تسبب API الجديد في مشاكل أكثر مما حلها.

تم استلهام API الـ BlocOverrides من واجهات مماثلة في Flutter/Dart:

المشاكل

على الرغم من أنه لم يكن السبب الرئيسي لهذه التغييرات، إلا أن API الـ BlocOverrides أضاف تعقيداً إضافياً للمطورين. فبالإضافة إلى زيادة مقدار التداخل (nesting) وعدد أسطر الكود اللازمة لتحقيق نفس التأثير، تطلب API الـ BlocOverrides من المطورين أن يكون لديهم فهم قوي للـ Zones في Dart. الـ Zones ليست مفهوماً سهلاً للمبتدئين، والفشل في فهم كيفية عملها قد يؤدي إلى ظهور أخطاء (مثل عدم تهيئة المراقبين، المحولات، أو مثيلات التخزين).

على سبيل المثال، كان لدى العديد من المطورين كود مثل:

void main() {
WidgetsFlutterBinding.ensureInitialized();
BlocOverrides.runZoned(...);
}

الكود أعلاه، رغم أنه يبدو غير ضار، يمكن أن يؤدي في الواقع إلى العديد من الأخطاء التي يصعب تتبعها. فأي Zone يتم استدعاء WidgetsFlutterBinding.ensureInitialized منها في البداية ستكون هي الـ Zone التي يتم فيها التعامل مع أحداث الإيماءات (مثل استدعاءات onTap و onPressed) بسبب GestureBinding.initInstances. هذه مجرد واحدة من العديد من المشاكل الناجمة عن استخدام zoneValues.

بالإضافة إلى ذلك، يقوم Flutter بالعديد من الأشياء خلف الكواليس والتي تتضمن تفريع/تعديل الـ Zones (خاصة عند تشغيل الاختبارات) مما قد يؤدي إلى سلوكيات غير متوقعة (وفي كثير من الحالات سلوكيات خارجة عن سيطرة المطور — انظر المشاكل أدناه).

بسبب استخدام runZoned، أدى الانتقال إلى API الـ BlocOverrides إلى اكتشاف العديد من الأخطاء/القيود في Flutter (تحديداً حول اختبارات الـ Widget والـ Integration):

والتي أثرت على العديد من المطورين الذين يستخدمون مكتبة bloc:

v8.0.x

void main() {
BlocOverrides.runZoned(
() {
// ...
},
blocObserver: CustomBlocObserver(),
eventTransformer: customEventTransformer(),
);
}

v8.1.0

void main() {
Bloc.observer = CustomBlocObserver();
Bloc.transformer = customEventTransformer();
// ...
}

❗✨ تقديم API الـ BlocOverrides الجديد

Section titled “❗✨ تقديم API الـ BlocOverrides الجديد”

كان API السابق المستخدم لتجاوز الـ BlocObserver والـ EventTransformer الافتراضيين يعتمد على “singleton” عالمي لكل منهما.

ونتيجة لذلك، لم يكن من الممكن:

  • امتلاك تطبيقات متعددة لـ BlocObserver أو EventTransformer محصورة في أجزاء مختلفة من التطبيق.
  • جعل تجاوزات BlocObserver أو EventTransformer محصورة في حزمة (package) معينة.
    • إذا كانت الحزمة تعتمد على package:bloc وسجلت الـ BlocObserver الخاص بها، فسيتعين على أي مستخدم للحزمة إما الكتابة فوق الـ BlocObserver الخاص بالحزمة أو إرسال التقارير إليه.

كما كان من الصعب أيضاً إجراء الاختبارات بسبب الحالة العالمية المشتركة عبر الاختبارات.

يقدم Bloc v8.0.0 فئة BlocOverrides التي تسمح للمطورين بتجاوز BlocObserver و/أو EventTransformer لـ Zone معينة بدلاً من الاعتماد على singleton عالمي قابل للتغيير.

v7.x.x

void main() {
Bloc.observer = CustomBlocObserver();
Bloc.transformer = customEventTransformer();
// ...
}

v8.0.0

void main() {
BlocOverrides.runZoned(
() {
// ...
},
blocObserver: CustomBlocObserver(),
eventTransformer: customEventTransformer(),
);
}

ستستخدم مثيلات Bloc الـ BlocObserver و/أو الـ EventTransformer للـ Zone الحالية عبر BlocOverrides.current. إذا لم تكن هناك BlocOverrides للـ zone، فستستخدم القيم الافتراضية الداخلية الموجودة (لا تغيير في السلوك/الوظيفة).

هذا يسمح لكل Zone بالعمل بشكل مستقل مع BlocOverrides الخاصة بها.

BlocOverrides.runZoned(
() {
// BlocObserverA and eventTransformerA
final overrides = BlocOverrides.current;
// Blocs in this zone report to BlocObserverA
// and use eventTransformerA as the default transformer.
// ...
// Later...
BlocOverrides.runZoned(
() {
// BlocObserverB and eventTransformerB
final overrides = BlocOverrides.current;
// Blocs in this zone report to BlocObserverB
// and use eventTransformerB as the default transformer.
// ...
},
blocObserver: BlocObserverB(),
eventTransformer: eventTransformerB(),
);
},
blocObserver: BlocObserverA(),
eventTransformer: eventTransformerA(),
);

❗✨ تحسين معالجة الأخطاء والإبلاغ عنها

Section titled “❗✨ تحسين معالجة الأخطاء والإبلاغ عنها”

الهدف من هذه التغييرات هو:

  • جعل الاستثناءات الداخلية غير المعالجة واضحة للغاية مع الحفاظ على وظائف bloc.
  • دعم addError دون تعطيل تدفق التحكم.

سابقاً، كان التعامل مع الأخطاء والإبلاغ عنها يختلف اعتماداً على ما إذا كان التطبيق يعمل في وضع debug أو release. بالإضافة إلى ذلك، كانت الأخطاء المبلغ عنها عبر addError تُعامل كاستثناءات غير ملتقطة في وضع debug، مما أدى إلى تجربة مطور سيئة عند استخدام API الـ addError (تحديداً عند كتابة اختبارات الوحدة).

في v8.0.0، يمكن استخدام addError بأمان للإبلاغ عن الأخطاء ويمكن استخدام blocTest للتحقق من الإبلاغ عن الأخطاء. لا تزال جميع الأخطاء تُبلغ إلى onError؛ ومع ذلك، يتم فقط إعادة رمي الاستثناءات غير الملتقطة (بغض النظر عن وضع debug أو release).

❗🧹 جعل BlocObserver فئة مجردة (abstract)

Section titled “❗🧹 جعل BlocObserver فئة مجردة (abstract)”

كان المقصود من BlocObserver أن يكون واجهة (interface). وبما أن التطبيقات الافتراضية للـ API هي عمليات فارغة (no-ops)، فإن BlocObserver الآن فئة abstract للتواصل بوضوح بأن الفئة مخصصة للتوسيع وليس لإنشاء مثيلات منها مباشرة.

v7.x.x

void main() {
// كان من الممكن إنشاء مثيل من الفئة الأساسية.

final observer = BlocObserver(); }

**v8.0.0**
```dart
class MyBlocObserver extends BlocObserver {...}
void main() {
// لا يمكن إنشاء مثيل من الفئة الأساسية.
final observer = BlocObserver(); // خطأ (ERROR)
// قم بتوسيع `BlocObserver` بدلاً من ذلك.
final observer = MyBlocObserver(); // مقبول (OK)
}

❗✨ استدعاء add يرمي StateError إذا كان الـ Bloc مغلقاً

Section titled “❗✨ استدعاء add يرمي StateError إذا كان الـ Bloc مغلقاً”

سابقاً، كان من الممكن استدعاء add على bloc مغلق وكان الخطأ الداخلي يتم تجاهله، مما يجعل من الصعب تصحيح سبب عدم معالجة الحدث المضاف. لجعل هذا السيناريو أكثر وضوحاً، في v8.0.0، سيؤدي استدعاء add على bloc مغلق إلى رمي StateError والذي سيتم الإبلاغ عنه كاستثناء غير ملتقط وتمريره إلى onError.

❗✨ استدعاء emit يرمي StateError إذا كان الـ Bloc مغلقاً

Section titled “❗✨ استدعاء emit يرمي StateError إذا كان الـ Bloc مغلقاً”

سابقاً، كان من الممكن استدعاء emit داخل bloc مغلق ولم يكن يحدث أي تغيير في الحالة، ولكن لم يكن هناك أيضاً أي إشارة إلى الخطأ، مما يجعل التصحيح صعباً. لجعل هذا السيناريو أكثر وضوحاً، في v8.0.0، سيؤدي استدعاء emit داخل bloc مغلق إلى رمي StateError والذي سيتم الإبلاغ عنه كاستثناء غير ملتقط وتمريره إلى onError.

❗🧹 إزالة واجهات برمجة التطبيقات (APIs) المهجورة

Section titled “❗🧹 إزالة واجهات برمجة التطبيقات (APIs) المهجورة”
  • تمت إزالة mapEventToState لصالح on<Event>.
  • تمت إزالة transformEvents لصالح API الـ EventTransformer.
  • تمت إزالة تعريف النوع TransitionFunction لصالح API الـ EventTransformer.
  • تمت إزالة listen لصالح stream.listen.

MockBloc و MockCubit لم يعودا يتطلبان registerFallbackValue

Section titled “✨ MockBloc و MockCubit لم يعودا يتطلبان registerFallbackValue”

تكون هناك حاجة لـ registerFallbackValue فقط عند استخدام المطابق any() من package:mocktail لنوع مخصص. سابقاً، كانت هناك حاجة لـ registerFallbackValue لكل Event و State عند استخدام MockBloc أو MockCubit.

v8.x.x

class FakeMyEvent extends Fake implements MyEvent {}
class FakeMyState extends Fake implements MyState {}
class MyMockBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}
void main() {
setUpAll(() {
registerFallbackValue(FakeMyEvent());
registerFallbackValue(FakeMyState());
});
// الاختبارات...
}

v9.0.0

class MyMockBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}
void main() {
// الاختبارات...
}

❗✨ تقديم API الـ HydratedBlocOverrides الجديد

Section titled “❗✨ تقديم API الـ HydratedBlocOverrides الجديد”

سابقاً، كان يتم استخدام singleton عالمي لتجاوز تطبيق الـ Storage.

ونتيجة لذلك، لم يكن من الممكن امتلاك تطبيقات Storage متعددة محصورة في أجزاء مختلفة من التطبيق. كما كان من الصعب أيضاً إجراء الاختبارات بسبب الحالة العالمية المشتركة عبر الاختبارات.

يقدم HydratedBloc v8.0.0 فئة HydratedBlocOverrides التي تسمح للمطورين بتجاوز الـ Storage لـ Zone معينة بدلاً من الاعتماد على singleton عالمي قابل للتغيير.

v7.x.x

void main() async {
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getApplicationSupportDirectory(),
);
// ...
}

v8.0.0

void main() {
final storage = await HydratedStorage.build(
storageDirectory: await getApplicationSupportDirectory(),
);
HydratedBlocOverrides.runZoned(
() {
// ...
},
storage: storage,
);
}

ستستخدم مثيلات HydratedBloc الـ Storage للـ Zone الحالية عبر HydratedBlocOverrides.current.

هذا يسمح لكل Zone بالعمل بشكل مستقل مع BlocOverrides الخاصة بها.

✨ تقديم API الـ on<Event> الجديد

Section titled “✨ تقديم API الـ on<Event> الجديد”

تم تقديم API الـ on<Event> كجزء من مقترح استبدال mapEventToState بـ on<Event> في Bloc. بسبب مشكلة في Dart، ليس من الواضح دائماً ما ستكون عليه قيمة الـ state عند التعامل مع مولدات غير متزامنة متداخلة (async*). على الرغم من وجود طرق للالتفاف على المشكلة، إلا أن أحد المبادئ الأساسية لمكتبة bloc هو أن تكون قابلة للتنبؤ. تم إنشاء API الـ on<Event> لجعل المكتبة آمنة قدر الإمكان للاستخدام وللقضاء على أي شك فيما يتعلق بتغييرات الحالة.

يسمح لك on<E> بتسجيل معالج أحداث (event handler) لجميع الأحداث من النوع E. بشكل افتراضي، سيتم معالجة الأحداث بشكل متزامن (concurrently) عند استخدام on<E> على عكس mapEventToState الذي يعالج الأحداث بشكل تسلسلي (sequentially).

v7.1.0

abstract class CounterEvent {}
class Increment extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
@override
Stream<int> mapEventToState(CounterEvent event) async* {
if (event is Increment) {
yield state + 1;
}
}
}

v7.2.0

abstract class CounterEvent {}
class Increment extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
}
}

إذا كنت ترغب في الاحتفاظ بنفس السلوك تماماً كما في v7.1.0، يمكنك تسجيل معالج أحداث واحد لجميع الأحداث وتطبيق محول تسلسلي (sequential transformer):

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc() : super(MyState()) {
on<MyEvent>(_onEvent, transformer: sequential())
}
FutureOr<void> _onEvent(MyEvent event, Emitter<MyState> emit) async {
// TODO: المنطق البرمجي يوضع هنا...
}
}

يمكنك أيضاً تجاوز الـ EventTransformer الافتراضي لجميع الـ blocs في تطبيقك:

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
void main() {
Bloc.transformer = sequential<dynamic>();
...
}

✨ تقديم API الـ EventTransformer الجديد

Section titled “✨ تقديم API الـ EventTransformer الجديد”

فتح API الـ on<Event> الباب أمام القدرة على توفير محول أحداث مخصص لكل معالج أحداث. تم تقديم تعريف نوع (typedef) جديد باسم EventTransformer والذي يمكّن المطورين من تحويل تدفق الأحداث الواردة لكل معالج أحداث بدلاً من الاضطرار إلى تحديد محول أحداث واحد لجميع الأحداث.

الـ EventTransformer مسؤول عن أخذ تدفق الأحداث الواردة جنباً إلى جنب مع EventMapper (معالج الأحداث الخاص بك) وإرجاع تدفق جديد من الأحداث.

typedef EventTransformer<Event> = Stream<Event> Function(Stream<Event> events, EventMapper<Event> mapper)

يقوم الـ EventTransformer الافتراضي بمعالجة جميع الأحداث بشكل متزامن ويبدو كالتالي:

EventTransformer<E> concurrent<E>() {
return (events, mapper) => events.flatMap(mapper);
}

v7.1.0

@override
Stream<Transition<MyEvent, MyState>> transformEvents(events, transitionFn) {
return events
.debounceTime(const Duration(milliseconds: 300))
.flatMap(transitionFn);
}

v7.2.0

/// تعريف `EventTransformer` مخصص
EventTransformer<MyEvent> debounce<MyEvent>(Duration duration) {
return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}
MyBloc() : super(MyState()) {
/// تطبيق الـ `EventTransformer` المخصص على الـ `EventHandler`
on<MyEvent>(_onEvent, transformer: debounce(const Duration(milliseconds: 300)))
}

⚠️ وضع علامة “مهجور” على API الـ transformTransitions

Section titled “⚠️ وضع علامة “مهجور” على API الـ transformTransitions”

تجعل أداة الحصول (getter) الـ stream في Bloc من السهل تجاوز تدفق الحالات الصادر، وبالتالي لم يعد من المفيد الحفاظ على API منفصل لـ transformTransitions.

v7.1.0

@override
Stream<Transition<Event, State>> transformTransitions(
Stream<Transition<Event, State>> transitions,
) {
return transitions.debounceTime(const Duration(milliseconds: 42));
}

v7.2.0

@override
Stream<State> get stream => super.stream.debounceTime(const Duration(milliseconds: 42));

❗ الـ Bloc والـ Cubit يوسعان BlocBase

Section titled “❗ الـ Bloc والـ Cubit يوسعان BlocBase”

كمطور، كانت العلاقة بين الـ blocs والـ cubits غريبة بعض الشيء. عندما تم تقديم cubit لأول مرة، بدأ كفئة أساسية للـ blocs، وهو ما كان منطقياً لأنه كان يحتوي على مجموعة فرعية من الوظائف وكان الـ blocs يوسعون Cubit ببساطة ويعرفون واجهات برمجة تطبيقات إضافية. جاء ذلك مع بعض العيوب:

  • كان لابد من إعادة تسمية جميع واجهات برمجة التطبيقات لتقبل cubit من أجل الدقة، أو كان لابد من إبقائها كـ bloc من أجل الاتساق على الرغم من أنها غير دقيقة هرمياً (#1708، #1560).

  • كان على Cubit توسيع Stream وتطبيق EventSink من أجل الحصول على قاعدة مشتركة يمكن بناء عناصر واجهة المستخدم مثل BlocBuilder و BlocListener وغيرها عليها (#1429).

لاحقاً، جربنا عكس العلاقة وجعل bloc هو القاعدة

والذي حل جزئياً النقطة الأولى أعلاه ولكنه قدم مشاكل أخرى:

  • أصبح API الـ cubit متضخماً بسبب واجهات برمجة تطبيقات bloc الأساسية مثل mapEventToState و add وغيرها (#2228).
    • يمكن للمطورين تقنياً استدعاء هذه الواجهات وتخريب الأشياء.
  • لا نزال نواجه نفس المشكلة المتمثلة في كشف cubit لكامل API الـ stream كما كان من قبل (#1429).

لمعالجة هذه المشكلات، قدمنا فئة أساسية لكل من Bloc و Cubit تسمى BlocBase بحيث لا تزال المكونات العلوية قادرة على التعامل مع كل من مثيلات bloc و cubit ولكن دون كشف كامل واجهات Stream و EventSink مباشرة.

BlocObserver

v6.1.x

class SimpleBlocObserver extends BlocObserver {
@override
void onCreate(Cubit cubit) {...}
@override
void onEvent(Bloc bloc, Object event) {...}
@override
void onChange(Cubit cubit, Object event) {...}
@override
void onTransition(Bloc bloc, Transition transition) {...}
@override
void onError(Cubit cubit, Object error, StackTrace stackTrace) {...}
@override
void onClose(Cubit cubit) {...}
}

v7.0.0

class SimpleBlocObserver extends BlocObserver {
@override
void onCreate(BlocBase bloc) {...}
@override
void onEvent(Bloc bloc, Object event) {...}
@override
void onChange(BlocBase bloc, Object? event) {...}
@override
void onTransition(Bloc bloc, Transition transition) {...}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...}
@override
void onClose(BlocBase bloc) {...}
}

Bloc/Cubit

v6.1.x

final bloc = MyBloc();
bloc.listen((state) {...});
final cubit = MyCubit();
cubit.listen((state) {...});

v7.0.0

final bloc = MyBloc();
bloc.stream.listen((state) {...});
final cubit = MyCubit();
cubit.stream.listen((state) {...});

seed ترجع دالة لدعم القيم الديناميكية

Section titled “❗ seed ترجع دالة لدعم القيم الديناميكية”

من أجل دعم وجود قيمة “بذرة” (seed) قابلة للتغيير ويمكن تحديثها ديناميكياً في setUp ، فإن seed ترجع دالة.

v7.x.x

blocTest(
'...',
seed: MyState(),
...
);

v8.0.0

blocTest(
'...',
seed: () => MyState(),
...
);

expect ترجع دالة لدعم القيم الديناميكية وتتضمن دعم الـ matcher

Section titled “❗ expect ترجع دالة لدعم القيم الديناميكية وتتضمن دعم الـ matcher”

من أجل دعم وجود توقع (expectation) قابل للتغيير ويمكن تحديثه ديناميكياً في setUp ، فإن expect ترجع دالة. كما يدعم expect أيضاً الـ Matchers.

v7.x.x

blocTest(
'...',
expect: [MyStateA(), MyStateB()],
...
);

v8.0.0

blocTest(
'...',
expect: () => [MyStateA(), MyStateB()],
...
);
// يمكن أن يكون أيضاً `Matcher`
blocTest(
'...',
expect: () => contains(MyStateA()),
...
);

errors ترجع دالة لدعم القيم الديناميكية وتتضمن دعم الـ matcher

Section titled “❗ errors ترجع دالة لدعم القيم الديناميكية وتتضمن دعم الـ matcher”

من أجل دعم وجود أخطاء قابلة للتغيير ويمكن تحديثها ديناميكياً في setUp ، فإن errors ترجع دالة. كما يدعم errors أيضاً الـ Matchers.

v7.x.x

blocTest(
'...',
errors: [MyError()],
...
);

v8.0.0

blocTest(
'...',
errors: () => [MyError()],
...
);
// يمكن أن يكون أيضاً `Matcher`
blocTest(
'...',
errors: () => contains(MyError()),
...
);

لدعم محاكاة (stubbing) مختلف واجهات برمجة التطبيقات الأساسية، يتم تصدير MockBloc و MockCubit كجزء من حزمة bloc_test. سابقاً، كان يجب استخدام MockBloc لكل من مثيلات Bloc و Cubit وهو ما لم يكن بديهياً.

v7.x.x

class MockMyBloc extends MockBloc<MyState> implements MyBloc {}
class MockMyCubit extends MockBloc<MyState> implements MyBloc {}

v8.0.0

class MockMyBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}
class MockMyCubit extends MockCubit<MyState> implements MyCubit {}

نظراً للقيود المختلفة لحزمة package:mockito الآمنة من القيم الخالية (null-safe) والموضحة هنا، يتم استخدام package:mocktail بواسطة MockBloc و MockCubit. يتيح ذلك للمطورين الاستمرار في استخدام واجهة محاكاة مألوفة دون الحاجة إلى كتابة stubs يدوياً أو الاعتماد على توليد الكود.

v7.x.x

import 'package:mockito/mockito.dart';
...
when(bloc.state).thenReturn(MyState());
verify(bloc.add(any)).called(1);

v8.0.0

import 'package:mocktail/mocktail.dart';
...
when(() => bloc.state).thenReturn(MyState());
verify(() => bloc.add(any())).called(1);

يرجى الرجوع إلى #347 بالإضافة إلى توثيق mocktail لمزيد من المعلومات.

❗ إعادة تسمية معامل cubit إلى bloc

Section titled “❗ إعادة تسمية معامل cubit إلى bloc”

نتيجة لإعادة الهيكلة في package:bloc لتقديم BlocBase الذي يوسعه كل من Bloc و Cubit ، تمت إعادة تسمية معاملات BlocBuilder و BlocConsumer و BlocListener من cubit إلى bloc لأن هذه العناصر تعمل على نوع BlocBase. كما أن هذا يتماشى بشكل أكبر مع اسم المكتبة ونأمل أن يحسن من قابلية القراءة.

v6.1.x

BlocBuilder(
cubit: myBloc,
...
)
BlocListener(
cubit: myBloc,
...
)
BlocConsumer(
cubit: myBloc,
...
)

v7.0.0

BlocBuilder(
bloc: myBloc,
...
)
BlocListener(
bloc: myBloc,
...
)
BlocConsumer(
bloc: myBloc,
...
)

storageDirectory مطلوب عند استدعاء HydratedStorage.build

Section titled “❗ storageDirectory مطلوب عند استدعاء HydratedStorage.build”

من أجل جعل package:hydrated_bloc حزمة Dart صرفة، تمت إزالة الاعتماد على package:path_provider وأصبح معامل storageDirectory عند استدعاء HydratedStorage.build مطلوباً ولم يعد يعود افتراضياً إلى getTemporaryDirectory.

v6.x.x

HydratedBloc.storage = await HydratedStorage.build();

v7.0.0

import 'package:path_provider/path_provider.dart';
...
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getTemporaryDirectory(),
);

❗ وضع علامة “مهجور” على context.bloc و context.repository لصالح context.read و context.watch

Section titled “❗ وضع علامة “مهجور” على context.bloc و context.repository لصالح context.read و context.watch”

تمت إضافة context.read و context.watch و context.select لتتماشى مع واجهة برمجة تطبيقات provider الحالية التي يألفها العديد من المطورين وللمعالجة المشكلات التي أثارها المجتمع. لتحسين أمان الكود والحفاظ على الاتساق، تم وضع علامة “مهجور” على context.bloc لأنه يمكن استبداله بـ context.read أو context.watch اعتماداً على ما إذا كان يتم استخدامه مباشرة داخل build.

context.watch

يعالج context.watch طلب الحصول على MultiBlocBuilder لأنه يمكننا مراقبة عدة blocs داخل Builder واحد من أجل عرض واجهة المستخدم بناءً على حالات متعددة:

Builder(
builder: (context) {
final stateA = context.watch<BlocA>().state;
final stateB = context.watch<BlocB>().state;
final stateC = context.watch<BlocC>().state;
// إرجاع Widget يعتمد على حالة BlocA و BlocB و BlocC
}
);

context.select

يسمح context.select للمطورين بعرض/تحديث واجهة المستخدم بناءً على جزء من حالة الـ bloc ويعالج طلب الحصول على buildWhen أبسط.

final name = context.select((UserBloc bloc) => bloc.state.user.name);

تسمح لنا القطعة البرمجية أعلاه بالوصول إلى الـ widget وإعادة بنائه فقط عندما يتغير اسم المستخدم الحالي.

context.read

على الرغم من أن context.read يبدو مطابقاً لـ context.bloc ، إلا أن هناك بعض الاختلافات الدقيقة ولكن الهامة. كلاهما يسمح لك بالوصول إلى bloc باستخدام BuildContext ولا يؤديان إلى إعادة البناء؛ ومع ذلك، لا يمكن استدعاء context.read مباشرة داخل طريقة build. هناك سببان رئيسيان لاستخدام context.bloc داخل build:

  1. للوصول إلى حالة الـ bloc
@override
Widget build(BuildContext context) {
final state = context.bloc<MyBloc>().state;
return Text('$state');
}

الاستخدام أعلاه عرضة للخطأ لأن عنصر الـ Text لن يتم إعادة بنائه إذا تغيرت حالة الـ bloc. في هذا السيناريو، يجب استخدام إما BlocBuilder أو context.watch.

@override
Widget build(BuildContext context) {
final state = context.watch<MyBloc>().state;
return Text('$state');
}

أو

@override
Widget build(BuildContext context) {
return BlocBuilder<MyBloc, MyState>(
builder: (context, state) => Text('$state'),
);
}
  1. للوصول إلى الـ bloc حتى يمكن إضافة حدث
@override
Widget build(BuildContext context) {
final bloc = context.bloc<MyBloc>();
return ElevatedButton(
onPressed: () => bloc.add(MyEvent()),
...
)
}

الاستخدام أعلاه غير فعال لأنه يؤدي إلى البحث عن الـ bloc في كل عملية إعادة بناء بينما لا تكون هناك حاجة للـ bloc إلا عندما ينقر المستخدم على الـ ElevatedButton. في هذا السيناريو، يفضل استخدام context.read للوصول إلى الـ bloc مباشرة حيث تبرز الحاجة إليه (في هذه الحالة، في استدعاء onPressed).

@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<MyBloc>().add(MyEvent()),
...
)
}

v6.0.x

@override
Widget build(BuildContext context) {
final bloc = context.bloc<MyBloc>();
return ElevatedButton(
onPressed: () => bloc.add(MyEvent()),
...
)
}

v6.1.x

@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<MyBloc>().add(MyEvent()),
...
)
}

?> إذا كنت تصل إلى bloc لإضافة حدث، فقم بالوصول إليه باستخدام context.read في الاستدعاء (callback) حيث تبرز الحاجة إليه.

v6.0.x

@override
Widget build(BuildContext context) {
final state = context.bloc<MyBloc>().state;
return Text('$state');
}

v6.1.x

@override
Widget build(BuildContext context) {
final state = context.watch<MyBloc>().state;
return Text('$state');
}

?> استخدم context.watch عند الوصول إلى حالة الـ bloc لضمان إعادة بناء الـ widget عند تغير الحالة.

بسبب دمج Cubit ، أصبحت onError الآن مشتركة بين كل من مثيلات Bloc و Cubit. وبما أن Cubit هو القاعدة، فإن BlocObserver سيقبل نوع Cubit بدلاً من نوع Bloc في تجاوز onError.

v5.x.x

class MyBlocObserver extends BlocObserver {
@override
void onError(Bloc bloc, Object error, StackTrace stackTrace) {
super.onError(bloc, error, stackTrace);
}
}

v6.0.0

class MyBlocObserver extends BlocObserver {
@override
void onError(Cubit cubit, Object error, StackTrace stackTrace) {
super.onError(cubit, error, stackTrace);
}
}

❗ الـ Bloc لا يرسل الحالة الأخيرة عند الاشتراك

Section titled “❗ الـ Bloc لا يرسل الحالة الأخيرة عند الاشتراك”

تم إجراء هذا التغيير لمواءمة Bloc و Cubit مع سلوك الـ Stream المدمج في Dart. بالإضافة إلى ذلك، أدى الالتزام بالسلوك القديم في سياق Cubit إلى العديد من الآثار الجانبية غير المقصودة وعقد بشكل عام التطبيقات الداخلية للحزم الأخرى مثل flutter_bloc و bloc_test دون داعٍ (مما تطلب استخدام skip(1) ، إلخ…).

v5.x.x

final bloc = MyBloc();
bloc.listen(print);

سابقاً، كانت القطعة البرمجية أعلاه تطبع الحالة الأولية للـ bloc متبوعة بتغييرات الحالة اللاحقة.

v6.x.x

في v6.0.0، لا تطبع القطعة البرمجية أعلاه الحالة الأولية وتطبع فقط تغييرات الحالة اللاحقة. يمكن تحقيق السلوك السابق بما يلي:

final bloc = MyBloc();
print(bloc.state);
bloc.listen(print);

?> ملاحظة: سيؤثر هذا التغيير فقط على الكود الذي يعتمد على اشتراكات bloc المباشرة. عند استخدام BlocBuilder أو BlocListener أو BlocConsumer ، لن يكون هناك تغيير ملحوظ في السلوك.

MockBloc يتطلب نوع الحالة (State) فقط

Section titled “❗ MockBloc يتطلب نوع الحالة (State) فقط”

هذا ليس ضرورياً ويزيل الكود الزائد مع جعل MockBloc متوافقاً مع Cubit.

v5.x.x

class MockCounterBloc extends MockBloc<CounterEvent, int> implements CounterBloc {}

v6.0.0

class MockCounterBloc extends MockBloc<int> implements CounterBloc {}

whenListen يتطلب نوع الحالة (State) فقط

Section titled “❗ whenListen يتطلب نوع الحالة (State) فقط”

هذا ليس ضرورياً ويزيل الكود الزائد مع جعل whenListen متوافقاً مع Cubit.

v5.x.x

whenListen<CounterEvent,int>(bloc, Stream.fromIterable([0, 1, 2, 3]));

v6.0.0

whenListen<int>(bloc, Stream.fromIterable([0, 1, 2, 3]));

blocTest لا يتطلب نوع الحدث (Event)

Section titled “❗ blocTest لا يتطلب نوع الحدث (Event)”

هذا ليس ضرورياً ويزيل الكود الزائد مع جعل blocTest متوافقاً مع Cubit.

v5.x.x

blocTest<CounterBloc, CounterEvent, int>(
'emits [1] when increment is called',
build: () async => CounterBloc(),
act: (bloc) => bloc.add(CounterEvent.increment),
expect: const <int>[1],
);

v6.0.0

blocTest<CounterBloc, int>(
'emits [1] when increment is called',
build: () => CounterBloc(),
act: (bloc) => bloc.add(CounterEvent.increment),
expect: const <int>[1],
);

❗ القيمة الافتراضية لـ skip في blocTest هي 0

Section titled “❗ القيمة الافتراضية لـ skip في blocTest هي 0”

بما أن مثيلات bloc و cubit لن ترسل الحالة الأخيرة للاشتراكات الجديدة، لم يعد من الضروري جعل القيمة الافتراضية لـ skip هي 1.

v5.x.x

blocTest<CounterBloc, CounterEvent, int>(
'emits [0] when skip is 0',
build: () async => CounterBloc(),
skip: 0,
expect: const <int>[0],
);

v6.0.0

blocTest<CounterBloc, int>(
'emits [] when skip is 0',
build: () => CounterBloc(),
skip: 0,
expect: const <int>[],
);

يمكن اختبار الحالة الأولية لـ bloc أو cubit بما يلي:

test('initial state is correct', () {
expect(MyBloc().state, InitialState());
});

❗ جعل build في blocTest متزامناً (synchronous)

Section titled “❗ جعل build في blocTest متزامناً (synchronous)”

سابقاً، تم جعل build غير متزامن (async) بحيث يمكن إجراء تحضيرات متنوعة لوضع الـ bloc تحت الاختبار في حالة معينة. لم يعد هذا ضرورياً كما أنه يحل العديد من المشكلات الناتجة عن التأخير المضاف بين البناء والاشتراك داخلياً. بدلاً من إجراء تحضير غير متزامن لوضع bloc في حالة مطلوبة، يمكننا الآن تعيين حالة الـ bloc عن طريق ربط emit بالحالة المطلوبة.

v5.x.x

blocTest<CounterBloc, CounterEvent, int>(
'emits [2] when increment is added',
build: () async {
final bloc = CounterBloc();
bloc.add(CounterEvent.increment);
await bloc.take(2);
return bloc;
}
act: (bloc) => bloc.add(CounterEvent.increment),
expect: const <int>[2],
);

v6.0.0

blocTest<CounterBloc, int>(
'emits [2] when increment is added',
build: () => CounterBloc()..emit(1),
act: (bloc) => bloc.add(CounterEvent.increment),
expect: const <int>[2],
);

❗ إعادة تسمية معامل bloc في BlocBuilder إلى cubit

Section titled “❗ إعادة تسمية معامل bloc في BlocBuilder إلى cubit”

من أجل جعل BlocBuilder يعمل مع مثيلات bloc و cubit ، تمت إعادة تسمية معامل bloc إلى cubit (بما أن Cubit هو الفئة الأساسية).

v5.x.x

BlocBuilder(
bloc: myBloc,
builder: (context, state) {...}
)

v6.0.0

BlocBuilder(
cubit: myBloc,
builder: (context, state) {...}
)

❗ إعادة تسمية معامل bloc في BlocListener إلى cubit

Section titled “❗ إعادة تسمية معامل bloc في BlocListener إلى cubit”

من أجل جعل BlocListener يعمل مع مثيلات bloc و cubit ، تمت إعادة تسمية معامل bloc إلى cubit (بما أن Cubit هو الفئة الأساسية).

v5.x.x

BlocListener(
bloc: myBloc,
listener: (context, state) {...}
)

v6.0.0

BlocListener(
cubit: myBloc,
listener: (context, state) {...}
)

❗ إعادة تسمية معامل bloc في BlocConsumer إلى cubit

Section titled “❗ إعادة تسمية معامل bloc في BlocConsumer إلى cubit”

من أجل جعل BlocConsumer يعمل مع مثيلات bloc و cubit ، تمت إعادة تسمية معامل bloc إلى cubit (بما أن Cubit هو الفئة الأساسية).

v5.x.x

BlocConsumer(
cubit: myBloc,
listener: (context, state) {...},
builder: (context, state) {...}
)

v6.0.0

BlocConsumer(
cubit: myBloc,
listener: (context, state) {...},
builder: (context, state) {...}
)

كمطور، كان الاضطرار إلى تجاوز initialState عند إنشاء bloc يمثل مشكلتين رئيسيتين:

  • يمكن أن تكون الـ initialState للـ bloc ديناميكية ويمكن أيضاً الرجوع إليها في وقت لاحق (حتى خارج الـ bloc نفسه). بطريقة ما، يمكن اعتبار ذلك تسريباً لمعلومات الـ bloc الداخلية إلى طبقة واجهة المستخدم.
  • إنها طريقة مطولة (verbose).

v4.x.x

class CounterBloc extends Bloc<CounterEvent, int> {
@override
int get initialState => 0;
...
}

v5.0.0

class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
...
}

?> لمزيد من المعلومات، راجع #1304

❗ إعادة تسمية BlocDelegate إلى BlocObserver

Section titled “❗ إعادة تسمية BlocDelegate إلى BlocObserver”

لم يكن اسم BlocDelegate وصفاً دقيقاً للدور الذي تلعبه الفئة. يوحي اسم BlocDelegate بأن الفئة تلعب دوراً نشطاً، بينما في الواقع كان الدور المقصود لـ BlocDelegate هو أن يكون مكوناً سلبياً يراقب ببساطة جميع الـ blocs في التطبيق.

v4.x.x

class MyBlocDelegate extends BlocDelegate {
...
}

v5.0.0

class MyBlocObserver extends BlocObserver {
...
}

كان BlocSupervisor مكوناً آخر يتعين على المطورين معرفته والتفاعل معه لغرض وحيد هو تحديد BlocDelegate مخصص. مع التغيير إلى BlocObserver ، شعرنا أنه من الأفضل لتجربة المطور تعيين المراقب مباشرة على الـ bloc نفسه.

?> مكننا هذا التغيير أيضاً من فصل إضافات bloc الأخرى مثل HydratedStorage عن الـ BlocObserver.

v4.x.x

BlocSupervisor.delegate = MyBlocDelegate();

v5.0.0

Bloc.observer = MyBlocObserver();

❗ إعادة تسمية condition في BlocBuilder إلى buildWhen

Section titled “❗ إعادة تسمية condition في BlocBuilder إلى buildWhen”

عند استخدام BlocBuilder ، كان بإمكاننا سابقاً تحديد condition لتحديد ما إذا كان يجب على الـ builder إعادة البناء.

BlocBuilder<MyBloc, MyState>(
condition: (previous, current) {
// إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ builder
},
builder: (context, state) {...}
)

اسم condition ليس واضحاً جداً أو بديهياً، والأهم من ذلك، عند التفاعل مع BlocConsumer ، أصبحت واجهة برمجة التطبيقات غير متسقة لأن المطورين يمكنهم تقديم شرطين (واحد للـ builder وواحد للـ listener). ونتيجة لذلك، كشف API الـ BlocConsumer عن buildWhen و listenWhen.

BlocConsumer<MyBloc, MyState>(
listenWhen: (previous, current) {
// إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ listener
},
listener: (context, state) {...},
buildWhen: (previous, current) {
// إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ builder
},
builder: (context, state) {...},
)

من أجل مواءمة واجهة برمجة التطبيقات وتوفير تجربة مطور أكثر اتساقاً، تمت إعادة تسمية condition إلى buildWhen.

v4.x.x

BlocBuilder<MyBloc, MyState>(
condition: (previous, current) {
// إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ builder
},
builder: (context, state) {...}
)

v5.0.0

BlocBuilder<MyBloc, MyState>(
buildWhen: (previous, current) {
// إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ builder
},
builder: (context, state) {...}
)

❗ إعادة تسمية condition في BlocListener إلى listenWhen

Section titled “❗ إعادة تسمية condition في BlocListener إلى listenWhen”

لنفس الأسباب المذكورة أعلاه، تمت أيضاً إعادة تسمية شرط BlocListener.

v4.x.x

BlocListener<MyBloc, MyState>(
condition: (previous, current) {
// إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ listener
},
listener: (context, state) {...}
)

v5.0.0

BlocListener<MyBloc, MyState>(
listenWhen: (previous, current) {
// إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ listener
},
listener: (context, state) {...}
)

❗ إعادة تسمية HydratedStorage و HydratedBlocStorage

Section titled “❗ إعادة تسمية HydratedStorage و HydratedBlocStorage”

من أجل تحسين إعادة استخدام الكود بين hydrated_bloc و hydrated_cubit ، تمت إعادة تسمية تطبيق التخزين الافتراضي الفعلي من HydratedBlocStorage إلى HydratedStorage. بالإضافة إلى ذلك، تمت إعادة تسمية واجهة HydratedStorage من HydratedStorage إلى Storage.

v4.0.0

class MyHydratedStorage implements HydratedStorage {
...
}

v5.0.0

class MyHydratedStorage implements Storage {
...
}

❗ فصل HydratedStorage عن BlocDelegate

Section titled “❗ فصل HydratedStorage عن BlocDelegate”

كما ذكرنا سابقاً، تمت إعادة تسمية BlocDelegate إلى BlocObserver وتم تعيينه مباشرة كجزء من الـ bloc عبر:

Bloc.observer = MyBlocObserver();

تم إجراء التغيير التالي من أجل:

  • البقاء متسقاً مع API مراقب bloc الجديد.
  • إبقاء التخزين محصوراً في HydratedBloc فقط.
  • فصل الـ BlocObserver عن الـ Storage.

v4.0.0

BlocSupervisor.delegate = await HydratedBlocDelegate.build();

v5.0.0

HydratedBloc.storage = await HydratedStorage.build();

❗ تبسيط عملية التهيئة (Initialization)

Section titled “❗ تبسيط عملية التهيئة (Initialization)”

سابقاً، كان على المطورين استدعاء super.initialState ?? DefaultInitialState() يدوياً من أجل إعداد مثيلات HydratedBloc الخاصة بهم. كان هذا الأمر ثقيلاً ومطولاً وغير متوافق أيضاً مع التغييرات الجذرية في initialState في bloc. ونتيجة لذلك، أصبحت تهيئة HydratedBloc في v5.0.0 مطابقة لتهيئة Bloc العادية.

v4.0.0

class CounterBloc extends HydratedBloc<CounterEvent, int> {
@override
int get initialState => super.initialState ?? 0;
}

v5.0.0

class CounterBloc extends HydratedBloc<CounterEvent, int> {
CounterBloc() : super(0);
...
}