MVIDroid: tinjauan perpustakaan MVI baru (Model-View-Intent)

Halo semuanya! Pada artikel ini saya ingin berbicara tentang perpustakaan baru yang membawa pola desain MVI ke Android. Perpustakaan ini disebut MVIDroid, ditulis 100% di Kotlin, ringan dan menggunakan RxJava 2.x. Saya pribadi adalah penulis perpustakaan, kode sumbernya tersedia di GitHub, dan Anda dapat menghubungkannya melalui JitPack (tautan ke repositori di akhir artikel). Artikel ini terdiri dari dua bagian: deskripsi umum tentang perpustakaan dan contoh penggunaannya.


MVI


Jadi, sebagai kata pengantar, izinkan saya mengingatkan Anda apa itu MVI. Model - View - Intent atau, jika dalam bahasa Rusia, Model - View - Intention. Ini adalah pola desain di mana Model adalah komponen aktif yang menerima Maksud dan menghasilkan Status. Presentasi (Tampilan) pada gilirannya menerima Model Representasi (Lihat Model) dan menghasilkan Niat yang sama. Keadaan dikonversi ke Model Tampilan menggunakan fungsi transformasi (View Model Mapper). Secara skematis, pola MVI dapat direpresentasikan sebagai berikut:


MVI


Dalam MVIDroid, Representasi tidak menghasilkan Intensi secara langsung. Sebagai gantinya, ia menghasilkan Acara UI, yang kemudian dikonversi ke Intent menggunakan fungsi transformasi.


Lihat


Komponen utama MVIDroid


Model


Mari kita mulai dengan Model. Di perpustakaan, konsep Model sedikit diperluas, di sini ia menghasilkan tidak hanya Negara tetapi juga Label. Label digunakan untuk mengkomunikasikan Model satu sama lain. Label beberapa Model dapat dikonversi menjadi Intensi Model lain menggunakan fungsi transformasi. Secara skematis, Model dapat direpresentasikan sebagai berikut:


Model


Di MVIDroid, Model diwakili oleh antarmuka MviStore (nama Toko dipinjam dari Redux):


interface MviStore<State : Any, in Intent : Any, Label : Any> : (Intent) -> Unit, Disposable { @get:MainThread val state: State val states: Observable<State> val labels: Observable<Label> @MainThread override fun invoke(intent: Intent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean } 

Dan agar kita memiliki:


  • Antarmuka memiliki tiga parameter Generik: Negara - jenis Negara, Intent - jenis Intent dan Label - jenis Label
  • Ini berisi tiga bidang: status - status Model saat ini, status - Status yang Dapat Diamati dan label - Label yang Dapat Diamati. Dua bidang terakhir memungkinkan untuk berlangganan masing-masing perubahan pada Status dan Tag.
  • Niat Konsumen
  • Ini adalah Disposable, yang memungkinkan untuk menghancurkan Model dan menghentikan semua proses yang terjadi di dalamnya

Perhatikan bahwa semua metode Model harus dijalankan pada utas utama. Hal yang sama berlaku untuk komponen lainnya. Tentu saja, Anda dapat melakukan tugas latar belakang menggunakan alat RxJava standar.


Komponen


Komponen dalam MVIDroid adalah sekelompok Model yang disatukan oleh tujuan bersama. Misalnya, Anda dapat memilih semua Model untuk layar di Komponen. Dengan kata lain, Komponen adalah fasad untuk Model-model yang terlampir di dalamnya dan memungkinkan Anda untuk menyembunyikan detail implementasi (Model, mentransformasikan fungsi dan hubungannya). Mari kita lihat diagram Komponen:


Komponen


Seperti yang dapat Anda lihat dari diagram, komponen memiliki fungsi penting untuk mengubah dan mengarahkan ulang peristiwa.


Daftar lengkap fungsi Komponen adalah sebagai berikut:


  • Mengaitkan Acara Representasi dan Tag yang masuk dengan masing-masing Model menggunakan fungsi transformasi yang disediakan
  • Bawa Label Model keluar ke luar
  • Hancurkan semua Model dan hancurkan semua ikatan saat Komponen dihancurkan

Komponen ini juga memiliki antarmuka sendiri:


 interface MviComponent<in UiEvent : Any, out States : Any> : (UiEvent) -> Unit, Disposable { @get:MainThread val states: States @MainThread override fun invoke(event: UiEvent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean } 

Pertimbangkan antarmuka Komponen secara lebih rinci:


  • Berisi dua parameter Generik: UiEvent - jenis Acara Lihat dan Negara - jenis Status Model
  • Berisi bidang state yang memberikan akses ke grup Model States (misalnya, sebagai antarmuka atau kelas data)
  • Acara Pandangan Konsumen
  • Ini adalah Disposable, yang memungkinkan untuk menghancurkan Komponen dan semua Modelnya

Lihat


Seperti yang Anda tebak, diperlukan tampilan untuk menampilkan data. Data untuk setiap Tampilan dikelompokkan ke dalam Model Tampilan dan biasanya direpresentasikan sebagai kelas data (Kotlin). Pertimbangkan antarmuka Tampilan:


 interface MviView<ViewModel : Any, UiEvent : Any> { val uiEvents: Observable<UiEvent> @MainThread fun subscribe(models: Observable<ViewModel>): Disposable } 

Semuanya sedikit lebih sederhana di sini. Dua parameter Generik: ViewModel - jenis Model Tampilan dan UiEvent - jenis Acara Lihat. Satu bidang uiEvents adalah Acara Tampilan yang Dapat Diamati, yang memungkinkan pelanggan untuk berlangganan acara yang sama ini. Dan satu metode berlangganan () yang memungkinkan Anda berlangganan Lihat Model.


Contoh penggunaan


Sekarang adalah waktunya untuk mencoba sesuatu dalam praktik. Saya mengusulkan untuk melakukan sesuatu yang sangat sederhana. Sesuatu yang tidak membutuhkan banyak upaya untuk memahami, dan pada saat yang sama memberikan gagasan tentang bagaimana menggunakan semua ini dan ke arah mana untuk melanjutkan. Biarkan itu menjadi generator UUID: dengan satu sentuhan tombol, kita akan menghasilkan UUID dan menampilkannya di layar.


Kiriman


Pertama, kami menggambarkan Model Tampilan:


 data class ViewModel(val text: String) 

Dan Lihat Acara:


 sealed class UiEvent { object OnGenerateClick: UiEvent() } 

Sekarang kita mengimplementasikan View itu sendiri, untuk ini kita perlu pewarisan dari kelas abstrak MviAbstractView:


 class View(activity: Activity) : MviAbstractView<ViewModel, UiEvent>() { private val textView = activity.findViewById<TextView>(R.id.text) init { activity.findViewById<Button>(R.id.button).setOnClickListener { dispatch(UiEvent.OnGenerateClick) } } override fun subscribe(models: Observable<ViewModel>): Disposable = models.map(ViewModel::text).distinctUntilChanged().subscribe { textView.text = it } } 

Semuanya sangat sederhana: kami berlangganan perubahan UUID dan memperbarui TextView ketika kami menerima UUID baru, dan ketika tombol diklik, kami mengirim acara OnGenerateClick.


Model


Model akan terdiri dari dua bagian: antarmuka dan implementasi.


Antarmuka:


 interface UuidStore : MviStore<State, Intent, Nothing> { data class State(val uuid: String? = null) sealed class Intent { object Generate : Intent() } } 

Semuanya sederhana di sini: antarmuka kami memperluas antarmuka MviStore, menunjukkan jenis State (Negara) dan Intent (Intent). Jenis Tag - Tidak Ada, karena Model kami tidak memproduksinya. Antarmuka juga berisi kelas State dan Intent.


Untuk mengimplementasikan Model, Anda perlu memahami cara kerjanya. Pada input Model, Intents diterima, yang dikonversi menjadi Aksi menggunakan fungsi IntentToAction khusus. Tindakan adalah masukan bagi Pelaksana, yang mengeksekusi mereka dan menghasilkan Hasil dan Label. Hasilnya kemudian pergi ke Reducer, yang mengubah Negara saat ini ke yang baru.


Keempat Model penulisan:


  • IntentToAction - fungsi yang mengubah Intent to Action
  • MviExecutor - Menjalankan Tindakan dan menghasilkan Hasil dan Tag
  • MviReducer - mengkonversi pasangan (Negara, Hasil) ke Negara baru
  • MviBootstrapper adalah komponen khusus yang memungkinkan Anda untuk menginisialisasi Model. Memberikan semua Tindakan yang sama yang juga pergi ke Pelaksana. Anda dapat melakukan Tindakan satu kali, atau Anda dapat berlangganan ke sumber data dan melakukan Tindakan pada peristiwa tertentu. Bootstrapper dimulai secara otomatis saat Anda membuat Model.

Untuk membuat Model itu sendiri, Anda harus menggunakan pabrik Model khusus. Itu diwakili oleh antarmuka MviStoreFactory dan penerapannya MviDefaultStoreFactory. Pabrik menerima komponen Model dan mengeluarkan Model yang siap digunakan.


Pabrik Model kami akan terlihat sebagai berikut:


 class UuidStoreFactory(private val factory: MviStoreFactory) { fun create(factory: MviStoreFactory): UuidStore = object : UuidStore, MviStore<State, Intent, Nothing> by factory.create( initialState = State(), bootstrapper = Bootstrapper, intentToAction = { when (it) { Intent.Generate -> Action.Generate } }, executor = Executor(), reducer = Reducer ) { } private sealed class Action { object Generate : Action() } private sealed class Result { class Uuid(val uuid: String) : Result() } private object Bootstrapper : MviBootstrapper<Action> { override fun bootstrap(dispatch: (Action) -> Unit): Disposable? { dispatch(Action.Generate) return null } } private class Executor : MviExecutor<State, Action, Result, Nothing>() { override fun invoke(action: Action): Disposable? { dispatch(Result.Uuid(UUID.randomUUID().toString())) return null } } private object Reducer : MviReducer<State, Result> { override fun State.reduce(result: Result): State = when (result) { is Result.Uuid -> copy(uuid = result.uuid) } } } 

Contoh ini menyajikan keempat komponen Model. Pertama, pabrik membuat metode, lalu Tindakan dan Hasil, diikuti oleh Kontraktor dan pada akhirnya Peredam.


Komponen


Status Komponen (Grup negara) dijelaskan oleh kelas data:


 data class States(val uuidStates: Observable<UuidStore.State>) 

Saat menambahkan Model baru ke Komponen, Statusnya juga harus ditambahkan ke grup.


Dan, pada kenyataannya, implementasinya sendiri:


 class Component(uuidStore: UuidStore) : MviAbstractComponent<UiEvent, States>( stores = listOf( MviStoreBundle( store = uuidStore, uiEventTransformer = UuidStoreUiEventTransformer ) ) ) { override val states: States = States(uuidStore.states) private object UuidStoreUiEventTransformer : (UiEvent) -> UuidStore.Intent? { override fun invoke(event: UiEvent): UuidStore.Intent? = when (event) { UiEvent.OnGenerateClick -> UuidStore.Intent.Generate } } } 

Kami mewarisi kelas abstrak MviAbstractComponent, menentukan jenis Negara dan Lihat Acara, meneruskan Model kami ke kelas super, dan menerapkan bidang negara. Selain itu, kami membuat fungsi transformasi yang akan mengubah Lihat Acara menjadi Intensi Model kami.


Memetakan Lihat Model


Kami memiliki Ketentuan dan Model Presentasi, sekarang saatnya untuk mengubah satu sama lain. Untuk melakukan ini, kami mengimplementasikan antarmuka MviViewModelMapper:


 object ViewModelMapper : MviViewModelMapper<States, ViewModel> { override fun map(states: States): Observable<ViewModel> = states.uuidStates.map { ViewModel(text = it.uuid ?: "None") } } 

Mengikat


Kehadiran Komponen dan Presentasi saja tidak cukup. Agar semuanya mulai bekerja, mereka harus terhubung. Saatnya membuat Aktivitas:


 class UuidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_uuid) bind( Component(UuidStoreFactory(MviDefaultStoreFactory).create()), View(this) using ViewModelMapper ) } } 

Kami menggunakan metode bind (), yang mengambil Component dan array Views dengan pemetaan dari Model mereka. Metode ini adalah metode ekstensi melalui LifecycleOwner (yang merupakan Aktivitas dan Fragmen) dan menggunakan DefaultLifecycleObserver dari paket Arch, yang membutuhkan kompatibilitas sumber Java 8. Jika karena alasan tertentu Anda tidak dapat menggunakan Java 8, maka metode bind () kedua cocok untuk Anda, yang bukan merupakan metode ekstensi dan mengembalikan MviLifecyleObserver. Dalam hal ini, Anda harus memanggil metode siklus hidup sendiri.


Referensi


Kode sumber perpustakaan, serta petunjuk terperinci untuk menghubungkan dan menggunakan dapat ditemukan di GitHub .

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


All Articles