بالفعل بعد حوالي المهايئ المكتوب ذاتيًا الثالث ، والذي كان من الضروري فيه تنفيذ منطق تذكر العنصر المحدد ، كان لديّ أفكار بأنه يجب أن يكون هناك بعض الحلول التي تتضمن بالفعل كل ما هو ضروري. خاصة إذا كان عليك أثناء عملية التطوير تغيير القدرة على اختيار عنصر واحد فقط للاختيار من متعدد.
بعد دراسة نهج MVVM والانغماس فيه بالكامل ، ظهر السؤال المذكور أعلاه بشكل ملحوظ. علاوة على ذلك ، يكون المحول نفسه في مستوى View
، في حين أن المعلومات حول العناصر المحددة غالبًا ما تكون ضرورية للغاية لـ ViewModel
.
ربما لم أقضي وقتًا كافيًا في البحث عن إجابات على الإنترنت ، لكنني لم أجد حلاً جاهزًا على أي حال. ومع ذلك ، في أحد المشاريع توصلت إلى فكرة تنفيذ يمكن أن تكون عالمية ، لذلك أردت مشاركتها.
ملاحظة . على الرغم من أنه سيكون من المنطقي ومناسب لـ MVVM على Android إجراء تطبيق مع LiveData
، إلا أنني لست مستعدًا في هذه المرحلة لكتابة التعليمات البرمجية في هذه المرحلة. لذلك هذا هو فقط للمستقبل. ولكن تبين أن الحل النهائي هو عدم وجود تبعيات Android
، مما يجعله ممكنًا على أي منصة يمكن أن تعمل فيها kotlin.
SelectionManager
لحل هذه المشكلة ، تم تصنيف واجهة SelectionManager
العامة:
interface SelectionManager { fun clearSelection() fun selectPosition(position: Int) fun isPositionSelected(position: Int): Boolean fun registerSelectionChangeListener(listener: (position: Int, isSelected: Boolean) -> Unit): Disposable fun getSelectedPositions(): ArrayList<Int> fun isAnySelected(): Boolean fun addSelectionInterceptor(interceptor: (position: Int, isSelected: Boolean, callback: () -> Unit) -> Unit): Disposable }
بشكل افتراضي ، يوجد بالفعل 3 تطبيقات مختلفة:
MultipleSelection
- يسمح لك الكائن بتحديد أكبر عدد تريده من القائمة ؛SingleSelection
- كائن يسمح لك بتحديد عنصر واحد فقط ؛NoneSelection
- الكائن لا يسمح بتحديد العناصر على الإطلاق.
من المحتمل ، مع وجود الأخير ، سيكون هناك أكثر من سؤال ، لذا سأحاول أن أعرض بالفعل مثالاً.
محول
من المفترض أن تضيف كائن SelectionManager
إلى المحول باعتباره تبعية خلال المُنشئ.
class TestAdapter(private val selectionManager: SelectionManager) : RecyclerView.Adapter<TestHolder>() {
في المثال ، لن أزعج منطق معالجة النقر فوق عنصر ما ، لذلك نحن نتفق فقط على أن المالك (بدون تفاصيل) مسؤول بالكامل عن تعيين مستمع النقرات.
class TestHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: Any, onItemClick: () -> Unit) {
علاوة على ذلك ، لكي يعمل هذا السحر ، يجب أن يقوم المحول بتنفيذ الخطوات الثلاث التالية:
1. onBindViewHolder
قم بتمرير رد اتصال إلى طريقة bind
الخاصة بالحامل ، والتي ستستدعي selectionManager.selectPosition(position)
للعنصر المعروض. هنا أيضًا ، ستحتاج على الأرجح إلى تغيير العرض (غالبًا الخلفية فقط) اعتمادًا على ما إذا كان العنصر محددًا حاليًا - يمكنك استدعاء selectionManager.isPositionSelected(position)
لهذا الغرض.
override fun onBindViewHolder(holder: TestHolder, position: Int) { val isItemSelected = selectionManager.isPositionSelected(position)
2. registerSelectionChangeListener
لكي يقوم الموفق بتحديث العناصر المضغوطة في الوقت المناسب ، يجب عليك الاشتراك في الإجراء المناسب. ولا تنس أن النتيجة التي يتم إرجاعها بواسطة طريقة الاشتراك يجب حفظها.
private val selectionDisposable = selectionManager.registerSelectionChangeListener { position, isSelected -> notifyItemChanged(position) }
ألاحظ أنه في هذه الحالة ، تكون قيمة المعلمة isSelected
مهمة ، لأنه مع أي تغيير يتغير مظهر العنصر. ولكن لا شيء يمنعك من إضافة معالجة إضافية ، والتي تعد هذه القيمة مهمة لها.
3. selectionDisposable
في الخطوة السابقة ، لم أقل فقط أنه يجب حفظ نتيجة الطريقة - يتم إرجاع كائن يمسح الاشتراك لتجنب التسريبات. بعد الانتهاء من العمل ، يجب استشارة هذا الكائن.
fun destroy() { selectionDisposable.dispose() }
ViewModel
لأن محول السحر يكفي ، ViewModel
إلى ViewModel
. تهيئة SelectionManager
بسيط للغاية:
class TestViewModel: ViewModel() { val selectionManager: SelectionManager = SingleSelection() }
هنا ، قياسًا على المحول ، يمكنك الاشتراك في التغييرات (على سبيل المثال ، لجعل الزر "حذف" غير ممكن الوصول إليه عند عدم تحديد أي عناصر) ، ولكن يمكنك أيضًا النقر فوق زر الملخص (على سبيل المثال ، "تنزيل المحدد") للحصول على قائمة بجميع العناصر المحددة .
fun onDownloadClick() { val selectedPositions: ArrayList<Int> = selectionManager.getSelectedPositions() ... }
وهنا يظهر أحد السلبيات في الحل الخاص بي: في المرحلة الحالية ، يكون الكائن قادرًا على تخزين مواضع العناصر فقط. وهذا يعني أنه من أجل الحصول على الكائنات المحددة بالضبط وليس مواضعها ، سوف يكون هناك حاجة إلى منطق إضافي باستخدام مصدر البيانات المتصل بالمحول (للأسف ، حتى الآن فقط). ولكن نأمل أن تتمكن من التعامل معها.
علاوة على ذلك يبقى فقط لتوصيل محول مع نموذج العرض. هذا بالفعل على مستوى النشاط.
class TestActivity: AppCompatActivity() { private lateinit var adapter: TestAdapter private lateinit var viewModel: TestViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
مرونة
بالنسبة للبعض ، قد يكون هذا مفهومًا تمامًا ، لكنني أريد أن أشير أيضًا إلى أنه من خلال هذا التطبيق ، أصبح من السهل التحكم في طريقة الاختيار في المحولات. يمكن للمحول الآن تحديد عنصر واحد فقط ، ولكن إذا TestViewModel
بتغيير تهيئة خاصية TestViewModel
في TestViewModel
، TestViewModel
بقية الكود "بطريقة جديدة" دون الحاجة إلى أي تغييرات. بمعنى ، قم بتعيين val selectionManager: SelectionManager = MultipleSelection()
، والآن يسمح لك المحول بتحديد أكبر عدد تريده من العناصر.
وإذا كان لديك نوع من فئة المحول الأساسي للتطبيق بأكمله ، فلا يمكنك الخوف من تضمين SelectionManager
بنفس الطريقة. في الواقع ، لا سيما بالنسبة للمحولات التي لا تنطوي على اختيار العناصر على الإطلاق ، هناك تطبيق لـ NoneSelection
- بغض النظر عن ما تفعله به ، فلن يحتوي على العناصر المحددة ولن يتصل أبدًا بأي من المستمعين. لا ، إنه لا يلقي استثناءات - إنه يتجاهل ببساطة جميع المكالمات ، لكن المحول لا يحتاج إلى معرفة ذلك على الإطلاق.
المعترض
هناك أيضًا حالات يكون فيها تغيير اختيار عنصر مصحوبًا بعمليات إضافية (على سبيل المثال ، تحميل معلومات مفصلة) ، إلى أن يؤدي الإكمال الناجح لتطبيق التغييرات إلى حالة غير صحيحة. خاصة بالنسبة لهذه الحالات ، أضفت آلية اعتراض.
لإضافة اعتراض ، تحتاج إلى استدعاء الأسلوب addSelectionInterceptor
(مرة أخرى ، تحتاج إلى حفظ النتيجة والوصول إليها بعد الانتهاء). أحد معلمات المعترض في مثال callback: () -> Unit
- حتى يتم استدعاؤها ، لن يتم تطبيق التغييرات. وهذا يعني ، في حالة عدم وجود شبكة ، لن يتم إكمال تحميل المعلومات التفصيلية من الخادم بنجاح ، وبالتالي ، لن تتغير حالة selectionManager
المستخدم. إذا كان هذا هو بالضبط السلوك الذي تسعى إليه - فأنت بحاجة إلى هذه الطريقة.
val interceptionDisposable = selectionManager.addSelectionInterceptor { position: Int, isSelected: Boolean, callback: () -> Unit -> if(isSelected) { val selectedItem = ...
إذا لزم الأمر ، يمكنك الاتصال بأكبر عدد ممكن من المعترضين. في هذه الحالة ، يبدأ callback()
الخاص بالمعترض الأول معالجة الثانية. و callback()
في الأخير منها سيؤدي في النهاية إلى تغيير في حالة selectionManager
.
آفاق
- يعد استخدام
Disposable
لمسح الاشتراكات فعالًا ، ولكنه ليس مناسبًا مثل LiveData
. التحسين الأول على الخط هو استخدام قدرات android.arch.lifecycle
لمزيد من العمل المريح. على الأرجح ، سيكون هذا مشروعًا منفصلاً ، حتى لا تتم إضافة اعتماد النظام الأساسي إلى المشروع الحالي. - كما قلت ، تبين أن الحصول على قائمة بالكائنات المحددة غير مريح. أريد أيضًا محاولة تنفيذ كائن يمكنه العمل مع حاوية بيانات بالطريقة نفسها. في الوقت نفسه ، يمكن أن يكون مصدر بيانات للمحول.
مراجع
يمكنك العثور على أكواد المصدر على الرابط - جيثب
المشروع متاح أيضًا للتنفيذ من خلال gradle - ru.ircover.selectionmanager:core:1.0.0