إرسال الأحداث من ViewModel إلى النشاط / الشظية في MVVM

اليوم سوف نتحدث عن كيفية تبادل الأحداث بين الأنشطة / شظايا و ViewModel في MVVM. للحصول على البيانات من ViewModel ، يوصى في النشاط / الشظية بالاشتراك في بيانات LiveData الموجودة في ViewModel. ولكن ما الذي يجب فعله لإرسال أحداث فردية (وليس فقط) ، مثل إظهار إشعار أو ، على سبيل المثال ، فتح جزء آخر؟



مرحباً بالجميع!

اسمي Alexei ، أنا مطور أندرويد في بنك التسليف المنزلي.

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

في تطبيقنا "أقساط البضائع من بنك التسليف المنزلي" نستخدم الشظايا ، وبالتالي سنتحدث عنها ، ولكن كل شيء له علاقة أيضًا بالنشاط.

ماذا نريد؟


لدينا جزء ، يتضمن العديد من ViewModel ، البيانات ملزمة بـ DataBinding . تلقي جميع أحداث المستخدم ViewModel. إذا كان الحدث هو التنقل: تحتاج إلى فتح جزء / نشاط آخر ، وإظهار AlertDialog ، Snackbar ، وطلب نظام Snackbar ، وما إلى ذلك ، ثم ينبغي تنفيذ مثل هذا الحدث في الجزء.

وما هي في الواقع المشكلة؟


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

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

ما الحلول الموجودة:


1. استخدام SingleLiveEvent
الايجابيات: يتم تنفيذ الحدث مرة واحدة.
سلبيات: حدث واحد - واحد SingleLiveEvent . مع وجود عدد كبير من الأحداث ، تظهر كائنات أحداث N في ViewModel ، حيث يتعين الاشتراك في كل جزء منها.

2. مثال جيد.
الايجابيات: يتم تنفيذ حدث واحد مرة واحدة ، يمكنك نقل البيانات من viewModel إلى جزء.
السلبيات: البيانات في الحدث مطلوبة ، ولكن إذا كنت بحاجة إلى تنفيذ حدث بدون بيانات (val content: T) ، فستحتاج إلى إنشاء فصل آخر. لا يحل مشكلة تنفيذ نوع واحد من الأحداث مرة واحدة (يتم تنفيذ الحدث نفسه مرة واحدة ، ولكن سيتم تنفيذ هذا النوع من الأحداث عدة مرات أثناء تشغيله من ViewModel). على سبيل المثال ، تذهب طلبات N بشكل غير متزامن ، ولكن لا توجد شبكة. سيعود كل طلب مع وجود خطأ في الشبكة ، وسيتم سحب جزء من أحداث N حول خطأ في الشبكة ، سيتم فتح تنبيهات N في الجزء. لن يوافق المستخدم على مثل هذا القرار :). يجب أن نظهر له مرة واحدة رسالة مع هذا الخطأ. بمعنى آخر ، يجب تنفيذ هذا النوع من الأحداث مرة واحدة.

قرار


نأخذ فكرة SingleLiveEvent لحفظ معلومات معالجة الأحداث كأساس.

تحديد أنواع الأحداث الممكنة


 enum class Type { EXECUTE_WITHOUT_LIMITS, //    –   ,             EXECUTE_ONCE, //      WAIT_OBSERVER_IF_NEEDED,//    ,     -       WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE //    ,     -           } 

إنشاء فئة الحدث الأساسي - NavigationEvent


يشير isHandled إلى ما إذا كان قد تم استلام الحدث (نعتقد أنه تم تنفيذه إذا تم استلام Observer في الجزء).

 open class NavigationEvent(var isHandled: Boolean = false, var type: Events.Type) 

إنشاء فئة Emitter - Emitter


يرث فئة باعث الحدث من LiveData < NavigationEvent >. سيتم استخدام هذه الفئة في ViewModel لإرسال الأحداث.

 class Emitter : MutableLiveData<NavigationEvent>() { private val waitingEvents: ArrayList<NavigationEvent> = ArrayList() private var isActive = false override fun onInactive() { isActive = false } override fun onActive() { isActive = true val postingEvents = ArrayList<NavigationEvent>() waitingEvents .forEach { if (hasObservers()) { this.value = it postingEvents.add(it) } }.also { waitingEvents.removeAll(postingEvents) } } private fun newEvent(event: NavigationEvent, type: Type) { event.type = type this.value = when (type) { Type.EXECUTE_WITHOUT_LIMITS, Type.EXECUTE_ONCE -> if (hasObservers()) event else null Type.WAIT_OBSERVER_IF_NEEDED, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE -> { if (hasObservers() && isActive) event else { waitingEvents.add(event) null } } } } /** Clear All Waiting Events */ fun clearWaitingEvents() = waitingEvents.clear() } 

isActive - نحن بحاجة إلى فهم ما إذا كان هناك Observer واحد على الأقل مشترك في Emitter . وفي حالة ظهور المشترك وتراكم الأحداث التي تنتظره ، نرسل هذه الأحداث. توضيح مهم: إرسال الأحداث ضروري ليس من خلال this.postValue(event) ، ولكن من خلال واضعة this.value = event . خلاف ذلك ، سيتلقى المشترك الحدث الأخير فقط في القائمة.

تقبل طريقة إرسال حدث جديد ، newEvent(event, type) ، معلمتين - في الواقع ، الحدث نفسه ونوع هذا الحدث.

لكي لا نتذكر جميع أنواع الأحداث (الأسماء الطويلة) ، سنقوم بإنشاء طرق عامة منفصلة تقبل الحدث نفسه فقط:

 class Emitter : MutableLiveData<NavigationEvent>() { … /** Default: Emit Event for Execution */ fun emitAndExecute(event: NavigationEvent) = newEvent(event, Type.EXECUTE_WITHOUT_LIMITS) /** Emit Event for Execution Once */ fun emitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.EXECUTE_ONCE) /** Wait Observer Available and Emit Event for Execution */ fun waitAndExecute(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED) /** Wait Observer Available and Emit Event for Execution Once */ fun waitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE) } 

من الناحية الرسمية ، يمكنك بالفعل الاشتراك في Emitter في ViewModel واستقبال الأحداث دون النظر إلى معالجتها (سواء تمت معالجة الحدث بالفعل أم لا).

إنشاء فئة مراقب الأحداث - EventObserver


 class EventObserver(private val handlerBlock: (NavigationEvent) -> Unit) : Observer<NavigationEvent> { private val executedEvents: HashSet<String> = hashSetOf() /** Clear All Executed Events */ fun clearExecutedEvents() = executedEvents.clear() override fun onChanged(it: NavigationEvent?) { when (it?.type) { Type.EXECUTE_WITHOUT_LIMITS, Type.WAIT_OBSERVER_IF_NEEDED -> { if (!it.isHandled) { it.isHandled = true it.apply(handlerBlock) } } Type.EXECUTE_ONCE, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE -> { if (it.javaClass.simpleName !in executedEvents) { if (!it.isHandled) { it.isHandled = true executedEvents.add(it.javaClass.simpleName) it.apply(handlerBlock) } } } } } } 

يقبل هذا المراقب وظيفة الترتيب العالي كمدخلات - ستتم معالجة معالجة الحدث في جزء (مثال أدناه).

طريقة clearExecutedEvents() لتنظيف الأحداث المنفذة (تلك التي كان ينبغي تنفيذها مرة واحدة). مطلوب عند تحديث الشاشة ، على سبيل المثال ، في swipeToRefresh() .

حسنًا ، في الواقع ، طريقة onChange() الرئيسية ، والتي تحدث عند تلقي بيانات باعث جديدة ، والتي يشترك فيها هذا المراقب.

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

 if (!it.isHandled) { it.isHandled = true it.apply(handlerBlock) } 

إذا كان الحدث من النوع الذي يجب تنفيذه مرة واحدة ، فتحقق مما إذا كانت فئة هذا الحدث موجودة في جدول التجزئة. إذا لم يكن الأمر كذلك ، فقم بتنفيذ الحدث وأضف فئة هذا الحدث إلى جدول التجزئة.

 if (it.javaClass.simpleName !in executedEvents) { if (!it.isHandled) { it.isHandled = true executedEvents.add(it.javaClass.simpleName) it.apply(handlerBlock) } } 

ولكن كيف تنقل البيانات داخل الأحداث؟


للقيام بذلك ، يتم MyFragmentNavigation واجهة MyFragmentNavigation ، والتي ستتألف من الفئات الموروثة من NavigationEvent() . يتم إنشاء فئات مختلفة أدناه مع وبدون المعلمات التي تم تمريرها.

 interface MyFragmentNavigation { class ShowCategoryList : NavigationEvent() class OpenProduct(val productId: String, val productName: String) : NavigationEvent() class PlayVideo(val url: String) : NavigationEvent() class ShowNetworkError : NavigationEvent() } 

كيف يعمل في الممارسة العملية


إرسال الأحداث من ViewModel:

 class MyViewModel : ViewModel() { val emitter = Events.Enitter() fun doOnShowCategoryListButtonClicked() = emitter.emitAndExecute(MyNavigation.ShowCategoryList()) fun doOnPlayClicked() = emitter.waitAndExecuteOnce(MyNavigation.PlayVideo(url = "https://site.com/abc")) fun doOnProductClicked() = emitter.emitAndExecute(MyNavigation.OpenProduct( productId = "123", productTitle = " Samsung") ) fun doOnNetworkError() = emitter.emitAndExecuteOnce(MyNavigation.ShowNetworkError()) fun doOnSwipeRefresh(){ emitter.clearWaitingEvents() ..//loadData() } } 

تلقي الأحداث في جزء:

 class MyFragment : Fragment() { private val navigationEventsObserver = Events.EventObserver { event -> when (event) { is MyFragmentNavigation.ShowCategoryList -> ShowCategoryList() is MyFragmentNavigation.PlayVideo -> videoPlayerView.loadUrl(event.url) is MyFragmentNavigation.OpenProduct -> openProduct(id = event.productId, name = event.otherInfo) is MyFragmentNavigation.ShowNetworkError -> showNetworkErrorAlert() } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Observer   ViewModels     myViewModel.emitter.observe(viewLifecycleOwner, navigationEventsObserver) myViewModelSecond.emitter.observe(viewLifecycleOwner, navigationEventsObserver) myViewModelThird.emitter.observe(viewLifecycleOwner, navigationEventsObserver) } private fun ShowCategoryList(){ ... } private fun openProduct(id: String, name: String){ ... } private fun showNetworkErrorAlert(){ ... } } 

في جوهرها ، حصلنا على تناظرية بين Rx- BehaviorSubjec ta و EventBus -a ، بناءً على LiveData فقط ، حيث يمكن لـ LiveData جمع الأحداث قبل ظهور فنان-المراقب ، وفيه يمكن LiveData مراقبة أنواع الأحداث والاتصال بها مرة واحدة فقط.

ويلكوم في التعليقات مع الاقتراحات.

رابط للمصدر .
خطة التقسيط من بنك التسليف المنزلي .

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


All Articles