ActionViews أو كيف لا أحب الصفيحة منذ الطفولة

مرحبا يا هبر! في هذه المقالة ، أرغب في مشاركة تجربة إنشاء آليتي الخاصة لأتمتة عرض أنواع العرض المختلفة: ContentView و LoadingView و NoInternetView و EmptyContentView و ErrorView.





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


سأقول على الفور أنني سأفكر في العمل على RxJava ، حيث لم أفعل مثل هذه الآلية بالنسبة للكوروتينات - لم تصل يدي. وبالنسبة للأدوات المشابهة الأخرى (Loaders ، AsyncTask ، إلخ.) ، ليس من المنطقي استخدام آليتي ، نظرًا لأنه غالبًا ما يتم استخدام RxJava أو coroutines.


أكشن


قال زميل لي أنه من المستحيل توحيد سلوك العرض ، لكنني ما زلت أحاول القيام بذلك. وفعلت ذلك.


يجب على شاشة التطبيق القياسية ، التي يتم أخذ بياناتها من الخادم ، على الأقل معالجة 5 حالات:


  • عرض البيانات
  • جارٍ التحميل
  • خطأ - أي خطأ غير موصوف أدناه
  • الافتقار إلى الإنترنت خطأ عالمي
  • شاشة فارغة - تم تمرير الطلب ، ولكن لا توجد بيانات
  • حالة أخرى هي أن البيانات تم تحميلها من ذاكرة التخزين المؤقت ، ولكن طلب التحديث عاد بخطأ ، أي إظهار البيانات القديمة (أفضل من لا شيء) - المكتبة لا تدعم ذلك.

وفقًا لذلك ، يجب أن يكون لكل وجهة نظر خاصة بها.


أسمي هذه طريقة العرض - ActionViews ، لأنها تستجيب إلى نوع من الإجراءات. في الواقع ، إذا كان بإمكانك تحديد النقطة التي يجب أن تظهر فيها طريقة العرض الخاصة بك بالضبط ، ومتى تريد الاختباء ، فيمكن أن تكون أيضًا ActionView.


هناك طريقة قياسية واحدة (أو ربما ليست واحدة) للعمل مع طريقة العرض هذه.


في الطرق التي تحتوي على العمل مع RxJava ، تحتاج إلى إضافة وسيطات الإدخال لجميع أنواع ActionViews وإضافة بعض المنطق إلى هذه المكالمات لإظهار وإخفاء ActionViews ، كما هو موضح هنا:


public void getSomeData(LoadingView loadingView, ErrorView errorView, NoInternetView noInternetView, EmptyContentView emptyContentView) { mApi.getProjects() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe(disposable -> { loadingView.show(); noInternetView.hide(); emptyContentView.hide(); }) .doFinally(loadingView::hide) .flatMap(projectResponse -> { /*    */ }) .subscribe( response -> {/*   */}, throwable -> { if (ApiUtils.NETWORK_EXCEPTIONS .contains(throwable.getClass())) noInternetView.show(); else errorView.show(throwable.getMessage()); } ); } 

لكن هذه الطريقة تحتوي على كمية كبيرة من الصفيحة ، وبشكل افتراضي لا نحبها. وهكذا بدأت العمل على تقليل الشفرة الروتينية.


يصل المستوى


كانت الخطوة الأولى في ترقية الطريقة القياسية للعمل مع ActionViews هي تقليل اللوح المعياري عن طريق وضع المنطق في فئات الأدوات المساعدة. لم أخترع الكود أدناه. أنا منتحل وتجسس على زميل عاقل. شكرا Arutar


الآن يبدو رمزنا كما يلي:


 public void getSomeData(LoadingView loadingView, ErrorView errorView, NoInternetView noInternetView, EmptyContentView emptyContentView) { mApi.getProjects() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .compose(RxUtil::loading(loadingView)) .compose(RxUtil::emptyContent(emptyContentView)) .compose(RxUtil::noInternet(errorView, noInternetView)) .subscribe(response -> { /*   */ }, RxUtil::error(errorView)); } 

الكود الذي نراه أعلاه ، على الرغم من خلوه من الكود المتداول ، لا يزال لا يسبب مثل هذه المتعة الساحرة. لقد أصبح بالفعل أفضل بكثير ، ولكن لا تزال هناك مشكلة تمرير الروابط إلى ActionViews في كل طريقة يوجد فيها العمل مع Rx. ويمكن أن يكون هناك عدد لا نهائي من هذه الأساليب في المشروع. أيضا كتابة هذه يؤلف باستمرار. Buueee. من يحتاج هذا؟ فقط المجتهد والعناد وليس كسولا. انا لست هكذا أنا من محبي الكسل ومحبي كتابة كود جميل ومريح ، لذلك تم اتخاذ قرار مهم - لتبسيط الرمز بأي وسيلة.


نقطة الاختراق


بعد إعادة كتابة العديد من الآلية ، جئت إلى هذا الخيار:


 public void getSomeData() { execute(() -> mApi.getProjects(), new BaseSubscriber<>(response -> { /*   */ })); } 

أعيدت كتابة آليتي حوالي 10-15 مرة ، وفي كل مرة كانت مختلفة تمامًا عن الإصدار السابق. لن أريكم جميع الإصدارات ، فلنركز على النسختين الأخيرتين. أول ما رأيته للتو.


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





سيكون مثل هذا الرمز في المستقبل أكثر صعوبة وصعوبة في الحفاظ عليه ، ويحتوي على عيوب ومشاكل خطيرة للغاية كانت حاسمة:


  • ماذا يحدث إذا كنت بحاجة إلى عرض طرق عرض تحميل متعددة على الشاشة؟ كيفية فصلها؟ كيف نفهم أي تحميل يجب عرضه متى؟
  • انتهاك مفهوم Rx - يجب أن يكون كل شيء في تيار واحد (تيار). ليس هذا هو الحال هنا.
  • تعقيد التخصيص. من الصعب للغاية تغيير السلوك والمنطق الموصوف للمستخدم النهائي ، وبالتالي ، من الصعب إضافة سلوكيات جديدة.
  • يجب عليك استخدام طريقة العرض المخصصة لكي تعمل الآلية. يعد ذلك ضروريًا حتى تفهم الآلية أي ActionView ينتمي إلى أي نوع. على سبيل المثال ، إذا كنت ترغب في استخدام ProgressBar ، فيجب أن يحتوي على تطبيقات LoadingView.
  • يجب أن يتطابق المعرف الخاص بـ ActionView مع المعرفات المحددة في الفئات الأساسية من أجل التخلص من اللوح المرجعي. هذا ليس مناسبًا للغاية ، على الرغم من أنه يمكنك التعامل مع هذا.
  • التأمل نعم ، كانت هنا ، وبسببها ، كانت الآلية تتطلب التحسين بشكل واضح.

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


وداعا جافا!


بعد مرور بعض الوقت ، كنت أجلس في المنزل ، انخرط في كنت أتخبط وأدرك فجأة أنه كان عليّ تجربة Kotlin وزيادة الامتدادات والقيم الافتراضية و lambdas والمندوبين.


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


هذا هو رمزنا السابق ، ولكن في النسخة النهائية:


 fun getSomeData() { api.getProjects() .withActionViews(view) .execute(onComplete = { /*   */ }) } 

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


 fun getSomeData() { api.getProjects() .withActionViews( view, doOnLoadStart = { /* */ }, doOnLoadEnd = { /* */ }) .execute(onComplete = { /*   */ }) } 

التغييرات السلوكية متاحة أيضًا لـ ActionViews الأخرى. إذا كنت تريد استخدام السلوك القياسي ، ولكن ليس لديك ActionViews افتراضي ، يمكنك ببساطة تحديد طريقة العرض التي يجب أن تحل محل ActionView الخاص بنا:


 fun getSomeData(projectLoadingView: LoadingView) { mApi.getPosts(1, 1) .withActionViews( view, loadingView = projectLoadingView ) .execute(onComplete = { /*   */ }) } 

لقد عرضت عليك كريم هذه الآلية ، ولكن لها أيضًا سعرها الخاص.
أولاً ، ستحتاج إلى إنشاء CustomViews لكي تعمل:


 class SwipeRefreshLayout : android.support.v4.widget.SwipeRefreshLayout, LoadingView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) } 

قد لا يكون من الضروري القيام بذلك. في الوقت الحالي ، أقوم بجمع التعليقات وقبول الاقتراحات لتحسين هذه الآلية. السبب الرئيسي الذي نحتاجه لاستخدام CustomViews هو أن ترث من واجهة تخبرك بنوع ActionView الذي تنتمي إليه. هذا من أجل الأمان ، لأنك قد ترتكب خطأً عن طريق الخطأ عند تحديد نوع العرض في طريقة withActionsViews.


هذا ما تبدو عليه طريقة withActionsViews:


 fun <T> Observable<T>.withActionViews( view: ActionsView, contentView: View = view.contentActionView, loadingView: LoadingView? = view.loadingActionView, noInternetView: NoInternetView? = view.noInternetActionView, emptyContentView: EmptyContentView? = view.emptyContentActionView, errorView: ErrorView = view.errorActionView, doOnLoadStart: () -> Unit = { doOnLoadSubscribe(contentView, loadingView) }, doOnLoadEnd: () -> Unit = { doOnLoadComplete(contentView, loadingView) }, doOnStartNoInternet: () -> Unit = { doOnNoInternetSubscribe(contentView, noInternetView) }, doOnNoInternet: (Throwable) -> Unit = { doOnNoInternet(contentView, errorView, noInternetView) }, doOnStartEmptyContent: () -> Unit = { doOnEmptyContentSubscribe(contentView, emptyContentView) }, doOnEmptyContent: () -> Unit = { doOnEmptyContent(contentView, errorView, emptyContentView) }, doOnError: (Throwable) -> Unit = { doOnError(errorView, it) } ) { /**/ } 

تبدو مخيفة ، لكنها مريحة وسريعة! كما ترى ، في معلمات الإدخال ، فإنه يقبل تحميل View: LoadingView؟ .. هذا يؤمننا ضد الأخطاء في نوع ActionView.


وفقًا لذلك ، لكي تعمل الآلية ، تحتاج إلى اتخاذ بعض الخطوات البسيطة:


  • أضف إلى تصميماتنا ActionViews ، وهي مخصصة. لقد صنعت بالفعل بعضًا منها ، ويمكنك فقط استخدامها.
  • تنفيذ واجهة HasActionsView وتجاوز المتغيرات الافتراضية المسؤولة عن ActionViews في التعليمات البرمجية:
     override var contentActionView: View by mutableLazy { recyclerView } override var loadingActionView: LoadingView? by mutableLazy { swipeRefreshLayout } override var noInternetActionView: NoInternetView? by mutableLazy { noInternetView } override var emptyContentActionView: EmptyContentView? by mutableLazy { emptyContentView } override var errorActionView: ErrorView by mutableLazy { ToastView(baseActivity) } 
  • أو ترث من فئة تم فيها تجاوز ActionViews بالفعل. في هذه الحالة ، سيكون عليك استخدام المعرف المحدد بدقة في التخطيط الخاص بك:


     abstract class ActionsFragment : Fragment(), HasActionsView { override var contentActionView: View by mutableLazy { findViewById<View>(R.id.contentView) } override var loadingActionView: LoadingView? by mutableLazy { findViewByIdNullable<View>(R.id.loadingView) as LoadingView? } override var noInternetActionView: NoInternetView? by mutableLazy { findViewByIdNullable<View>(R.id.noInternetView) as NoInternetView? } override var emptyContentActionView: EmptyContentView? by mutableLazy { findViewByIdNullable<View>(R.id.emptyContentView) as EmptyContentView? } override var errorActionView: ErrorView by mutableLazy { ToastView(baseActivity) } } 

  • استمتع بالعمل بدون لوحة مرجعية!

إذا كنت ستستخدم ملحقات Kotlin ، فلا تنس أنه يمكنك إعادة تسمية الاستيراد إلى اسم مناسب لك:


 import kotlinx.android.synthetic.main.fr_gifts.contentView as recyclerView 

ما هي الخطوة التالية؟


عندما بدأت العمل في هذه الآلية ، لم أفكر في ماذا ستأتي مكتبة. ولكن حدث أن أردت مشاركة إبداعاتي ، والآن أحلى شيء في انتظاري - نشر المكتبة ، وجمع المشكلات ، وتلقي الملاحظات ، وإضافة / تحسين الوظائف وإصلاح الأخطاء.


بينما كنت أكتب مقالاً ...


تمكنت من ترتيب كل شيء في شكل مكتبات:



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


إذا كان لديك أي اقتراحات وتوصيات لتحسين وظائف الآلية نفسها وتشغيلها ، فسوف يسعدني الاستماع إليها. مرحبًا بك في التعليقات وفي حالة Telegram فقط: tanchuev


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


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


PPPS إذا كنت مهتمًا بعملي ، فيمكننا مناقشته شخصيًا في 28 سبتمبر في موسكو في مؤتمر MBLT DEV 2018 الدولي لمطوري الهواتف المحمولة. بالمناسبة ، تذاكر الطيور المبكرة تنفد بالفعل!

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


All Articles