Template Arsitektur MVI di Kotlin Multiplatform, Bagian 1



Sekitar setahun yang lalu, saya menjadi tertarik pada teknologi Multiplatform Kotlin baru. Ini memungkinkan Anda untuk menulis kode umum dan mengkompilasinya untuk platform yang berbeda, sambil memiliki akses ke API mereka. Sejak itu saya telah aktif bereksperimen di bidang ini dan mempromosikan alat ini di perusahaan kami. Salah satu hasil, misalnya, adalah perpustakaan Reaktive kami - Ekstensi Reaktif untuk Multiplatform Kotlin.

Dalam aplikasi Badoo dan Bumble untuk pengembangan untuk Android, kami menggunakan template arsitektur MVI (untuk detail lebih lanjut tentang arsitektur kami, lihat artikel oleh Zsolt Kocsi: “ Arsitektur MVI modern berbasis Kotlin"). Bekerja di berbagai proyek, saya menjadi penggemar berat pendekatan ini. Tentu saja, saya tidak dapat melewatkan kesempatan untuk mencoba MVI di Kotlin Multiplatform. Selain itu, kasusnya cocok: kami perlu menulis contoh untuk perpustakaan Reaktive. Setelah percobaan saya ini, saya bahkan lebih terinspirasi oleh MVI.

Saya selalu memperhatikan bagaimana pengembang menggunakan Multiplatform Kotlin dan bagaimana mereka membangun arsitektur proyek-proyek tersebut. Menurut pengamatan saya, rata-rata pengembang Multipliner Kotlin sebenarnya adalah pengembang Android yang menggunakan templat MVVM dalam karyanya hanya karena ia sudah terbiasa. Beberapa tambahan menerapkan "arsitektur bersih". Namun, menurut saya, MVI paling cocok untuk Kotlin Multiplatform, dan "arsitektur bersih" adalah komplikasi yang tidak perlu.

Karena itu, saya memutuskan untuk menulis seri tiga artikel ini dengan topik-topik berikut:

  1. Deskripsi singkat tentang templat MVI, pernyataan masalah dan pembuatan modul umum menggunakan Multiplatform Kotlin.
  2. Integrasi modul umum dalam aplikasi iOS dan Android.
  3. Pengujian unit dan integrasi.

Di bawah ini adalah artikel pertama dalam seri ini. Ini akan menarik bagi semua orang yang sudah menggunakan atau hanya berencana untuk menggunakan Multiplatform Kotlin.

Saya segera mencatat bahwa tujuan artikel ini bukan untuk mengajarkan Anda cara bekerja dengan Multiplatform Kotlin itu sendiri. Jika Anda merasa tidak memiliki cukup pengetahuan di bidang ini, saya sarankan Anda untuk membiasakan diri dengan pengenalan dan dokumentasi (terutama bagian “ Konkurensi ” dan “ Kekebalan ” untuk memahami fitur model memori Kotlin / Asli). Dalam artikel ini saya tidak akan menjelaskan konfigurasi proyek, modul, dan hal-hal lain yang tidak terkait dengan topik.

MVI


Pertama, mari kita ingat apa itu MVI. Singkatan singkatan dari Model-View-Intent. Hanya ada dua komponen utama dalam sistem:

  • model - lapisan logika dan data (model juga menyimpan kondisi sistem saat ini);
  • (View) — UI-, (states) (intents).

Diagram berikut ini mungkin sudah akrab bagi banyak orang:



Jadi, kita melihat komponen-komponen yang sangat mendasar: model dan presentasi. Yang lainnya adalah data yang beredar di antara mereka.

Sangat mudah untuk melihat bahwa data hanya bergerak dalam satu arah. Negara datang dari model dan jatuh ke tampilan untuk tampilan, niat datang dari tampilan dan masuk ke model untuk diproses. Sirkulasi ini disebut Aliran Data Searah.

Dalam praktiknya, suatu model sering diwakili oleh entitas yang disebut Store (dipinjam dari Redux). Namun, ini tidak selalu terjadi. Sebagai contoh, di perpustakaan MVICore kami , model ini disebut Fitur.

Perlu juga dicatat bahwa MVI sangat erat kaitannya dengan reaktivitas. Penyajian aliran data dan transformasi mereka, serta manajemen siklus langganan sangat nyaman untuk diimplementasikan menggunakan perpustakaan pemrograman reaktif. Sejumlah besar dari mereka sekarang tersedia, namun, ketika menulis kode umum di Kotlin Multiplatform, kita hanya dapat menggunakan pustaka multi-platform. Kita perlu abstraksi untuk stream data, kita perlu kemampuan untuk menghubungkan dan memutuskan input dan output mereka, serta melakukan transformasi. Saat ini, saya tahu dua perpustakaan seperti itu:

  • perpustakaan Reaktive kami - implementasi Extensions Reaktif di Kotlin Multiplatform;
  • Coroutine and Flow - implementasi aliran dingin dengan coroutine Kotlin.

Perumusan masalah


Tujuan artikel ini adalah untuk menunjukkan cara menggunakan templat MVI di Kotlin Multiplatform dan apa kelebihan dan kekurangan dari pendekatan ini. Karenanya, saya tidak akan terikat pada implementasi MVI tertentu. Namun, saya akan menggunakan Reaktive, karena aliran data masih diperlukan. Jika diinginkan, setelah memahami gagasan itu, Reaktive dapat digantikan oleh coroutine dan Flow. Secara umum, saya akan mencoba membuat MVI kami sesederhana mungkin, tanpa komplikasi yang tidak perlu.

Untuk menunjukkan MVI, saya akan mencoba menerapkan proyek paling sederhana yang memenuhi persyaratan berikut:

  • dukungan untuk Android dan iOS;
  • Demonstrasi operasi asinkron (input-output, pemrosesan data, dll.);
  • kode umum sebanyak mungkin;
  • Implementasi UI dengan alat asli dari setiap platform;
  • kurangnya Rx di sisi platform (sehingga Anda tidak perlu menentukan dependensi pada Rx sebagai "api").

Sebagai contoh, saya memilih aplikasi yang sangat sederhana: satu layar dengan tombol, dengan mengklik di mana daftar dengan gambar kucing yang sewenang-wenang akan diunduh dan ditampilkan. Untuk mengunggah gambar, saya akan menggunakan API terbuka: https://thecatapi.com . Ini akan memungkinkan Anda untuk memenuhi persyaratan operasi asinkron, karena Anda harus mengunduh daftar dari Web dan mem-parsing file JSON.

Anda dapat menemukan semua kode sumber proyek di GitHub kami .

Persiapan: abstraksi untuk MVI


Pertama kita perlu memperkenalkan beberapa abstraksi untuk MVI kita. Kita akan membutuhkan komponen yang sangat mendasar - model dan tampilan - dan beberapa typealias.

Jenis huruf


Untuk memproses niat, kami memperkenalkan aktor (Aktor) - fungsi yang menerima niat dan keadaan saat ini dan mengembalikan aliran hasil (Efek):

typealias Aktor < Negara , Intent , Effect > = ( Negara , Intent ) - > diamati < Effect >

Kami juga membutuhkan peredam (Peredam) - fungsi yang mengambil efek dan kondisi saat ini dan mengembalikan status baru:

typealias Reducer < Negara , Efek > = ( Negara , Effect ) - > Negara

Toko


Store akan menghadirkan model dari MVI. Dia harus menerima niat dan memberikan aliran negara. Ketika berlangganan aliran negara, negara saat ini harus dikeluarkan.

Mari kita perkenalkan antarmuka yang sesuai:

antarmuka Toko < di Intent : Setiap , keluar Negara : Setiap > : Konsumen < Intent >, diamati < Negara >, pakai
lihat mentah MviKmpStoreInterface.kt diselenggarakan dengan ❤ oleh GitHub

Jadi, Toko kami memiliki properti berikut:

  • memiliki dua parameter umum: Intent input dan Status output;
  • adalah konsumen yang memiliki niat (Konsumen <Intent>);
  • adalah aliran negara (Dapat diamati <State>);
  • itu dirusak (Disposable).

Karena tidak nyaman untuk mengimplementasikan antarmuka seperti itu setiap kali, kita akan memerlukan asisten tertentu:

class StoreHelper < dalam Intent : Any , out State : Any , in Effect : Any > (
initialState : State ,
aktor val pribadi : Aktor < State , Intent , Effect >,
private val reducer: Reducer<State, Effect>
) : Observable<State>, DisposableScope by DisposableScope() {
init {
ensureNeverFrozen()
}
private val subject = BehaviorSubject(initialState)
fun onIntent(intent: Intent) {
actor(subject.value, intent).subscribeScoped(isThreadLocal = true, onNext = ::onEffect)
}
fun onEffect ( efek : Efek ) {
subject.onNext (peredam (subject.value, efek))
}
override fun subscribe ( observer : ObservableObserver < State >) {
subject.subscribe (pengamat)
}
}
lihat mentah MviKmpStoreHelper.kt diselenggarakan dengan ❤ oleh GitHub


StoreHelper adalah kelas kecil yang akan memudahkan kita untuk membuat Toko. Ini memiliki sifat-sifat berikut:

  • memiliki tiga parameter umum: Intent input dan Efek dan Status output;
  • menerima keadaan awal melalui konstruktor, aktor dan gearbox;
  • adalah aliran negara;
  • destructible (Disposable);
  • non-beku (sehingga pelanggan juga tidak membeku );
  • mengimplementasikan DisposableScope (antarmuka dari Reaktive untuk mengelola langganan);
  • menerima dan memproses niat dan efek.

Lihatlah diagram Toko kami. Aktor dan gearbox di dalamnya adalah detail implementasi:



Mari kita pertimbangkan metode onIntent lebih detail:

  • menerima niat sebagai argumen;
  • memanggil aktor dan melewati niat dan keadaan saat ini ke dalamnya;
  • berlangganan aliran efek yang dikembalikan oleh aktor;
  • mengarahkan semua efek ke metode onEffect;
  • Berlangganan efek dilakukan menggunakan bendera isThreadLocal (ini menghindari pembekuan di Kotlin / Asli).

Sekarang mari kita melihat lebih dekat pada metode onEffect:

  • mengambil efek sebagai argumen;
  • memanggil gearbox dan mentransfer efek dan keadaan saat ini ke dalamnya;
  • melewati negara baru ke BehaviorSubject, yang mengarah ke penerimaan negara baru oleh semua pelanggan.

Melihat


Sekarang mari masuk ke presentasi. Ini harus menerima model untuk ditampilkan dan memberikan aliran acara. Kami juga akan membuat antarmuka terpisah:

antarmuka MviView < dalam Model : Any , out Event : Any > {
val events : Dapat diamati < Event >
fun render ( model : Model )
}


Tampilan memiliki properti berikut:

  • memiliki dua parameter umum: Model input dan Output event;
  • menerima model untuk ditampilkan menggunakan metode render;
  • mengirimkan aliran acara menggunakan properti acara.

Saya menambahkan awalan Mvi ke nama MviView untuk menghindari kebingungan dengan Android View. Juga, saya tidak memperluas antarmuka Consumer and Observable, tetapi hanya menggunakan properti dan metode. Ini agar Anda dapat mengatur antarmuka presentasi ke platform untuk implementasi (Android atau iOS) tanpa mengekspor Rx sebagai ketergantungan "api". Caranya adalah bahwa klien tidak akan langsung berinteraksi dengan properti "events", tetapi akan mengimplementasikan antarmuka MviView, memperluas kelas abstrak.

Segera tambahkan kelas abstrak ini untuk mewakili:

abstract class AbstractMviView<in Model : Any, Event : Any> : MviView<Model, Event> {
private val subject = PublishSubject<Event>()
override val events: Observable<Event> = subject
protected fun dispatch(event: Event) {
subject.onNext(event)
}
}

Kelas ini akan membantu kami dengan penerbitan acara, dan juga menyelamatkan platform dari berinteraksi dengan Rx.

Berikut adalah diagram yang menunjukkan cara kerjanya:



Toko menghasilkan status yang diubah menjadi model dan ditampilkan oleh tampilan. Yang terakhir menghasilkan acara yang dikonversi menjadi niat dan dikirimkan ke Store untuk diproses. Pendekatan ini menghilangkan koherensi antara Store dan presentasi. Tetapi dalam kasus-kasus sederhana, suatu pandangan dapat bekerja secara langsung dengan keadaan dan niat.

Itu saja yang kita butuhkan untuk mengimplementasikan MVI. Mari kita menulis kode umum.

Kode umum


Rencana


  1. Kami akan membuat modul umum yang tugasnya mengunduh dan menampilkan daftar gambar kucing.
  2. UI kami abstrak antarmuka dan kami akan mentransfer implementasinya di luar.
  3. Kami akan menyembunyikan implementasi kami di balik fasad yang nyaman.

Kittenstore


Mari kita mulai dengan hal utama - buat KittenStore yang akan memuat daftar gambar:

antarmuka internal KittenStore : Store < Intent , State > {
Intent kelas tersegel {
objek Muat Ulang : Intent ()
}
Negara kelas data (
val isLoading : Boolean = false ,
val data : Data = Data . Gambar ()
) {
Data kelas tersegel {
Data kelas Gambar ( val url : Daftar < String > = emptyList ()) : data ()
objek error : Data ()
}
}
}
lihat mentah MviKmpKittenStoreInterface.kt diselenggarakan dengan ❤ oleh GitHub


Kami telah memperluas antarmuka Store dengan tipe maksud dan tipe status. Harap dicatat: antarmuka dinyatakan sebagai internal. KittenStore kami adalah detail implementasi modul. Niat kami hanya satu - Muat ulang, itu menyebabkan pemuatan daftar gambar. Tetapi kondisinya patut dipertimbangkan secara lebih rinci:

  • bendera isLoading menunjukkan apakah unduhan saat ini sedang berlangsung atau tidak;
  • Properti data dapat mengambil salah satu dari dua opsi:
    • Gambar - daftar tautan ke gambar;
    • Kesalahan - berarti kesalahan telah terjadi.

Sekarang mari kita mulai implementasinya. Kami akan melakukan ini secara bertahap. Pertama, buat kelas KittenStoreImpl kosong yang akan mengimplementasikan antarmuka KittenStore:

kelas internal KittenStoreImpl (
) : KittenStore , DisposableScope oleh DisposableScope () {
menimpa kesenangan onNext ( nilai : Intent ) {
}
override fun subscribe ( observer : ObservableObserver < State >) {
}
}


Kami juga mengimplementasikan antarmuka DisposableScope yang sudah dikenal. Ini diperlukan untuk manajemen langganan yang nyaman.

Kita perlu mengunduh daftar gambar dari Web dan mem-parsing file JSON. Nyatakan dependensi yang sesuai:

kelas internal KittenStoreImpl (
jaringan val pribadi : Jaringan ,
parser val pribadi : Parser
) : KittenStore, DisposableScope by DisposableScope() {
override fun onNext(value: Intent) {
}
override fun subscribe(observer: ObservableObserver<State>) {
}
interface Network {
fun load(): Maybe<String>
}
interface Parser {
fun parse ( json : String ) : Mungkin < List < String >>
}
}
lihat mentah MviKmpKittenStoreImpl2.kt diselenggarakan dengan ❤ oleh GitHub

Jaringan akan mengunduh teks untuk file JSON dari Jaringan, dan Parser akan mem-parsing file JSON dan mengembalikan daftar tautan gambar. Jika terjadi kesalahan, Mungkin hanya akan berakhir tanpa hasil. Pada artikel ini, kami tidak tertarik pada jenis kesalahan.

Sekarang nyatakan efek dan peredam:

kelas internal KittenStoreImpl (
jaringan val pribadi : Jaringan ,
parser val pribadi : Parser
) : KittenStore, DisposableScope by DisposableScope() {
override fun onNext(value: Intent) {
}
override fun subscribe(observer: ObservableObserver<State>) {
}
private fun reduce(state: State, effect: Effect): State =
when (effect) {
is Effect.LoadingStarted -> state.copy(isLoading = true)
is Effect.LoadingFinished -> state.copy(isLoading = false, data = State.Data.Images(urls = effect.imageUrls))
is Effect.LoadingFailed -> state.copy(isLoading = false, data = State.Data.Error)
}
private sealed class Effect {
object LoadingStarted : Effect()
data class LoadingFinished(val imageUrls: List<String>) : Effect()
object LoadingFailed : Effect()
}
interface Network {
fun load(): Maybe<String>
}
interface Parser {
fun parse ( json : String ) : Mungkin < List < String >>
}
}

Sebelum memulai unduhan, kami memberikan efek LoadingStarted, yang menyebabkan flag isLoading diatur. Setelah unduhan selesai, kami mengeluarkan LoadingFinished atau LoadingFailed. Dalam kasus pertama, kami menghapus bendera isLoading dan menerapkan daftar gambar, di yang kedua, kami juga menghapus bendera dan menerapkan status kesalahan. Harap perhatikan bahwa efek adalah API pribadi KittenStore kami.

Sekarang kami menerapkan unduhan itu sendiri:

kelas internal KittenStoreImpl (
private val network: Network,
private val parser: Parser
) : KittenStore, DisposableScope by DisposableScope() {
override fun onNext(value: Intent) {
}
override fun subscribe(observer: ObservableObserver<State>) {
}
private fun reload(network: Network, parser: Parser): Observable<Effect> =
network
.load()
.flatMap(parser::parse)
.map(Effect::LoadingFinished)
.observeOn(mainScheduler)
.asObservable()
.defaultIfEmpty(Effect.LoadingFailed)
.startWithValue(Effect.LoadingStarted)
private fun reduce(state: State, effect: Effect): State =
// Omitted code
private sealed class Effect {
// Omitted code
}
interface Network {
fun load(): Maybe<String>
}
interface Parser {
fun parse(json: String): Maybe<List<String>>
}
}

Di sini perlu memperhatikan fakta bahwa kami melewati Network dan Parser ke fungsi memuat ulang, meskipun faktanya mereka sudah tersedia bagi kami sebagai properti dari konstruktor. Ini dilakukan untuk menghindari referensi tentang ini dan, sebagai akibatnya, membekukan seluruh KittenStore.

Akhirnya, gunakan StoreHelper dan selesaikan implementasi KittenStore:

kelas internal KittenStoreImpl (
jaringan val pribadi : Jaringan ,
parser val pribadi : Parser
) : KittenStore , DisposableScope oleh DisposableScope () {
private val helper = StoreHelper(State(), ::handleIntent, ::reduce).scope()
override fun onNext(value: Intent) {
helper.onIntent(value)
}
override fun subscribe(observer: ObservableObserver<State>) {
helper.subscribe(observer)
}
private fun handleIntent(state: State, intent: Intent): Observable<Effect> =
when (intent) {
is Intent.Reload -> reload(network, parser)
}
private fun reload(network: Network, parser: Parser): Observable<Effect> =
// Omitted code
private fun reduce(state: State, effect: Effect): State =
// Omitted code
private sealed class Effect {
// Omitted code
}
interface Network {
fun load () : Mungkin < String >
}
Parser antarmuka {
fun parse ( json : String ) : Mungkin < List < String >>
}
}

KittenStore kami siap! Kami lolos ke presentasi.

Kittenview


Deklarasikan antarmuka berikut:

antarmuka KittenView : MviView < Model , Event > {
Model kelas data (
val isLoading : Boolean ,
val isError : Boolean ,
val imageUrls : Daftar < String >
)
acara kelas tertutup {
objek RefreshTriggered : Event ()
}
}
lihat mentah MviKmpKittenViewInterface.kt diselenggarakan dengan ❤ oleh GitHub

Kami mengumumkan model tampilan dengan flag load and error dan daftar tautan gambar. Kami hanya memiliki satu acara - RefreshTriggered. Itu dikeluarkan setiap kali pengguna meminta pembaruan. KittenView adalah API publik dari modul kami.

KittenDataSource


Tugas sumber data ini adalah mengunduh teks untuk file JSON dari Web. Seperti biasa, deklarasikan antarmuka:

antarmuka internal KittenDataSource {
fun load ( limit : Int , offset : Int ) : Mungkin < String >
}

Implementasi sumber data akan dibuat untuk setiap platform secara terpisah. Oleh karena itu, kita dapat mendeklarasikan metode pabrik menggunakan ekspektasi / aktual:

internal mengharapkan kesenangan KittenDataSource () : KittenDataSource
lihat mentah MviKmpKittenDataSourceFactory.kt diselenggarakan dengan ❤ oleh GitHub

Implementasi sumber data akan dibahas pada bagian selanjutnya, di mana kami akan mengimplementasikan aplikasi untuk iOS dan Android.

Integrasi


Tahap terakhir adalah integrasi semua komponen.

Implementasi antarmuka jaringan:

kelas internal KittenStoreNetwork (
private val dataSource : KittenDataSource
) : KittenStoreImpl . Jaringan {
menimpa fun load () : Mungkin < String > = dataSource.load (limit = 50 , offset = 0 )
}
lihat mentah KittenStoreNetwork.kt diselenggarakan dengan ❤ oleh GitHub


Implementasi antarmuka Parser:

objek internal KittenStoreParser : KittenStoreImpl . Parser {
override fun parse ( json : String ) : Mungkin < List < String >> =
mungkinFromFungsi {
Json ( JsonConfiguration . Stable )
.parseJson (json)
.jsonArray
.map {it.jsonObject.getPrimitive ( " url " ) .content}
}
.subscribeOn (computationScheduler)
.onErrorComplete ()
}
lihat mentah KittenStoreParser.kt diselenggarakan dengan ❤ oleh GitHub

Di sini kami menggunakan perpustakaan kotlinx.serialization . Parsing dilakukan pada penjadwal komputasi untuk menghindari pemblokiran utas utama.

Konversi status untuk melihat model:

Keadaan menyenangkan internal . toModel () : Model =
Model (
isLoading = isLoading,
isError = when ( data ) {
adalah Negara . Data . Gambar - > salah
adalah Negara . Data . Kesalahan - > benar
},
imageUrls = kapan ( data ) {
adalah Negara . Data . Gambar - > data .urls
adalah Negara . Data . Kesalahan - > blankList ()
}
)
lihat mentah MviKmpStateToModel.kt diselenggarakan dengan ❤ oleh GitHub

Konversi acara menjadi niat:

Acara menyenangkan internal . toIntent () : Intent =
kapan ( ini ) {
adalah Event . RefreshTriggered - > Intent . Muat ulang
}

Persiapan fasad:

kelas KittenComponent {
fun onViewCreated ( lihat : KittenView ) {
}
fun onStart () {
}
fun onStop () {
}
fun onViewDestroyed () {
}
fun onDestroy () {
}
}
lihat mentah MviKmpKittenComponent1.kt diselenggarakan dengan ❤ oleh GitHub

Akrab bagi banyak siklus hidup pengembang Android. Ini bagus untuk iOS, dan bahkan JavaScript. Diagram transisi antara keadaan siklus hidup fasad kami terlihat seperti ini: Saya akan


menjelaskan secara singkat apa yang terjadi di sini:

  • pertama, metode onCreate dipanggil, setelah itu - onViewCreated, dan kemudian - onStart: ini menempatkan fasad dalam kondisi kerja (dimulai);
  • pada titik tertentu setelah ini, metode onStop disebut: ini menempatkan fasad dalam keadaan berhenti (dihentikan);
  • dalam keadaan terhenti, salah satu dari dua metode dapat disebut: onStart atau onViewDestroyed, yaitu, baik fasad dapat dimulai lagi, atau pandangannya dapat dihancurkan;
  • ketika tampilan dihancurkan, bisa dibuat lagi (onViewCreated), atau seluruh fasad dapat dihancurkan (onDestroy).

Implementasi fasad mungkin terlihat seperti ini:

kelas KittenComponent {
toko val pribadi =
KittenStoreImpl (
network = KittenStoreNetwork (dataSource = KittenDataSource ()),
parser = KittenStoreParser
)
swasta var tampilan : KittenView? = nol
private var startStopScope : DisposableScope? = nol
fun onViewCreated ( lihat : KittenView ) {
ini .view = view
}
fun onStart() {
val view = requireNotNull(view)
startStopScope = disposableScope {
store.subscribeScoped(onNext = view::render)
view.events.map(Event::toIntent).subscribeScoped(onNext = store::onNext)
}
}
fun onStop() {
startStopScope?.dispose()
}
fun onViewDestroyed() {
view = null
}
fun onDestroy() {
store.dispose()
}
}

Bagaimana itu bekerja:

  • pertama-tama kita membuat instance KittenStore;
  • dalam metode onViewCreated kita mengingat tautan ke KittenView;
  • di onStart kami menandatangani KittenStore dan KittenView satu sama lain;
  • di onStop kita cerminkan mereka dari satu sama lain;
  • di onViewDestroyed kami menghapus tautan ke tampilan;
  • di onDestroy, hancurkan KittenStore.

Kesimpulan


Ini adalah artikel pertama dalam seri MVI saya di Kotlin Multiplatform. Di dalamnya kita:

  • ingat apa itu MVI dan bagaimana cara kerjanya;
  • membuat implementasi MVI paling sederhana di Kotlin Multiplatform menggunakan perpustakaan Reaktive;
  • membuat modul umum untuk memuat daftar gambar menggunakan MVI.

Perhatikan properti paling penting dari modul umum kami:

  • kami berhasil memasukkan semua kode ke dalam modul multi platform kecuali untuk kode UI; semua logika, ditambah koneksi dan konversi antara logika dan UI adalah umum;
  • logika dan UI sama sekali tidak terkait;
  • implementasi UI sangat sederhana: Anda hanya perlu menampilkan model tampilan yang masuk dan melempar acara;
  • integrasi modul juga sederhana: yang Anda butuhkan adalah:
    • mengimplementasikan antarmuka KittenView (protokol);
    • buat instance KittenComponent;
    • sebut metode siklus hidupnya pada waktu yang tepat;
  • pendekatan ini menghindari "aliran" Rx (atau coroutine) ke dalam platform, yang berarti bahwa kita tidak harus mengelola langganan apa pun di tingkat aplikasi;
  • semua kelas penting diabstraksikan oleh antarmuka dan diuji.

Pada bagian selanjutnya, saya akan menunjukkan dalam praktik seperti apa integrasi KittenComponent di aplikasi iOS dan Android.

Ikuti saya di Twitter dan tetap terhubung!

All Articles