اليوم سوف نتحدث عن كيفية تبادل الأحداث بين الأنشطة / شظايا و 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,
إنشاء فئة الحدث الأساسي - 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 } } } } fun clearWaitingEvents() = waitingEvents.clear() }
isActive - نحن بحاجة إلى فهم ما إذا كان هناك
Observer واحد على الأقل مشترك في
Emitter . وفي حالة ظهور المشترك وتراكم الأحداث التي تنتظره ، نرسل هذه الأحداث. توضيح مهم: إرسال الأحداث ضروري ليس من خلال
this.postValue(event) ، ولكن من خلال واضعة
this.value = event . خلاف ذلك ، سيتلقى المشترك الحدث الأخير فقط في القائمة.
تقبل طريقة إرسال حدث جديد ،
newEvent(event, type) ، معلمتين - في الواقع ، الحدث نفسه ونوع هذا الحدث.
لكي لا نتذكر جميع أنواع الأحداث (الأسماء الطويلة) ، سنقوم بإنشاء طرق عامة منفصلة تقبل الحدث نفسه فقط:
class Emitter : MutableLiveData<NavigationEvent>() { … fun emitAndExecute(event: NavigationEvent) = newEvent(event, Type.EXECUTE_WITHOUT_LIMITS) fun emitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.EXECUTE_ONCE) fun waitAndExecute(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED) 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() 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() ..
تلقي الأحداث في جزء:
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)
في جوهرها ، حصلنا على تناظرية بين Rx-
BehaviorSubjec ta و
EventBus -a ، بناءً على
LiveData فقط ، حيث يمكن لـ
LiveData جمع الأحداث قبل ظهور فنان-المراقب ، وفيه يمكن
LiveData مراقبة أنواع الأحداث والاتصال بها مرة واحدة فقط.
ويلكوم في التعليقات مع الاقتراحات.
رابط للمصدر .
خطة التقسيط من بنك التسليف المنزلي .