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

البيانات
لغرض البساطة ، قمت بإنشاء فئة Repository
تحتوي على طريقة getUser()
تحاكي مكالمة شبكة غير متزامنة وتعيد كائن Future<User>
بقيم مضغوطة.
إذا لم تكن معتادًا على العقود المستقبلية والبرمجة غير المتزامنة في Dart ، فيمكنك معرفة المزيد عنها من خلال اتباع هذا البرنامج التعليمي وقراءة مستند .
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)
المستخدم ، يتم استدعاء أسلوب الإنشاء build(BuildContext context)
لإنشاء واجهة المستخدم. يتم اتخاذ جميع القرارات المتعلقة بالحاجيات المصغّرة التي يجب إنشاؤها لتمثيل الحالة الحالية في رمز إعلان واجهة المستخدم.
body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), )
من أجل عرض مؤشر التقدم عند قيام المستخدم بالنقر فوق الزر "تحميل تفاصيل المستخدم" ، فإننا نتبع ذلك.
setState(() { _isLoading = true; });
يقوم setState () باستدعاء إطار العمل بأن الحالة الداخلية لهذا الكائن قد تغيرت بطريقة قد تؤثر على واجهة المستخدم في هذه الشجرة الفرعية ، مما يتسبب في قيام إطار العمل بجدولة بناء لكائن الحالة هذا.
هذا يعني أنه بعد استدعاء setState()
أسلوب أسلوب build(BuildContext context)
يتم استدعاؤه بواسطة الإطار مرة أخرى ويتم إعادة إنشاء شجرة عنصر واجهة المستخدم بالكامل . نظرًا _isLoading
تم الآن تعيين _buildLoading()
الطريقة true
يتم استدعاء _buildBody()
بدلاً من _buildBody()
ويتم عرض مؤشر التحميل على الشاشة. بالضبط نفس الشيء يحدث عندما نتعامل مع رد الاتصال من getUser()
وندعو setState()
لإعادة تعيين _user
وحقول _user
.
widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); });
الايجابيات
- سهلة التعلم والفهم.
- لا توجد مكتبات خارجية مطلوبة.
سلبيات
- يتم إعادة بناء شجرة عنصر واجهة المستخدم بالكامل في كل مرة تتغير فيها حالة عنصر واجهة المستخدم.
- إنه يخرق مبدأ المسؤولية الفردية. القطعة ليست مسؤولة فقط عن بناء واجهة المستخدم ، بل هي أيضًا مسؤولة عن تحميل البيانات ، ومنطق الأعمال وإدارة الحالة.
- يتم اتخاذ القرارات المتعلقة بكيفية تمثيل الحالة الحالية في رمز إعلان واجهة المستخدم. إذا كان لدينا قليلا أكثر تعقيدا قراءة رمز الدولة ستنخفض.
نموذج النطاق
Scoped Model هي حزمة خارجية لا يتم تضمينها في إطار Flutter. هكذا يصفه مطورو Scoped Model:
مجموعة من الأدوات المساعدة التي تسمح لك بتمرير نموذج بيانات بسهولة من أداة الأصل إلى الأسفل. بالإضافة إلى ذلك ، يقوم أيضًا بإعادة إنشاء كافة الأطفال الذين يستخدمون النموذج عند تحديث النموذج. تم استخراج هذه المكتبة في الأصل من قاعدة الكود الفوشية.
دعونا بناء نفس الشاشة باستخدام نموذج النطاق. أولاً ، نحتاج إلى تثبيت حزمة scoped_model
Model عن طريق إضافة تبعية pubspec.yaml
إلى pubspec.yaml
ضمن قسم dependencies
.
scoped_model: ^1.0.1
دعنا نلقي نظرة على أداة UserModelScreen
ومقارنتها بالمثال السابق الذي تم تصميمه دون استخدام Scoped Model. دعنا نلقي نظرة على أداة 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 على الإطلاق ، لذلك لا فائدة من إعادة بنائه. من الناحية المثالية ، يجب علينا إعادة إنشاء عناصر واجهة المستخدم التي تم تحديثها فقط. يمكن أن يساعدنا Scoped Model في حل ذلك.
يتم استخدام عنصر واجهة ScopedModelDescendant<UserModel>
للعثور على UserModel
في شجرة Widget. سيتم إعادة بنائه تلقائيًا كلما 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
إذا كان _isLoading
true
في رمز إعلان واجهة المستخدم لتحديد الأداة التي يجب تقديمها. في حالة استخدام 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
رمز UserBlocScreen
أكثر بساطة مقارنة بالأمثلة السابقة. للاستماع إلى تغييرات الحالة ، نستخدم StreamBuilder . StreamBuilder
هو StatefulWidget
الذي يبني نفسه على أساس أحدث لقطة من التفاعل مع Stream .
الايجابيات
لا حاجة إلى مكتبات طرف ثالث.
منطق الأعمال ، وإدارة الدولة وفصل منطق واجهة المستخدم.
انها رد الفعل. ليست هناك حاجة إلى مكالمات إضافية كما هو الحال في حالة notifyListeners()
Model's notifyListeners()
.
سلبيات
تجربة العمل مع التدفقات أو rxdart مطلوب.
الروابط
يمكنك التحقق من الكود المصدري للأمثلة المذكورة أعلاه من نموذج جيثب الريبو هذا.
تم نشر المقالة في الأصل على " متوسطة"