
مبادئ عامة
Flutter هو إطار تفاعلي ، وبالنسبة لمطور متخصص في التنمية المحلية ، قد تكون فلسفته غير عادية. لذلك ، نبدأ بمراجعة قصيرة.
تتكون واجهة المستخدم على Flutter ، كما هو الحال في معظم الأطر الحديثة ، من شجرة من المكونات (الحاجيات). عندما يتغير أحد المكونات ، يتم إعادة عرض هذا وكافة مكوناته الفرعية (مع التحسينات الداخلية الموضحة أدناه). عندما تتغير الشاشة بشكل عام (على سبيل المثال ، تحويل الشاشة) ، يتم إعادة رسم شجرة الأداة بالكامل.
قد يبدو هذا النهج غير فعال ، ولكنه في الواقع يمنح المبرمج التحكم في سرعة العمل. إذا قمت بتحديث الواجهة على أعلى مستوى دون الحاجة ، فسيعمل كل شيء ببطء ، ولكن مع التصميم الصحيح للأدوات ، يمكن أن تكون التطبيقات على Flutter سريعة جدًا.
يحتوي Flutter على نوعين من الأدوات - عديمي الجنسية والدولة. السابق (مشابه للمكونات النقية في التفاعل) ليس له حالة ويتم وصفه بالكامل بواسطة معلماته. إذا لم تتغير ظروف العرض (على سبيل المثال ، حجم المنطقة التي يجب أن تعرض فيها الأداة) ومعلماتها ، فإن النظام يعيد استخدام العرض المرئي الذي تم إنشاؤه مسبقًا للأداة ، لذلك فإن استخدام عناصر واجهة Stateless يكون له تأثير جيد على الأداء. في هذه الحالة ، على أي حال ، في كل مرة يتم إعادة رسم عنصر واجهة المستخدم ، يتم إنشاء كائن جديد رسميًا ويتم تشغيل المُنشئ.
تحتفظ الحاجيات ذات الحالة ببعض الحالة بين الأداءات. للقيام بذلك ، يتم وصفها بواسطة فئتين. يصف الأول من الفئات ، عنصر واجهة المستخدم نفسه ، الكائنات التي تم إنشاؤها أثناء كل عرض. تصف الفئة الثانية حالة الأداة ويتم نقل كائناتها إلى كائنات عنصر واجهة التعامل التي تم إنشاؤها. الحاجيات الدولة ذات الولاية هي مصدر رئيسي لإعادة رسم الواجهة. للقيام بذلك ، تحتاج إلى تغيير خصائصه داخل المكالمة إلى طريقة SetState. وبالتالي ، على عكس العديد من الأطر الأخرى ، لا يحتوي Flutter على تتبع حالة ضمني - أي تغيير في خصائص الأداة خارج طريقة SetState لا يؤدي إلى إعادة رسم الواجهة.
الآن ، بعد وصف الأساسيات ، يمكنك البدء بتطبيق بسيط يستخدم عناصر واجهة مستخدم Stateless و Stateful:
تطبيق أساسيimport 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new MyHomePage(), ), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { Random rand = Random(); @override Widget build(BuildContext context) { return new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }); } }
مثال كامل
إذا كنت بحاجة إلى شروط أكثر عنادا
دعنا ننتقل. يتم الحفاظ على حالة الأدوات المصغرة بين واجهات إعادة الرسم ، ولكن فقط طالما كانت هناك حاجة للأداة ، أي يقع بالفعل على الشاشة. دعونا نجري تجربة بسيطة - ضع قائمتنا على علامة التبويب:
تطبيق Tab class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { Random rand = Random(); TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } }
مثال كامل
عند بدء التشغيل ، يمكنك أن ترى أنه عند التبديل بين علامات التبويب ، يتم حذف الحالة (تسمى طريقة التخلص ()) ، عندما يتم إرجاعها ، يتم إنشاؤها مرة أخرى (طريقة initState ()). هذا أمر معقول ، لأن تخزين حالة الأدوات غير القابلة للعرض سيستهلك موارد النظام. في الحالة التي يجب أن تنجو فيها حالة الأداة من إخفاءها الكامل ، هناك عدة طرق ممكنة:
أولاً ، يمكنك استخدام كائنات منفصلة (ViewModel) لتخزين الحالة. يدعم Dart على مستوى اللغة منشئي المصانع التي يمكن استخدامها لإنشاء مصانع وعلامات فردية تقوم بتخزين البيانات اللازمة.
أنا أحب هذا النهج أكثر ، لأنه يسمح لك بعزل منطق الأعمال عن واجهة المستخدم. هذا صحيح بشكل خاص بسبب حقيقة أن Flutter Release Preview 2 أضاف القدرة على إنشاء واجهات مثالية للبكسل لنظام iOS ، لكنك تحتاج إلى القيام بذلك ، بالطبع ، على الحاجيات المقابلة.
ثانيًا ، من الممكن استخدام نهج رفع الحالة ، المألوف لدى المبرمجين المتفاعلين ، عندما يتم تخزين البيانات في المكونات الموجودة في المنبع. نظرًا لأن Flutter يعيد رسم الواجهة فقط عند استدعاء طريقة setState () ، يمكن تغيير هذه البيانات واستخدامها دون التقديم. هذا النهج أكثر تعقيدًا إلى حد ما ويزيد من اتصال الأدوات في الهيكل ، ولكنه يسمح لك بتحديد مستوى تخزين البيانات بشكل نقطي.
أخيرًا ، هناك مكتبات تخزين حكومية مثل flutter_redux .
من أجل البساطة ، نستخدم النهج الأول. دعونا نجعل فئة ListData منفصلة ، singleton ، والتي تخزن القيم لقائمتنا. عند العرض ، سنستخدم هذه الفئة.
تطبيق استعادة البيانات المبوب class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: ListData().build), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListData { static ListData _instance = ListData._internal(); ListData._internal(); factory ListData() { return _instance; } Random _rand = Random(); Map<int, int> _values = new Map(); Widget build (BuildContext context, int index) { if (!_values.containsKey(index)) { _values[index] = _rand.nextInt(100); } return Text('Random number ${_values[index]}',); } }
مثال كامل
حفظ موضع التمرير
إذا قمت بالتمرير لأسفل القائمة من المثال السابق ، ثم قم بالتبديل بين علامات التبويب ، فمن السهل ملاحظة أنه لم يتم حفظ موضع التمرير. هذا أمر منطقي ، لأنه لا يتم تخزينه في فئة ListData الخاصة بنا ، ولا تستمر حالة الأداة الخاصة بالتبديل بين التبديل بين علامات التبويب. ننفذ تخزين حالة التمرير يدويًا ، ولكن من أجل المتعة ، لن نضيفها إلى فئة منفصلة وليس إلى ListData ، ولكن إلى حالة مستوى أعلى لإظهار كيفية العمل معها.
انتبه إلى عناصر واجهة مستخدم ScrollController و NotificationListener (بالإضافة إلى أدوات DefaultTabController المستخدمة سابقًا). يجب أن يكون مفهوم الأدوات التي لا تحتوي على شاشة عرض خاصة بها مألوفًا للمطورين الذين يعملون مع React / Redux - يتم استخدام مكونات الحاوية بنشاط في هذه الحزمة. في Flutter ، يتم استخدام عناصر واجهة المستخدم غير المعروضة بشكل شائع لإضافة وظائف إلى عناصر واجهة مستخدم فرعية. هذا يسمح لك بترك الأدوات المصغرة نفسها خفيفة الوزن وليس معالجة أحداث النظام حيث لا تكون هناك حاجة إليها.
يعتمد الكود على الحل الذي اقترحه Marcin Szałek في Stakoverflow ( https://stackoverflow.com/questions/45341721/flutter-listview-inside-on-a-tabbarview-loses-its-scroll-position ). الخطة كما يلي:
- أضف ScrollController إلى القائمة للعمل مع موضع التمرير.
- أضف NotificationListener إلى القائمة لتمرير حالة التمرير.
- نقوم بحفظ موضع التمرير في _MyHomePageState (وهو مستوى أعلى من علامات التبويب) وربطه بتمرير القائمة.
تطبيق مع حفظ موقف التمرير class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { double listViewOffset=0.0; TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [new ListTab( getOffsetMethod: () => listViewOffset, setOffsetMethod: (offset) => this.listViewOffset = offset, ), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListTab extends StatefulWidget { ListTab({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key); final GetOffsetMethod getOffsetMethod; final SetOffsetMethod setOffsetMethod; @override _ListTabState createState() => _ListTabState(); } class _ListTabState extends State<ListTab> { ScrollController scrollController; @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); } @override Widget build(BuildContext context) { return NotificationListener( child: new ListView.builder( controller: scrollController, itemBuilder: ListData().build, ), onNotification: (notification) { if (notification is ScrollNotification) { widget.setOffsetMethod(notification.metrics.pixels); } }, ); } }
مثال كامل
تواجه إيقاف تشغيل التطبيق
يعد حفظ المعلومات طوال مدة التطبيق أمرًا جيدًا ، ولكن غالبًا ما تريد حفظه بين الجلسات ، خاصة بالنظر إلى عادة أنظمة التشغيل لإغلاق تطبيقات الخلفية عندما لا تتوفر ذاكرة كافية. الخيارات الرئيسية لتخزين البيانات المستمر في Flutter هي:
- التفضيلات المشتركة ( https://pub.dartlang.org/packages/shared_preferences ) هي التفاف حول NSUserDefaults (على iOS) و SharedPreferences (على Android) ويسمح لك بتخزين عدد صغير من أزواج قيمة المفتاح. عظيم لتخزين الإعدادات.
- sqflite ( https://pub.dartlang.org/packages/sqflite ) هو مكون إضافي للعمل مع SQLite (مع بعض القيود). يدعم كلا من الاستعلامات والمساعدين على مستوى منخفض. بالإضافة إلى ذلك ، عن طريق القياس مع Room ، فإنه يسمح لك بالعمل مع إصدارات مخطط قاعدة البيانات وتعيين التعليمات البرمجية لتحديث المخطط عند تحديث التطبيق.
- Cloud Firestore ( https://pub.dartlang.org/packages/cloud_firestore ) هو جزء من عائلة مكونات FireBase الرسمية.
للتوضيح ، سنحفظ حالة التمرير في التفضيلات المشتركة. للقيام بذلك ، قم بإضافة استعادة موضع التمرير عند تهيئة حالة _MyHomePageState والحفظ عند التمرير.
هنا نحتاج إلى الخوض في نموذج Flutter / Dart غير المتزامن ، حيث تعمل جميع الخدمات الخارجية على المكالمات غير المتزامنة. يشبه مبدأ تشغيل هذا النموذج node.js - هناك خيط رئيسي واحد للتنفيذ (خيط) ، يتم مقاطعته بواسطة المكالمات غير المتزامنة. في كل مقاطعة تالية (وتجعلها واجهة المستخدم تعمل باستمرار) ، تتم معالجة نتائج العمليات غير المتزامنة المكتملة. وفي الوقت نفسه ، من الممكن إجراء حسابات ثقيلة في سلاسل العمليات الخلفية (عبر وظيفة الحساب).
لذلك ، تتم الكتابة والقراءة في SharedPreferences بشكل غير متزامن (على الرغم من أن المكتبة تسمح بالقراءة المتزامنة من ذاكرة التخزين المؤقت). بادئ ذي بدء ، سنتعامل مع القراءة. يبدو النهج القياسي لاسترداد البيانات غير المتزامنة على هذا النحو - ابدأ العملية غير المتزامنة ، وعند اكتمالها ، قم بتنفيذ SetState ، وكتابة القيم المستلمة. ونتيجة لذلك ، سيتم تحديث واجهة المستخدم باستخدام البيانات المستلمة. ومع ذلك ، في هذه الحالة ، نحن لا نعمل مع البيانات ، ولكن مع وضع التمرير. لسنا بحاجة إلى تحديث الواجهة ، نحتاج فقط إلى استدعاء طريقة jumpTo على ScrollController. تكمن المشكلة في أن نتيجة معالجة طلب غير متزامن يمكن أن تعود في أي وقت ولن تكون بالضرورة مكان التمرير ومكانه. من أجل ضمان إجراء عملية على واجهة تمت تهيئتها بالكامل ، نحتاج إلى ... الاستمرار في التمرير داخل setState.
نحصل على شيء مثل هذا الرمز:
إعداد الدولة @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); _restoreState().then((double value) => scrollController.jumpTo(value)); } Future<double> _restoreState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getDouble('listViewOffset'); } void setScroll(double value) { setState(() { scrollController.jumpTo(value); }); }
مع التسجيل ، كل شيء أكثر إثارة للاهتمام. والحقيقة هي أنه في عملية التمرير ، تأتي تقارير الأحداث حول هذا باستمرار. قد يؤدي بدء التسجيل غير المتزامن في كل مرة يتم فيها تغيير القيمة إلى حدوث أخطاء في التطبيق. نحتاج إلى معالجة الحدث الأخير من السلسلة فقط. فيما يتعلق بالبرمجة التفاعلية ، وهذا ما يسمى debounce وسنستخدمه. يدعم Dart الميزات الرئيسية للبرمجة التفاعلية من خلال تدفقات البيانات ، لذلك سنحتاج إلى إنشاء دفق من تحديثات موضع التمرير والاشتراك فيه ، وتحويله باستخدام Debounce. للتحويل ، نحن بحاجة إلى مكتبة stream_transform . كنهج بديل ، يمكنك استخدام RxDart والعمل من حيث ReactiveX.
اتضح الرمز التالي:
سجل الحالة StreamSubscription _stream; StreamController<double> _controller = new StreamController<double>.broadcast(); @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); _stream = _controller.stream.transform(debounce(new Duration(milliseconds: 500))).listen(_saveState); } void _saveState(double value) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setDouble('listViewOffset', value); }
مثال كامل