تطبيق رفرفة التطبيق 101: الفانيليا ، نموذج النطاق ، BLOC


(نشرت أصلا على متوسطة )


يوفر Flutter إطارًا حديثًا لأسلوب رد الفعل ، ومجموعة وأدوات غنية للأدوات الذكية ، ولكن لا يوجد شيء مماثل لدليل Android لهندسة التطبيقات .


في الواقع ، لا توجد بنية أساسية تلبي جميع المتطلبات الممكنة ، ولكن دعونا نواجه حقيقة أن معظم تطبيقات الهاتف المحمول التي نعمل عليها لديها على الأقل بعض الوظائف التالية:


  1. طلب / تحميل البيانات من / إلى الشبكة.
  2. خريطة ، تحويل ، إعداد البيانات وتقديمها للمستخدم.
  3. ضع / احصل على البيانات من / إلى قاعدة البيانات.

مع مراعاة ذلك ، قمتُ بإنشاء تطبيق نموذج يعمل على حل المشكلة نفسها تمامًا باستخدام ثلاثة أساليب مختلفة للهيكل.


يتم تقديم المستخدم مع زر "تحميل بيانات المستخدم" في وسط الشاشة. عند قيام المستخدم بالنقر فوق الزر ، يتم تشغيل تحميل البيانات غير المتزامن ويتم استبدال الزر بمؤشر تحميل. بعد تحميل البيانات ، يتم استبدال مؤشر التحميل بالبيانات.


لنبدأ.



البيانات


لغرض البساطة ، قمت بإنشاء فئة 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; }); }); 

الايجابيات


  1. سهلة التعلم والفهم.
  2. لا توجد مكتبات خارجية مطلوبة.

سلبيات


  1. يتم إعادة بناء شجرة عنصر واجهة المستخدم بالكامل في كل مرة تتغير فيها حالة عنصر واجهة المستخدم.
  2. إنه يخرق مبدأ المسؤولية الفردية. القطعة ليست مسؤولة فقط عن بناء واجهة المستخدم ، بل هي أيضًا مسؤولة عن تحميل البيانات ، ومنطق الأعمال وإدارة الحالة.
  3. يتم اتخاذ القرارات المتعلقة بكيفية تمثيل الحالة الحالية في رمز إعلان واجهة المستخدم. إذا كان لدينا قليلا أكثر تعقيدا قراءة رمز الدولة ستنخفض.

نموذج النطاق


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() .


الايجابيات


  1. منطق الأعمال ، وإدارة الدولة وفصل رمز واجهة المستخدم.
  2. سهل التعلم.

    سلبيات

  3. يتطلب مكتبة طرف ثالث.
  4. نظرًا لأن النموذج يزداد تعقيدًا ، من الصعب تتبعه عندما يتعين عليك الاتصال بـ 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 مطلوب.



يمكنك التحقق من الكود المصدري للأمثلة المذكورة أعلاه من نموذج جيثب الريبو هذا.


تم نشر المقالة في الأصل على " متوسطة"

Source: https://habr.com/ru/post/ar438524/


All Articles