اليوم سوف نتحدث عن كيفية تبادل الأحداث بين الأنشطة / شظايا و 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
مراقبة أنواع الأحداث والاتصال بها مرة واحدة فقط.
ويلكوم في التعليقات مع الاقتراحات.
رابط للمصدر .
خطة التقسيط من بنك التسليف المنزلي .