فصل الاهتمامات في تطبيقات الرفرفة
Engineering at ClickUp

فصل الاهتمامات في تطبيقات الرفرفة

في الآونة الأخيرة، اضطررتُ مؤخرًا إلى تنفيذ إرشادات إرشادية حول الإعداد لـ انقر فوق الوافدون الجدد لقد كانت هذه مهمة مهمة مهمة حقًا لأن الكثير من المستخدمين الجدد كانوا على وشك اكتشاف المنصة مع الإعلان المضحك بشكل لا يصدق الذي عرضناه لأول مرة في السوبر بول ! ✨

عبر ClickUp

تسمح هذه الإرشادات التفصيلية للعديد من المستخدمين الجدد، الذين ربما لا يعرفون ClickUp حتى الآن، بفهم كيفية استخدام العديد من وظائف التطبيق بسرعة. إنه جهد مستمر، تمامًا مثل الجديد جامعة ClickUp المورد الذي نتابعه! 🚀

لحسن الحظ، سمحت لي البنية البرمجية الكامنة وراء تطبيق ClickUp Flutter للهاتف المحمول بتنفيذ هذه الوظيفة بسرعة كبيرة، حتى من خلال إعادة استخدام الأدوات الحقيقية من التطبيق! وهذا يعني أن الإرشادات التفصيلية ديناميكية وسريعة الاستجابة وتتطابق تمامًا مع شاشات التطبيق الحقيقية للتطبيق - وستستمر كذلك حتى عندما تتطور الأدوات المصغّرة.

تمكنت أيضًا من تنفيذ الوظيفة بسبب الفصل الصحيح بين الاهتمامات.

لنرى ما أعنيه هنا. 🤔

فصل الاهتمامات

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

الفائدة الرئيسية لإنشاء أنظمة صغيرة مفصولة هي بلا شك قابلية الاختبار! وهذا ما ساعدني في إنشاء بديل تجريبي للشاشات الموجودة من التطبيق!

دليل خطوة بخطوة

الآن، كيف يمكننا تطبيق هذه المبادئ على تطبيق Flutter؟

سنقوم بمشاركة بعض التقنيات التي نستخدمها لبناء ClickUp مع مثال تفصيلي بسيط.

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

التطبيق

كمثال، سنقوم بإنشاء تطبيق يعرض عدد سكان الولايات المتحدة الأمريكية لكل عام.

عبر ClickUp

لدينا شاشتان هنا :

  • 'الشاشة الرئيسية': تسرد ببساطة جميع السنوات من عام 2000 إلى الآن. عندما ينقر المستخدمون على لوحة سنة ما، سينتقلون إلى "شاشة التفاصيل" مع وسيطة تنقل إلى السنة المحددة.
  • تحصل 'شاشة التفاصيل': تحصل على السنة من وسيطة التنقل، وتستدعي وسيطة التنقلواجهة برمجة تطبيقات datausa.io لهذه السنة، ويحلل بيانات JSON لاستخراج القيمة السكانية المرتبطة بها. إذا كانت البيانات متوفرة، يتم عرض تسمية مع عدد السكان.

سنركز على تطبيق 'DetailScreen' نظرًا لأنه الأكثر إثارة للاهتمام من خلال استدعائه غير المتزامن.

الخطوة 1. النهج الساذج

النهج الساذج القطعة الساذجة

عبر ClickUp

التنفيذ الأكثر وضوحًا لتطبيقنا، هو استخدام "عنصر واجهة مستخدم واحد" واحد "StatefulWidget" للمنطق بأكمله. كود مصدر السهام والعرض التوضيحي

الوصول إلى وسيطة التنقل "عام

للوصول إلى السنة المطلوبة، نقرأ "إعدادات الطريق" من الأداة الموروثة "ModalRoute".

باطلة didChangeDependencies() {{
    super.didChangeDependencies();
    السنة النهائية = ModalRoute.of(السياق).settings.arguments كـ int;
    // ...
}

استدعاء HTTP

يستدعي هذا المثال دالة "الحصول على" من حزمة "http" للحصول على البيانات من datausa.io API ويحلل JSON الناتج باستخدام طريقة 'jsonDecode' من مكتبة 'dart:convert'، ويحتفظ بـ 'المستقبل' كجزء من الحالة بخاصية تسمى 'المستقبل'.

في وقت متأخر من المستقبل<Map<Map<dynamic, dynamic>؟


باطلة didChangeDependencies() {{
    super.didChangeDependencies();
    السنة النهائية = ModalRoute.of(context).settings.arguments كـ int;
    إذا (_year != السنة) { {
      _future = _loadMeasure(year);
    }
}


المستقبل<خريطة المستقبل<خريطة<ديناميكية، ديناميكية>؟ > _loadMeasure(int year) مزامنة {
    _year = السنة;
    نهائي uri = Uri.parse(
        'https://datausa.io/api/data?drilldowns=Nation&measures=Population&year=$year');
    النتيجة النهائية = انتظر الحصول على (uri);
    الجسم النهائي = jsonDecode(result.body);
    بيانات نهائية = body['data'] كقائمة<dynamic>;
    إذا (data.isNotEmpty) {
      إرجاع data.first;
    }
    إرجاع فارغة;

التقديم

لإنشاء شجرة الأداة، نستخدم "منشئ المستقبل"، والذي يعيد بناء نفسه فيما يتعلق بالحالة الحالية لاستدعاء "المستقبل" غير المتزامن.

تجاوز
بناء عنصر واجهة المستخدم(BuildContext context) {{
إرجاع السقالة(
    شريط التطبيق: شريط التطبيق(
    العنوان: Text('Year $_year'),
    ),
    الجسم: منشئ المستقبل <Map<Map<dynamic, dynamic>؟
    مستقبل: _المستقبل,
    المنشئ: (سياق، لقطة) {
        التبديل (snapshot.connectionState) {
        حالة ConnectionState.done:
            خطأ نهائي = خطأ في اللقطة;
            إذا (خطأ != لاغٍ) { {
                // إرجاع "خطأ" الشجرة.
            }
            البيانات النهائية = snapshot.data.data;
            إذا (البيانات != لاغية) {{
                // إرجاع شجرة "النتيجة".
            }
            // إرجاع بيانات "فارغة" الشجرة.case ConnectionState.none:
        حالة ConnectionState.waiting:
        حالة ConnectionState.active:
            // إرجاع شجرة البيانات "تحميل".
        }
    },
    ),
);
}

مراجعة

حسنًا، التنفيذ قصير ويستخدم عناصر واجهة مستخدم مدمجة فقط، ولكن فكر الآن في نيتنا الأولية: بناء بدائل (أو اختبارات) تجريبية لهذه الشاشة. من الصعب جدًا التحكم في نتيجة استدعاء HTTP لإجبار التطبيق على العرض في حالة معينة.

هذا هو المكان الذي سيساعدنا فيه مفهوم انعكاس التحكم. 🧐

الخطوة 2. عكس السيطرة

انعكاس التحكم إلى الموفر وعميل واجهة برمجة التطبيقات

عبر ClickUp

قد يكون من الصعب فهم هذا المبدأ للمطورين الجدد (وأيضًا من الصعب شرحه)، ولكن الفكرة العامة هي استخراج الاهتمامات خارج مكوناتنا -بحيث لا تكون مسؤولة عن اختيار السلوك- وتفويضه بدلًا من ذلك.

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

لكن لا تقلق، سيكون الأمر أكثر منطقية بعد المثال التالي! 👀 كود مصدر السهام والعرض التوضيحي

إنشاء كائن عميل API

من أجل التحكم بمكالمة HTTP إلى واجهة برمجة التطبيقات الخاصة بنا، قمنا بعزل تطبيقنا في فئة "DataUsaApiClient" مخصصة. لقد أنشأنا أيضًا فئة "قياس" لتسهيل معالجة البيانات وصيانتها.

فئة DataUsApiClient {
  const DataUsaApiClient({{
    نقطة النهاية هذه = 'https://datausa.io/api/data',
  });


  نقطة نهاية السلسلة النهائية;


  المستقبل<Measure?> getMeasure(int year) متزامن {
    نهائي uri =
        Uri.parse('$endpoint?drilldowns=Nation&measures=Population&year=$year');
    النتيجة النهائية = انتظر الحصول على (uri);
    الجسم النهائي = jsonDecode(result.body);
    بيانات نهائية = body['data'] كقائمة<dynamic>;
    إذا (data.isNotEmpty) {
      العودة Measure.fromJson(data.first.data.first ك Map<String, Object?>);
    }
    العودة فارغة;
  }

توفير عميل API

بالنسبة لمثالنا، نستخدم في مثالنا هذا المزود لحقن مثيل 'DataUsApiClient' في جذر شجرتنا.

موفر <DataUsApiClient>(
    إنشاء: (السياق) => const DataUsaUsApiClient(),
        التابع: const MaterialApp(
        الصفحة الرئيسية: الصفحة الرئيسية(),
    ),
)

باستخدام عميل واجهة برمجة التطبيقات

يسمح الموفر لأي عنصر واجهة مستخدم سليل (_ مثل "شاشة التفاصيل" الخاصة بنا) _ بقراءة أقرب "DataUsApiClient" العلوي في الشجرة. يمكننا بعد ذلك استخدام أسلوب 'getMeasure' الخاص به لبدء 'المستقبل' الخاص بنا، بدلاً من تنفيذ HTTP الفعلي.

@overridevoid didChangeDependencies() {
    super.didChangeDependencies();
    السنة النهائية = ModalRoute.of(context).settings.arguments كـ int;
    إذا كانت (_year != سنة) { {
      _year = سنة;
      نهائي api = context.read.read<DataUsaApiClient>();
      _future = api.getMeasure(year);
    }
}

عميل API التجريبي

الآن يمكننا الاستفادة من هذا!

في حالة عدم معرفتك: أي فئات في dart تُعرّف ضمنيًا أيضًا واجهة مرتبطة بها . يتيح لنا ذلك توفير تطبيق بديل لـ "DataUsaApiClient" والذي يُرجع دائمًا نفس المثيل من استدعاءات أسلوب "الحصول على القياس".

هذا الأسلوب

تنفذ فئة DemoDataDataUsaUsaApiClient {
  يشكل DemoDataUsaUsaApiClient(هذا.measure);


  مقياس نهائي قياس;


  @overrideString get endpoint => ''';


  @تجاوز
  المستقبل<Measure?> getMeasure(int year) {
    إرجاع Future.value(قياس);
  }

عرض صفحة تجريبية

لدينا الآن جميع المفاتيح اللازمة لعرض مثيل تجريبي لـ 'صفحة التفاصيل'!

نحن ببساطة نتجاوز ببساطة مثيل "DataUsApiClient" المتوفر حاليًا عن طريق تغليف "شاشة التفاصيل" الخاصة بنا في موفر يقوم بإنشاء مثيل "DemoDataUsApiClient" بدلاً من ذلك!

وهذا كل شيء - تقرأ "شاشة التفاصيل" الخاصة بنا هذا المثيل التجريبي بدلاً من ذلك، وتستخدم بيانات "demoMeasure" بدلاً من استدعاء HTTP.

ListTileTile(
    العنوان: const Text('فتح العرض التوضيحي'),
    onTap: () {) {
        const demoMeasure = قياس(
            السنة: 2022,
            عدد السكان: 425484,
            الأمة: 'الولايات المتحدة',
        );
        Navigator.push(
            السياق,
            MaterialPageRoute(
                settings: RouteSettings(الوسيطات: demoMeasure.year),
                المنشئ: (السياق) {
                    إرجاع موفر<DataUsaApiClient>(
                        إنشاء: (السياق) => (السياق) =>
                            const DemoDataUsaUsApiClient(demoMeasure),
                        التابع: const DetailScreen(),
                    );
                },
            ),
        );
    },
)

مراجعة

هذا مثال رائع لـ انعكاس التحكم. لم تعد أداة 'DetailScreen' الخاصة بنا مسؤولة عن منطق الحصول على البيانات بعد الآن، ولكنها تفوضها بدلاً من ذلك إلى كائن عميل مخصص. ونحن الآن قادرون على إنشاء مثيلات تجريبية للشاشة، أو تنفيذ اختبارات الأداة الذكية لشاشتنا! رائع! 👏

ولكن يمكننا القيام بما هو أفضل!

بما أننا غير قادرين على محاكاة حالة التحميل، على سبيل المثال، ليس لدينا تحكم كامل في أي تغيير في الحالة على مستوى القطعة.

الخطوة 3. إدارة الحالة

إدارة البيانات في تطبيقات الرفرفة

عبر ClickUp

هذا موضوع ساخن في Flutter!

أنا متأكد من أنك قرأت بالفعل خيوطًا طويلة من الأشخاص الذين يحاولون اختيار أفضل حل لإدارة الحالة لـ Flutter. ولكي نكون واضحين، هذا ليس ما سنفعله في هذه المقالة. في رأينا، طالما أنك تفصل منطق عملك عن المنطق المرئي الخاص بك، فأنت بخير! إنشاء هذه الطبقات مهم حقًا لقابلية الصيانة. مثالنا بسيط، ولكن في التطبيقات الحقيقية، يمكن أن يصبح المنطق معقدًا بسرعة، ومثل هذا الفصل يجعل من السهل جدًا العثور على خوارزمياتك المنطقية البحتة. غالبًا ما يُلخَّص هذا الموضوع بـ إدارة الحالة.

في هذا المثال، نستخدم في هذا المثال 'ValueNotifier' الأساسي إلى جانب 'موفر'. ولكن كان بإمكاننا أيضًا استخدام flutter\bloc أو ريفربود (أو حل آخر)، وكان سيعمل بشكل رائع أيضًا. تظل المبادئ كما هي، وطالما أنك فصلت بين حالاتك ومنطقك، فمن الممكن حتى نقل قاعدة شفرتك من أحد الحلول الأخرى.

يساعدنا هذا الفصل أيضًا على التحكم في أي حالة من حالات أدواتنا حتى نتمكن من السخرية منها بكل طريقة ممكنة! كود مصدر السهام والعرض التوضيحي

إنشاء حالة مخصصة

بدلًا من الاعتماد على 'AsyncSnapshot' من إطار العمل، نحن الآن نمثل حالة الشاشة ككائن 'DetailState'.

من المهم أيضًا تنفيذ طريقتَي "رمز التجزئة" و"المشغل ==" لجعل كائننا قابلاً للمقارنة بالقيمة. يسمح لنا ذلك باكتشاف ما إذا كان ينبغي اعتبار حالتين مختلفتين.

💡 إن قابل للمساواة أو مجمد الحزم هي خيارات رائعة لمساعدتك في تنفيذ طريقتَي 'hashCode' و 'operator =='!

فئة مجردة DetailState {
  const DetailState(this.year);
  سنة نهائية int سنة;


  @overridebool operator ==(كائن آخر) =>
      متطابق (هذا، آخر) || (الآخر) ||
      (الآخر هو DetailState &&&
          نوع وقت التشغيل = = other.runtimeType &&&
          سنة = = other.year);


  @overrideint get get hashCode => runtimeType.runType.hashCode ^ عام;

ترتبط حالتنا دائمًا بـ "سنة"، ولكن لدينا أيضًا أربع حالات محتملة مختلفة فيما يتعلق بما نريد إظهاره للمستخدم:

  • 'NotLotLoadedDetailState': لم يبدأ تحديث البيانات بعد
  • 'LoadingDetailState': البيانات قيد التحميل حاليًا
  • 'LoadedDetailState': تم تحميل البيانات بنجاح مع 'قياس' مرتبط بها
  • 'NoDataDetailState': تم تحميل البيانات، ولكن لا توجد بيانات متاحة
  • 'UnknownErrorDetailState': فشلت العملية بسبب 'خطأ' غير معروف
صنف NotLoadedDetailState امتداد لحالة التفاصيل {
  const NotLoadedDetailDetailState(int year) : سوبر(year);
}


صنف LoadedDetailState ممتد للحالة التفصيلية {
  const LoadedDetailDetailState({
    مطلوب int سنة,
    مطلوب هذا المقياس,
  }): سوبر (سنة);


  مقياس نهائي قياس;


  @overridebool operator ==(كائن آخر) =>
      متطابق (هذا، آخر) || (الآخر) ||
      (الآخر هو LoadedDetailStateState &&& قياس = = other.measure);


  @overrideint get get hashCode => runtimeType.hashCode ^ measure.hashCode;
}


صنف NoDataDetailState يمتد من الحالة التفصيلية {
  const NoDataDetailState(int year) : سوبر(year);
}


صنف LoadingDetailState يمدد حالة التفاصيل {
  const LoadingDetailState(int year) : سوبر(عام);
}


صنف UnknownErrorDetailState يمدد حالة التفاصيل {
  const UnknownErrorDetailDetailState({
    مطلوب int سنة,
    مطلوب هذا الخطأ,
  }): سوبر(السنة);


  خطأ ديناميكي نهائي;


  @overridebool operator ==(كائن آخر) =>
      متطابق (هذا، آخر) || ||
      (الآخر هو UnknownErrorDetailStateState &&&
          السنة = = other.year &&&
          خطأ = = other.error.other);


  @overrideint get get hashCode => Object.hash(super.hashCode, error.has

هذه الحالات أكثر وضوحًا من 'AsyncSnapshot'، لأنّها تمثّل حقًا حالات استخدامنا. ومجددًا، هذا يجعل شيفرتنا أكثر قابلية للصيانة!

💡 نوصي بشدة بـ أنواع الاتحاد من الحزمة المجمدة لتمثيل الحالات المنطقية الخاصة بك! تضيف الكثير من الأدوات المساعدة مثل طريقتا 'نسخ مع' أو 'خريطة'.

وضع المنطق في المخبر

الآن بعد أن أصبح لدينا تمثيل لحالتنا، نحتاج إلى تخزين مثيل لها في مكان ما - وهذا هو الغرض من 'DetailNotifier'. سيحتفظ بنموذج "حالة التفاصيل" الحالي في خاصية "القيمة" الخاصة به وسيوفر طرقًا لتحديث الحالة.

نحن نوفر حالة أولية "غير محملة لحالة التفاصيل"، وطريقة "تحديث" لتحميل البيانات من "واجهة برمجة التطبيقات" وتحديث "القيمة" الحالية.

صنف DetailNotifier يمتد ValueNotifier<DetailState> {
  مخبر التفاصيل({{
    مطلوب int سنة,
    مطلوب this.api,
  })): سوبر(DetailState.notLoaded(year));


  عميل DataUsApiClient النهائي DataUsApiClient api;


  int get get year => value.year;


  المستقبل<void> تحديث() متزامن {
    إذا كانت (القيمة هي! LoadingDetailState) { {
      القيمة = DetailState.loading(year);
      حاول {
        نتيجة نهائية = انتظر api.getMeasure(السنة);
        إذا (النتيجة != فارغة) { {
          القيمة = DetailState.load(
            السنة: السنة,
            القياس: النتيجة,
          );
        ) } أخرى {
          القيمة = DetailState.noData(السنة);
        }
      } التقاط (خطأ) {
        القيمة = DetailState.unknownError(
          السنة: السنة,
          خطأ: خطأ,
        );
      }
    }
  }

توفير حالة للعرض

لتهيئة ومراقبة حالة شاشتنا، نعتمد أيضًا على الموفر و 'موفر 'ChangeNotifierProvider' الخاص به. يبحث هذا النوع من الموفر تلقائيًا عن أي 'موفر 'ChangeListener' تم إنشاؤه وسيقوم بتشغيل إعادة بناء من 'المستهلك' في كل مرة يتم إخطاره بتغيير (عندما تكون قيمة المُنبِّه مختلفة عن القيمة السابقة).

صنف DetailScreen يمدد القطعة عديمة الحالة {
  const DetailScreen({{
    مفتاح؟ مفتاح,
  }): سوبر(المفتاح: مفتاح);


  تجاوز
  بناء القطعة(BuildContext السياق) { {
    سنة نهائية = ModalRoute.of(context).settings.arguments كـ int;
    إرجاع ChangeNotifierProvider<DetailNotifier>(
      إنشاء: (السياق) {
        مُعرِّف نهائي = مُعرِّف التفاصيل(
          السنة: السنة,
          api: context.read<DataUsaApiClient>(),
        );
        notifier.refresh();
        إرجاع المخبر;
      },
      الطفل: مستهلك<DetailNotifier>(
        المنشئ: (السياق، المُعلم، الطفل) {
             الحالة النهائية = notifier.value;
            // ...
        },
      ),
    );
  }
}

مراجعة

عظيم! بدأت بنية تطبيقنا تبدو جيدة جداً. كل شيء مقسم إلى طبقات واضحة المعالم، مع اهتمامات محددة! 🤗

لكن لا يزال هناك شيء واحد مفقود من أجل قابلية الاختبار، نريد تحديد "حالة التفاصيل" الحالية للتحكم في حالة "شاشة التفاصيل" المرتبطة بنا.

الخطوة 4. أداة تخطيط مرئية مخصصة

أدوات تخطيط مرئية مخصصة في تطبيقات الرفرفة

عبر ClickUp

في الخطوة الأخيرة، أعطينا مسؤولية كبيرة جدًا لأداة "DetailScreen" الخاصة بنا: لقد كانت مسؤولة عن إنشاء "DetailNotifier". وكما رأينا سابقًا، نحاول تجنب أي مسؤولية منطقية في طبقة العرض!

يمكننا حل هذه المشكلة بسهولة عن طريق إنشاء طبقة أخرى لأداة الشاشة لدينا: سنقوم بتقسيم أداة 'DetailScreen' إلى قسمين:

  • 'DetailScreen' مسؤولة عن إعداد التبعيات المختلفة لشاشتنا من حالة التطبيق الحالية (التنقل، المُخطِرات، الحالة، الخدمات، ...),
  • أما 'DetailLayout' فيقوم ببساطة بتحويل 'حالة التفاصيل' إلى شجرة مخصصة من الأدوات.

من خلال الجمع بين الاثنين، سنكون قادرين ببساطة على إنشاء 'DetailLayout' مثيلات تجريبية/اختبارية، ولكن مع وجود 'DetailScreen' لحالة الاستخدام الحقيقي في تطبيقنا. كود مصدر Dart وعرض توضيحي

تخطيط مخصص

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

يبقى استدعاء 'ModalRoute.of' ومثيل 'ChangeNotifierProvider' في 'شاشة التفاصيل'، وتعيد هذه الأداة ببساطة 'مخطط التفاصيل' مع شجرة تبعية مهيأة مسبقًا!

هذا التحسين البسيط خاص باستخدام الموفر، ولكنك ستلاحظ أننا أضفنا أيضًا "ProxyProvider" بحيث يمكن لأي عنصر واجهة مستخدم سليل أن يستهلك "حالة التفاصيل" مباشرةً. سيجعل هذا الأمر من الأسهل محاكاة البيانات.

صنف DetailScreen يمدد عنصر واجهة المستخدم عديم الحالة {
  const DetailScreen({{
    مفتاح؟ مفتاح,
  }): سوبر(المفتاح: مفتاح);


  تجاوز
  بناء القطعة(BuildContext السياق) { {
    سنة نهائية = ModalRoute.of(context).settings.arguments كـ int;
    إرجاع ChangeNotifierProvider<DetailNotifier>(
      إنشاء: (السياق) {
        مُعرِّف نهائي = مُعرِّف التفاصيل(
          السنة: السنة,
          api: context.read<DataUsaApiClient>(),
        );
        notifier.refresh();
        إرجاع المخبر;
      },
      التابع: التابع: ProxyProvider<DetailNotifier, DetailState>(
        تحديث: (سياق، قيمة، سابقة) => القيمة.value.value,
        الطفل: const DetailLayout(),
      ),
    );
  }
}


صنف DetailLayout يمدد StatelessWidget {
  const DetailLayout({ {
    مفتاح؟ مفتاح,
  }): ممتاز(المفتاح: مفتاح);


  تجاوز
  بناء القطعة(BuildContext السياق) { {
    إرجاع المستهلك<DetailState>(
      منشئ: (سياق، حالة، طفل) {
        إرجاع السقالة(
          appBar: شريط التطبيق(
            العنوان: Text('Year('Year ${state.year})'),
          ),
          الجسم: () {) {
              // ...
          }(),
        );
      },
    );
  }

استخراج الأدوات كفئات مخصصة

لا تتردد أبدًا في استخراج شجرة الأدوات في فئة مخصصة! سوف تحسين الأداء وجعل الكود أكثر قابلية للصيانة.

في مثالنا أنشأنا أداة تخطيط مرئية واحدة لكل نوع من أنواع الحالات المرتبطة:

إذا كانت (الحالة هي NotLoadedDetailState ||الحالة هي LoadingDetailState) {
    إرجاع const LoadingDetailLayout();
}
إذا كانت (الحالة هي حالة تحميلDetailState) {{
    إرجاع LoadedDetailDetailLayout(الحالة: الحالة);
}
إذا (الحالة هي UnknownErrorDetailState) { {
    إرجاع UnknownErrorDetailDetailLayout(الحالة: الحالة);
}
إرجاع NoDataDetailDetailLayout();

الحالات التجريبية

الآن لدينا سيطرة كاملة على ما يمكننا محاكاته وعرضه على شاشتنا!

علينا فقط أن نغلف "تخطيط التفاصيل" بـ "موفر" لمحاكاة حالة التخطيط.

ListTileTile(
    العنوان: const Text('فتح العرض التوضيحي "المحمل"),
    عند النقر: () {) {
        Navigator.push(
        السياق,
        MaterialPageRoute(
            المنشئ: (السياق) {
            إرجاع الموفر<DetailState>.value(
                    القيمة: const DetailState.load(
                    سنة: 2022,
                    القياس: قياس(
                        سنة: 2022,
                        عدد السكان 425484,
                        الأمة: "الولايات المتحدة",
                    ),
                    ),
                    الطفل: const DetailLayout(),
                );
            },
        ),
        );
    },
),
ListTile(
    العنوان: const Text("فتح "تحميل" تجريبي"),
    عند النقر: () {) {
        Navigator.push(
        السياق,
        MaterialPageRoute(
            المنشئ: (السياق) {
                    إرجاع الموفر<DetailState>.value(
                        القيمة: const DetailState.loading(2022),
                        تابع: const DetailLayout(),
                    );
                },
            ),
        );
    },
),

خاتمة

إنشاء بنية برمجية قابلة للصيانة بشكل نهائي ليس بالأمر السهل! قد يتطلب توقع السيناريوهات المستقبلية الكثير من الجهد، ولكن آمل أن تساعدك النصائح القليلة التي شاركتها في المستقبل!

قد تبدو الأمثلة بسيطة - قد يبدو الأمر وكأننا نفرط في الهندسة - ولكن مع ازدياد تعقيد تطبيقك، فإن وجود هذه المعايير سيساعدك كثيرًا! 💪

استمتع مع Flutter، وتابع المدونة للحصول على المزيد من المقالات التقنية مثل هذه المقالة! ابقوا معنا!

ClickUp Logo

تطبيق واحد ليحل محلهم جميعًا