
(المقال الأصلي المنشور باللغة الإنجليزية على متوسط )
يوفر Flutter إطارًا حديثًا سريع الاستجابة ، ومجموعة كبيرة من الأدوات والأدوات. ولكن لسوء الحظ ، فإن الوثائق ليست بمثابة دليل للهيكل الموصى به لتطبيق Android .
لا يوجد هيكل مثالي وعالمي يمكن أن يلائم أي متطلبات يمكن تصورها لمهمة تقنية ، ولكن دعنا نعترف بأن معظم تطبيقات الهاتف المحمول التي نعمل عليها لديها الوظائف التالية:
- طلب وتنزيل البيانات.
- تحويل وإعداد البيانات للمستخدم.
- كتابة وقراءة البيانات من قاعدة بيانات أو نظام ملفات.
في ضوء كل هذا ، قمتُ بإنشاء تطبيق تجريبي يحل المشكلة نفسها باستخدام طرق مختلفة للعمارة.
في البداية ، يتم عرض شاشة للمستخدم بها زر "تحميل بيانات المستخدم" الموجود في الوسط. عندما ينقر المستخدم على الزر ، يحدث تحميل بيانات غير متزامن ، ويتم استبدال الزر بمؤشر تحميل. عند اكتمال تنزيل البيانات ، يتم استبدال مؤشر التنزيل بالبيانات.
لذلك دعونا نبدأ.

البيانات
لتبسيط المهمة ، قمت بإنشاء فئة Repository
، والتي تحتوي على أسلوب getUser()
. تحاكي هذه الطريقة التحميل غير المتزامن للبيانات من الشبكة وتقوم بإرجاع Future<User>
.
إذا لم تكن معتادًا على العقود المستقبلية والبرمجة غير المتزامنة في Dart ، فيمكننا قراءة المزيد عنها هنا وقراءة وثائق فئة Future .
class Repository { Future<User> getUser() async { await Future.delayed(Duration(seconds: 2)); return User(name: 'John', surname: 'Smith'); } }
class User { User({ @required this.name, @required this.surname, }); final String name; final String surname; }
الفانيليا
دعنا نطور التطبيق ، كما يفعل المطور إذا قرأ وثائق Flutter على الموقع الرسمي.
افتح شاشة VanillaScreen
باستخدام Navigator
Navigator.push( context, MaterialPageRoute( builder: (context) => VanillaScreen(_repository), ), );
نظرًا لأن حالة عنصر واجهة المستخدم يمكن أن تتغير عدة مرات خلال دورة حياتها ، فنحن بحاجة إلى أن نرث من StatefulWidget
. لتطبيق عنصر واجهة المستخدم الخاص بك ، تحتاج أيضًا إلى فئة State
. يمثل bool _isLoading
و User _user
في فئة _VanillaScreenState
حالة عنصر واجهة المستخدم. تتم تهيئة كلا الحقلين قبل استدعاء الأسلوب build(BuildContext context)
لأول مرة.
class VanillaScreen extends StatefulWidget { VanillaScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _VanillaScreenState(); } class _VanillaScreenState extends State<VanillaScreen> { bool _isLoading = false; User _user; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Vanilla'), ), body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), ), ); } Widget _buildBody() { if (_user != null) { return _buildContent(); } else { return _buildInit(); } } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { setState(() { _isLoading = true; }); widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); }); }, ), ); } Widget _buildContent() { return Center( child: Text('Hello ${_user.name} ${_user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } }
بعد إنشاء كائن حالة عنصر واجهة المستخدم ، يتم استدعاء أسلوب البنية build(BuildContext context)
لإنشاء واجهة المستخدم. يتم اتخاذ جميع القرارات المتعلقة بأي عنصر واجهة مستخدم على الشاشة مباشرةً في رمز إعلان واجهة المستخدم.
body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), )
لعرض مؤشر التقدم ، عندما يقوم المستخدم بالنقر فوق الزر "تحميل تفاصيل المستخدم" ، فإننا نقوم بما يلي.
setState(() { _isLoading = true; });
من الوثائق (الترجمة):
تقوم استدعاء الأسلوب setState () بإعلام الإطار الذي تم تغيير الحالة الداخلية لهذا الكائن فيه ، وقد يؤثر على واجهة المستخدم في الشجرة الفرعية. هذا هو السبب في أن الإطار يستدعي أسلوب الإنشاء على كائن الحالة هذا.
هذا يعني أنه بعد استدعاء الأسلوب setState()
، سيقوم الإطار باستدعاء الأسلوب build(BuildContext context)
مرة أخرى ، مما سيعيد إنشاء شجرة عنصر واجهة المستخدم بالكامل . نظرًا لأن قيمة الحقل _isLoading
تغيرت إلى true
، فبدلاً من الأسلوب _buildLoading()
سيتم استدعاء طريقة _buildLoading()
، وسيتم عرض مؤشر التقدم على الشاشة.
بالضبط نفس الشيء سيحدث عندما نحصل على رد من getUser()
الطريقة
setState()
لتعيين قيم جديدة إلى حقول _user
و _user
.
widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); });
الايجابيات
- عتبة دخول منخفضة.
- لا مكتبات طرف ثالث المطلوبة.
سلبيات
- عندما تتغير حالة عنصر واجهة المستخدم ، يتم إعادة إنشاء شجرة عنصر واجهة المستخدم بالكامل في كل مرة.
- ينتهك مبدأ المسؤولية الوحيدة. عنصر واجهة المستخدم مسؤول ليس فقط عن إنشاء واجهة المستخدم ، ولكن أيضًا عن تحميل البيانات ومنطق الأعمال وإدارة الحالة.
- يتم اتخاذ القرارات المتعلقة بكيفية عرض الحالة الحالية مباشرة في رمز واجهة المستخدم. إذا أصبحت الحالة أكثر تعقيدًا ، فسوف تقل قابلية قراءة التعليمات البرمجية إلى حد كبير.
نموذج النطاق
Scoped Model هي مكتبة تابعة لجهة خارجية . إليك كيف يصفه المطورون:
مجموعة من الأدوات المساعدة التي تسمح لك بنقل نموذج البيانات لعنصر السلف إلى جميع أحفاده. بالإضافة إلى ذلك ، عندما تتغير بيانات النموذج ، سيتم إعادة إنشاء جميع الأحفاد التي تستخدم النموذج. أخذت هذه المكتبة في الأصل من رمز مشروع Fuchsia .
دعنا ننشئ نفس الشاشة كما في المثال السابق ، ولكن باستخدام نموذج النطاق. أولاً ، نحتاج إلى إضافة مكتبة Scoped Model إلى المشروع. أضف تبعية scoped_model
إلى ملف scoped_model
في قسم dependencies
.
scoped_model: ^1.0.1
دعونا نلقي نظرة على كود UserModelScreen
ومقارنته بالمثال السابق ، الذي لم نستخدم فيه Scoped Model. لجعل نموذجنا في متناول أحفاد عنصر واجهة المستخدم ، نحتاج إلى التفاف عنصر واجهة المستخدم والطراز في ScopedModel
.
class UserModelScreen extends StatefulWidget { UserModelScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserModelScreenState(); } class _UserModelScreenState extends State<UserModelScreen> { UserModel _userModel; @override void initState() { _userModel = UserModel(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return ScopedModel( model: _userModel, child: Scaffold( appBar: AppBar( title: const Text('Scoped model'), ), body: SafeArea( child: ScopedModelDescendant<UserModel>( builder: (context, child, model) { if (model.isLoading) { return _buildLoading(); } else { if (model.user != null) { return _buildContent(model); } else { return _buildInit(model); } } }, ), ), ), ); } Widget _buildInit(UserModel userModel) { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { userModel.loadUserData(); }, ), ); } Widget _buildContent(UserModel userModel) { return Center( child: Text('Hello ${userModel.user.name} ${userModel.user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } }
في المثال السابق ، في كل مرة تتغير فيها عنصر واجهة المستخدم ، تمت إعادة إنشاء شجرة عنصر واجهة المستخدم بالكامل. ولكن هل نحتاج حقًا إلى إعادة إنشاء شجرة القطعة بالكامل (ملء الشاشة)؟ على سبيل المثال ، لا يتغير AppBar على الإطلاق ، ولا يوجد أي فائدة في إعادة إنشائه. من الناحية المثالية ، يجب عليك إعادة إنشاء الأدوات المصغّرة فقط التي يجب أن تتغير وفقًا لتغيير الحالة. ويمكن أن يساعدنا نموذج النطاق في حل هذه المشكلة.
يتم استخدام عنصر واجهة ScopedModelDescendant<UserModel>
للعثور على UserModel
في شجرة عنصر واجهة المستخدم. سيتم إعادة إنشائه تلقائيًا في كل مرة يخطر فيها UserModel
حدوث تغيير.
تحسين آخر هو أن UserModelScreen
لم يعد مسؤولاً عن إدارة الدولة ومنطق العمل وتحميل البيانات.
لنلقِ نظرة على الكود الخاص بفئة UserModel
.
class UserModel extends Model { UserModel(this._repository); final Repository _repository; bool _isLoading = false; User _user; User get user => _user; bool get isLoading => _isLoading; void loadUserData() { _isLoading = true; notifyListeners(); _repository.getUser().then((user) { _user = user; _isLoading = false; notifyListeners(); }); } static UserModel of(BuildContext context) => ScopedModel.of<UserModel>(context); }
UserModel
يحتوي الآن ويدير الدولة. لإعلام المستمعين (وإعادة إنشاء أحفادهم) بحدوث تغيير ، يجب عليك استدعاء الأسلوب notifyListeners()
.
الايجابيات
- إدارة الدولة ومنطق الأعمال وتحميل البيانات منفصلة عن رمز واجهة المستخدم.
- عتبة دخول منخفضة.
سلبيات
- تبعية مكتبة الطرف الثالث.
- إذا أصبح النموذج معقدًا بدرجة كافية ، فسيكون من الصعب تتبع متى يكون من الضروري حقًا استدعاء طريقة
notifyListeners()
لتجنب الاستجمام غير الضروري.
BLOC
BLoC ( B usiness Logic C omponents) هو نمط موصى به من قبل المطورين من Google. يتم استخدام التدفقات لإدارة الحالة والإبلاغ عن تغيير الحالة.
لمطوري Android: يمكنك أن تتخيل أن Bloc
هو StreamController
، وأن StreamController
هو LiveData
. هذا سيجعل الكود التالي سهل الفهم ، لأنك على دراية بالمبادئ الأساسية.
class UserBloc { UserBloc(this._repository); final Repository _repository; final _userStreamController = StreamController<UserState>(); Stream<UserState> get user => _userStreamController.stream; void loadUserData() { _userStreamController.sink.add(UserState._userLoading()); _repository.getUser().then((user) { _userStreamController.sink.add(UserState._userData(user)); }); } void dispose() { _userStreamController.close(); } } class UserState { UserState(); factory UserState._userData(User user) = UserDataState; factory UserState._userLoading() = UserLoadingState; } class UserInitState extends UserState {} class UserLoadingState extends UserState {} class UserDataState extends UserState { UserDataState(this.user); final User user; }
يُظهر الرمز أنه لم تعد هناك حاجة إلى استدعاء طرق إضافية للإبلاغ عن تغييرات الحالة.
لقد أنشأت 3 فئات لتمثيل الحالات المحتملة:
UserInitState
للحالة عندما يفتح المستخدم شاشة مع زر في الوسط.
UserLoadingState
للحالة عندما يتم عرض مؤشر التحميل أثناء تحميل البيانات.
UserDataState
للحالة عندما يتم بالفعل تحميل البيانات وعرضها على الشاشة.
يتيح لنا تمرير الحالة بهذه الطريقة التخلص تمامًا من المنطق في رمز واجهة المستخدم. في مثال Scoped Model ، ما زلنا نتحقق مما إذا كانت قيمة حقل _isLoading
true
أم false
لتحديد الأداة التي يجب إنشاؤها. في حالة BLoC ، نقوم بتمرير الحالة الجديدة إلى الدفق ، والمهمة الوحيدة UserBlocScreen
واجهة مستخدم UserBlocScreen
إنشاء واجهة مستخدم للحالة الحالية.
class UserBlocScreen extends StatefulWidget { UserBlocScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserBlocScreenState(); } class _UserBlocScreenState extends State<UserBlocScreen> { UserBloc _userBloc; @override void initState() { _userBloc = UserBloc(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Bloc'), ), body: SafeArea( child: StreamBuilder<UserState>( stream: _userBloc.user, initialData: UserInitState(), builder: (context, snapshot) { if (snapshot.data is UserInitState) { return _buildInit(); } if (snapshot.data is UserDataState) { UserDataState state = snapshot.data; return _buildContent(state.user); } if (snapshot.data is UserLoadingState) { return _buildLoading(); } }, ), ), ); } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { _userBloc.loadUserData(); }, ), ); } Widget _buildContent(User user) { return Center( child: Text('Hello ${user.name} ${user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } @override void dispose() { _userBloc.dispose(); super.dispose(); } }
إن رمز عنصر واجهة مستخدم UserBlocScreen
أبسط مما كان عليه في الأمثلة السابقة. من أجل الاستماع إلى التغييرات الدولة ، يتم استخدام StreamBuilder . StreamBuilder
عبارة عن StatefulWidget
يقوم بإنشاء نفسه وفقًا للقيمة الأخيرة (لقطة) للتيار ( Stream ).
الايجابيات
- لا مكتبات طرف ثالث المطلوبة.
- منطق الأعمال وإدارة الحالة وتحميل البيانات منفصلان عن رمز واجهة المستخدم.
- رد الفعل ليست هناك حاجة لاستدعاء طرق إضافية ، كما في المثال مع
notifyListeners()
Model notifyListeners()
.
سلبيات
- عتبة الدخول أعلى قليلاً. بحاجة إلى تجربة مع تيارات أو rxdart.
الروابط
يمكنك قراءة الكود الكامل عن طريق تنزيله من مستودع جيثب الخاص بي .
المقالة الأصلية نشرت على متوسط