MVVM dan pemilihan elemen dalam adaptor

Sudah setelah tentang adaptor 3 yang ditulis sendiri, di mana perlu untuk menerapkan logika mengingat elemen yang dipilih, saya punya pemikiran bahwa harus ada beberapa solusi yang sudah mencakup semua yang diperlukan. Apalagi jika selama proses pengembangan Anda harus mengubah kemampuan untuk memilih hanya satu item untuk pilihan ganda.


Setelah mempelajari pendekatan MVVM dan sepenuhnya tenggelam di dalamnya, pertanyaan yang disebutkan di atas muncul jauh lebih nyata. Selain itu, adaptor itu sendiri berada pada tingkat View , sedangkan informasi tentang elemen yang dipilih seringkali sangat diperlukan untuk ViewModel .


Mungkin saya tidak menghabiskan cukup waktu untuk mencari jawaban di Internet, tetapi, bagaimanapun juga, saya tidak menemukan solusi yang sudah jadi. Namun, di salah satu proyek saya datang dengan ide implementasi yang bisa universal, jadi saya ingin membaginya.


Komentar . Meskipun akan logis dan sesuai untuk MVVM di Android untuk membuat implementasi dengan LiveData , pada tahap ini saya tidak siap untuk menulis kode menggunakannya. Jadi ini hanya untuk masa depan. Tetapi solusi akhirnya ternyata tanpa ketergantungan Android , yang berpotensi memungkinkan untuk menggunakannya pada platform mana pun di mana kotlin dapat bekerja.


Manajer Seleksi


Untuk mengatasi masalah ini, antarmuka SelectionManager umum dikompilasi:


 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 } 

Secara default, sudah ada 3 implementasi yang berbeda:


  • MultipleSelection - objek memungkinkan Anda untuk memilih elemen sebanyak yang Anda suka dari daftar;
  • SingleSelection - sebuah objek memungkinkan Anda memilih hanya satu elemen;
  • NoneSelection - objek tidak memungkinkan untuk memilih elemen sama sekali.

Mungkin, dengan yang terakhir akan ada sebagian besar dari semua pertanyaan, jadi saya akan mencoba menunjukkan pada contoh.


Adaptor


Seharusnya menambahkan objek SelectionManager ke adaptor sebagai ketergantungan melalui konstruktor.


 class TestAdapter(private val selectionManager: SelectionManager) : RecyclerView.Adapter<TestHolder>() { //class body } 

Dalam contoh ini, saya tidak akan repot dengan logika pemrosesan mengklik suatu elemen, jadi kami hanya setuju bahwa pemegang (tanpa detail) sepenuhnya bertanggung jawab untuk menunjuk pendengar klik.


 class TestHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: Any, onItemClick: () -> Unit) { //all bind logic } } 

Lebih lanjut, agar keajaiban ini bekerja, adaptor harus melakukan 3 langkah berikut:


1. onBindViewHolder


Lewati panggilan balik ke metode bind pemegang, yang akan memanggil selectionManager.selectPosition(position) untuk elemen yang ditampilkan. Juga di sini, Anda kemungkinan besar perlu mengubah tampilan (paling sering hanya latar belakang) tergantung pada apakah item saat ini dipilih - Anda dapat memanggil selectionManager.isPositionSelected(position) untuk ini.


 override fun onBindViewHolder(holder: TestHolder, position: Int) { val isItemSelected = selectionManager.isPositionSelected(position) //do whatever you need depending on `isItemSelected` value val item = ... //get current item by `position` value holder.bind(item) { selectionManager.selectPosition(position) } } 

2. registerSelectionChangeListener


Agar adaptor dapat memperbarui elemen yang ditekan tepat waktu, Anda harus berlangganan ke tindakan yang sesuai. Dan jangan lupa bahwa hasil yang dikembalikan oleh metode berlangganan harus disimpan.


 private val selectionDisposable = selectionManager.registerSelectionChangeListener { position, isSelected -> notifyItemChanged(position) } 

Saya perhatikan bahwa dalam hal ini nilai parameter isSelected tidak penting, karena dengan perubahan apa pun, perubahan elemen akan berubah. Tetapi tidak ada yang mencegah Anda menambahkan pemrosesan tambahan, yang penting nilai ini.


3. selectionDisposable


Pada langkah sebelumnya, saya tidak hanya mengatakan bahwa hasil dari metode ini harus disimpan - sebuah objek dikembalikan yang menghapus langganan untuk menghindari kebocoran. Setelah menyelesaikan pekerjaan, objek ini harus dikonsultasikan.


 fun destroy() { selectionDisposable.dispose() } 

ViewModel


Untuk adaptor sihir sudah cukup, kita akan beralih ke ViewModel . Menginisialisasi SelectionManager sangat sederhana:


 class TestViewModel: ViewModel() { val selectionManager: SelectionManager = SingleSelection() } 

Di sini, dengan analogi dengan adaptor, Anda dapat berlangganan perubahan (misalnya, untuk membuat tombol "Hapus" tidak dapat diakses ketika tidak ada item yang dipilih), tetapi Anda juga dapat mengklik tombol ringkasan (misalnya, "Unduh Terpilih") untuk mendapatkan daftar semua yang dipilih .


 fun onDownloadClick() { val selectedPositions: ArrayList<Int> = selectionManager.getSelectedPositions() ... } 

Dan di sini salah satu kekurangan solusi saya muncul: pada tahap saat ini, objek hanya dapat menyimpan posisi elemen. Artinya, untuk mendapatkan objek yang dipilih secara tepat, dan bukan posisinya, logika tambahan akan diperlukan menggunakan sumber data yang terhubung ke adaptor (sayangnya, sejauh ini saja). Tapi semoga Anda bisa mengatasinya.


Selanjutnya hanya menghubungkan adaptor dengan model tampilan. Ini sudah di tingkat aktivitas.


 class TestActivity: AppCompatActivity() { private lateinit var adapter: TestAdapter private lateinit var viewModel: TestViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //`TestViewModel` initialization adapter = TestAdapter(viewModel.selectionManager) } } 

Fleksibilitas


Untuk beberapa orang, ini mungkin cukup dimengerti, tetapi saya juga ingin mencatat bahwa dengan implementasi ini ternyata mudah untuk mengontrol metode pemilihan pada adaptor. Sekarang adaptor hanya dapat memilih satu elemen, tetapi jika TestViewModel mengubah inisialisasi properti selectionManager di TestViewModel , sisa kode akan bekerja "dengan cara baru" tanpa memerlukan perubahan apa pun. Yaitu, atur val selectionManager: SelectionManager = MultipleSelection() , dan sekarang adaptor memungkinkan Anda untuk memilih elemen sebanyak yang Anda suka.


Dan jika Anda memiliki semacam kelas adaptor dasar untuk seluruh aplikasi, Anda tidak perlu takut untuk memasukkan SelectionManager dengan cara yang sama. Memang, terutama untuk adaptor yang tidak menyiratkan pilihan elemen sama sekali, ada implementasi NoneSelection - tidak peduli apa yang Anda lakukan dengannya, itu tidak akan pernah memiliki elemen yang dipilih dan tidak akan pernah memanggil salah satu pendengar. Tidak, dia tidak melempar pengecualian - dia mengabaikan semua panggilan, tetapi adaptor tidak perlu mengetahui hal ini sama sekali.


Pencegat


Ada juga kasus di mana perubahan dalam pemilihan elemen disertai dengan operasi tambahan (misalnya, memuat informasi terperinci), sebelum penyelesaian yang berhasil di mana penerapan perubahan mengarah ke keadaan yang salah. Khusus untuk kasus-kasus ini, saya menambahkan mekanisme intersepsi.


Untuk menambahkan interseptor, Anda perlu memanggil metode addSelectionInterceptor (sekali lagi, Anda perlu menyimpan hasilnya dan mengaksesnya setelah selesai). Salah satu parameter pencegat dalam contoh callback: () -> Unit - sampai dipanggil, perubahan tidak akan diterapkan. Artinya, dengan tidak adanya jaringan, pemuatan informasi terperinci dari server tidak dapat diselesaikan dengan sukses, oleh karena itu, keadaan Manajer selectionManager digunakan tidak akan berubah. Jika ini persis perilaku yang Anda perjuangkan - Anda perlu metode ini.


 val interceptionDisposable = selectionManager.addSelectionInterceptor { position: Int, isSelected: Boolean, callback: () -> Unit -> if(isSelected) { val selectedItem = ... //get current item by `position` value val isDataLoadingSuccessful: Boolean = ... //download data for `selectedItem` if(isDataLoadingSuccessful) { callback() } } } 

Jika perlu, Anda dapat menghubungkan pencegat sebanyak yang Anda suka. Dalam hal ini, callback() dari pencegat pertama mulai memproses yang kedua. Dan hanya callback() di yang terakhir yang pada akhirnya akan menyebabkan perubahan dalam status seleksiManager.


Prospek


  1. Menggunakan Disposable untuk menghapus langganan efektif, tetapi tidak LiveData . Peningkatan pertama di telepon adalah menggunakan kemampuan android.arch.lifecycle untuk pekerjaan yang lebih nyaman. Kemungkinan besar, ini akan menjadi proyek yang terpisah, sehingga tidak menambah ketergantungan platform ke yang sekarang.
  2. Seperti yang saya katakan, mendapatkan daftar objek yang dipilih ternyata tidak nyaman. Saya juga ingin mencoba mengimplementasikan objek yang dapat bekerja dengan wadah data dengan cara yang sama. Pada saat yang sama, itu bisa menjadi sumber data untuk adaptor.

Referensi


Anda dapat menemukan kode sumber di tautan - GitHub
Proyek ini juga tersedia untuk implementasi melalui gradle - ru.ircover.selectionmanager:core:1.0.0

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


All Articles