Arsitektur MVI modern berdasarkan pada Kotlin



Selama dua tahun terakhir, pengembang Android di Badoo telah menempuh jalan panjang dan sulit dari MVP ke pendekatan arsitektur aplikasi yang sama sekali berbeda. ANublo dan saya ingin membagikan terjemahan sebuah artikel oleh rekan kami Zsolt Kocsi , menjelaskan masalah yang kami temui dan solusinya.

Ini adalah yang pertama dari beberapa artikel yang ditujukan untuk pengembangan arsitektur MVI modern di Kotlin.

Mari kita mulai dari awal: masalah negara


Setiap saat, aplikasi memiliki status tertentu yang menentukan perilakunya dan apa yang dilihat pengguna. Jika Anda hanya fokus pada beberapa kelas, status ini mencakup semua nilai variabel - mulai dari flag sederhana hingga objek individual. Masing-masing variabel hidup sendiri dan dikendalikan oleh bagian kode yang berbeda. Anda dapat menentukan kondisi aplikasi saat ini hanya dengan memeriksa semuanya satu per satu.

Bekerja pada kode, kami membuat model kerja sistem yang ada di kepala kami. Kami dengan mudah menerapkan kasus-kasus yang ideal ketika semuanya berjalan sesuai rencana, tetapi sama sekali tidak dapat menghitung semua masalah dan kondisi aplikasi yang mungkin. Dan cepat atau lambat, salah satu kondisi yang belum kita bayangkan akan menyusul kita, dan kita akan menemukan bug.

Awalnya, kode ini ditulis sesuai dengan ide kami tentang bagaimana sistem seharusnya bekerja. Tetapi di masa depan, melalui lima tahap debugging , Anda harus dengan susah payah mengulang semuanya, secara bersamaan mengubah model sistem yang sudah dibuat yang telah dikembangkan di kepala. Diharapkan cepat atau lambat kita akan mengerti apa yang salah dan bug akan diperbaiki.

Tapi ini jauh dari selalu beruntung. Semakin kompleks sistemnya, semakin besar kemungkinannya untuk menghadapi beberapa kondisi yang tidak terduga, debugging yang akan menjadi impian untuk waktu yang lama dalam mimpi buruk.

Di Badoo, semua aplikasi pada dasarnya tidak sinkron - tidak hanya karena fungsionalitas luas yang tersedia bagi pengguna melalui UI, tetapi juga karena kemungkinan pengiriman data satu arah oleh server. Keadaan dan perilaku aplikasi dipengaruhi oleh banyak hal - dari mengubah status pembayaran menjadi kecocokan baru dan permintaan verifikasi.

Akibatnya, dalam modul obrolan kami, kami menemukan beberapa bug aneh dan sulit untuk mereproduksi yang merusak banyak darah bagi semua orang. Kadang-kadang penguji berhasil menuliskannya, tetapi mereka tidak diulang pada perangkat pengembang. Karena kode asinkron, pengulangan yang penuh dengan rangkaian peristiwa sangat tidak mungkin. Dan karena aplikasi tidak macet, kami bahkan tidak memiliki jejak tumpukan yang menunjukkan tempat untuk memulai pencarian.

Arsitektur Bersih juga tidak dapat membantu kami. Bahkan setelah kami menulis ulang modul obrolan, tes A / B menunjukkan perbedaan kecil namun signifikan dalam jumlah pesan dari pengguna yang menggunakan modul baru dan lama. Kami memutuskan bahwa ini disebabkan oleh sulitnya reproduksi bug dan keadaan ras. Perbedaan tetap ada setelah memeriksa semua faktor lainnya. Kepentingan perusahaan menderita, sulit bagi pengembang untuk mempertahankan kode.

Anda tidak dapat merilis komponen baru jika berfungsi lebih buruk dari yang sudah ada, tetapi Anda juga tidak bisa melepaskannya - karena butuh pembaruan, ada alasannya. Jadi, Anda perlu memahami mengapa dalam sistem yang terlihat sangat normal dan tidak macet, jumlah pesan turun.

Di mana memulai pencarian?

Spoiler: ini bukan kesalahan Arsitektur Bersih - seperti biasa, faktor manusia yang harus disalahkan. Pada akhirnya, tentu saja, kami memperbaiki bug ini, tetapi menghabiskan banyak waktu dan upaya untuk ini. Kemudian kami berpikir: adakah cara yang lebih mudah untuk menghindari masalah ini?

Cahaya di ujung terowongan ...


Istilah modis seperti Model-View-Intent dan "aliran data searah" sudah tidak asing lagi bagi kami. Jika ini tidak terjadi dalam kasus Anda, saya sarankan Anda untuk google mereka - ada banyak artikel tentang topik ini di Internet. Pengembang Android terutama merekomendasikan materi delapan potong Hannes Dorfman .

Kami mulai bermain dengan ide-ide ini yang diambil dari pengembangan web pada awal 2017. Pendekatan seperti Flux dan Redux ternyata sangat berguna - mereka membantu kami menangani banyak masalah.

Pertama-tama, sangat berguna untuk memuat semua elemen state (variabel yang mempengaruhi UI dan memicu berbagai aksi) dalam satu objek - State . Ketika semuanya disimpan di satu tempat, gambar keseluruhan terlihat lebih baik. Misalnya, jika Anda ingin membayangkan memuat data menggunakan pendekatan ini, maka Anda memerlukan bidang muatan dan isian. Melihat mereka, Anda akan melihat kapan data diterima ( payload ) dan apakah animasi ( isLoading ) ditampilkan kepada pengguna.

Selanjutnya, jika kita beralih dari eksekusi kode paralel dengan panggilan balik dan menyatakan perubahan dalam status aplikasi sebagai serangkaian transaksi, kita akan mendapatkan satu titik masuk. Kami hadir Anda Reducer , yang datang kepada kami dari pemrograman fungsional. Dibutuhkan status saat ini dan data untuk tindakan lebih lanjut ( Intent ) dan membuat status baru dari mereka:

Reducer = (State, Intent) -> State

Melanjutkan contoh sebelumnya dengan memuat data, kami mendapatkan tindakan berikut:

  • MulaiMemuat
  • Selesai dengan Sukses


Kemudian Anda dapat membuat Reducer dengan aturan berikut:

  1. Dalam kasus StartingLoading, buat objek State baru dengan menyalin yang lama dan atur nilainya isLoading ke true.
  2. Dalam kasus FinishedWithSuccess, buat objek State baru, salin yang lama, di mana nilai isLoading akan disetel ke false, dan nilai payload akan menjadi
    pertandingan diunggah.

Jika kami menampilkan seri State yang dihasilkan ke log, kami akan melihat yang berikut:

  1. Status ( payload = null, isLoading = false) - status awal.
  2. Status ( payload = null, isLoading = true) - setelah StartingLoading.
  3. Status ( payload = data, isLoading = false) - setelah FinishedWithSuccess.

Dengan menghubungkan negara-negara ini ke UI, Anda akan melihat semua tahapan proses: pertama layar kosong, lalu layar pemuatan dan, akhirnya, data yang diperlukan.

Pendekatan ini memiliki banyak keunggulan.

  • Pertama, dengan mengubah keadaan secara terpusat menggunakan serangkaian transaksi, kami tidak mengizinkan kondisi ras dan banyak bug yang mengganggu yang tidak terlihat.
  • Kedua, setelah mempelajari serangkaian transaksi, kita dapat memahami apa yang terjadi, mengapa itu terjadi, dan bagaimana hal itu mempengaruhi keadaan aplikasi. Selain itu, dengan Reducer, jauh lebih mudah untuk membayangkan semua perubahan status sebelum peluncuran pertama aplikasi pada perangkat.
  • Akhirnya, kami dapat membuat antarmuka yang sederhana. Karena semua status disimpan di satu tempat (Store), yang memperhitungkan niat akun (Intents), membuat perubahan menggunakan Reducer dan menunjukkan rantai status, maka Anda bisa meletakkan semua logika bisnis di Store dan menggunakan antarmuka untuk meluncurkan niat dan menampilkan status.


Atau tidak?

... mungkin kereta bergegas ke arahmu


Peredam saja jelas tidak cukup. Bagaimana dengan tugas yang tidak sinkron dengan hasil yang berbeda? Bagaimana menanggapi push dari server? Bagaimana dengan peluncuran tugas tambahan (misalnya, membersihkan cache atau memuat data dari database lokal) setelah perubahan status? Ternyata kami tidak memasukkan semua logika ini dalam Reducer (yaitu, setengah dari logika bisnis tidak akan dibahas, dan itu harus diurus oleh mereka yang memutuskan untuk menggunakan komponen kami), atau kami memaksa Reducer untuk melakukan semuanya sekaligus.

Persyaratan Kerangka MVI


Tentu saja, kami ingin menyertakan seluruh logika bisnis dari fitur individual dalam komponen independen, yang dapat digunakan dengan mudah oleh pengembang dari tim lain dengan hanya membuat instance dan berlangganan ke kondisinya.

Selain itu:

  • Seharusnya mudah berinteraksi dengan komponen lain dari sistem;
  • dalam struktur internalnya harus ada pemisahan tugas yang jelas;
  • semua bagian internal komponen harus sepenuhnya deterministik;
  • implementasi dasar dari komponen semacam itu harus sederhana dan rumit hanya jika elemen tambahan diperlukan.

Kami tidak segera beralih dari Reducer ke solusi yang kami gunakan hari ini. Setiap tim menghadapi masalah dengan menggunakan pendekatan yang berbeda, dan mengembangkan solusi universal yang cocok untuk semua orang tampaknya tidak mungkin.

Namun, keadaan saat ini cocok untuk semua orang. Kami senang memperkenalkan Anda MVICore! Kode sumber perpustakaan terbuka dan tersedia di GitHub .

Apa yang baik MVICore


  • Cara mudah untuk mengimplementasikan fitur bisnis pemrograman reaktif dengan aliran data searah.
  • Penskalaan: implementasi dasar hanya mencakup Peredam, dan dalam kasus yang lebih kompleks, Anda dapat menggunakan komponen tambahan.
  • Solusi untuk bekerja dengan acara yang tidak ingin Anda sertakan di negara bagian ( masalah SingleLiveEvent ).
  • API sederhana untuk mengikat fitur (dan komponen reaktif lainnya dari sistem Anda) ke UI dan satu sama lain dengan dukungan untuk siklus hidup Android (dan tidak hanya).
  • Dukungan Middleware (lihat di bawah) untuk setiap komponen sistem.
  • Logger siap pakai dan kemampuan untuk melakukan debug perjalanan untuk setiap komponen.


Pengantar Singkat untuk Fitur


Karena petunjuk langkah demi langkah telah diposting di GitHub, saya akan menghilangkan contoh terperinci dan fokus pada komponen utama kerangka kerja.

Fitur - elemen utama dari kerangka kerja yang berisi semua logika bisnis komponen. Fitur didefinisikan oleh tiga parameter: Fitur antarmuka <Wish, State, News>

Wish sesuai dengan Intent dari Model-View-Intent - ini adalah perubahan yang ingin kita lihat dalam model (karena istilah Intent memiliki maknanya dalam lingkungan pengembang Android, kami harus menemukan nama yang berbeda). Keinginan adalah titik masuk untuk Fitur.

Keadaan adalah, seperti yang sudah Anda pahami, keadaan komponen. Negara tidak dapat berubah: kita tidak dapat mengubah nilai-nilai internalnya, tetapi kita dapat menciptakan Negara baru. Ini adalah output: setiap kali kita membuat negara baru, kita meneruskannya ke aliran Rx.

Berita - komponen untuk memproses sinyal yang seharusnya tidak berada di Negara; Berita digunakan sekali selama pembuatan ( masalah SingleLiveEvent ). Menggunakan Berita adalah opsional (Anda dapat menggunakan Tidak Ada dari Kotlin di tanda tangan Fitur).

Juga dalam Fitur harus ada Reducer .

Fitur dapat mengandung komponen-komponen berikut:

  • Aktor - melakukan tugas asinkron dan / atau modifikasi kondisi bersyarat berdasarkan kondisi saat ini (misalnya, validasi formulir). Aktor mengikat Wish ke nomor Efek tertentu, dan kemudian meneruskannya ke Reducer (tanpa adanya Aktor, Reducer menerima Wish secara langsung).
  • NewsPublisher - Dipanggil ketika Wish menjadi Efek apa pun yang menghasilkan hasil sebagai Negara baru. Berdasarkan data ini, ia memutuskan apakah akan membuat Berita.
  • PostProcessor - juga dipanggil setelah membuat Negara baru dan juga tahu apa yang menyebabkan efek penciptaannya. Ini meluncurkan tindakan tambahan tertentu (Tindakan). Tindakan - ini adalah "Keinginan internal" (misalnya, membersihkan cache) yang tidak dapat dimulai dari luar. Mereka dieksekusi di Aktor, yang mengarah ke rantai Efek dan Negara baru.
  • Bootstrapper adalah komponen yang dapat menjalankan tindakan sendiri. Fungsi utamanya adalah untuk menginisialisasi Fitur dan / atau menghubungkan sumber eksternal dengan Action. Sumber eksternal ini dapat berupa Berita dari Fitur lain atau data server yang harus memodifikasi Status tanpa campur tangan pengguna.


Diagram ini mungkin terlihat sederhana:


atau sertakan semua komponen tambahan di atas:


Fitur itu sendiri, yang berisi semua logika bisnis dan siap digunakan, terlihat lebih mudah:



Apa lagi


Fitur, landasan kerangka kerja, bekerja pada tingkat konseptual. Tetapi perpustakaan memiliki lebih banyak untuk ditawarkan.

  • Karena semua komponen Fitur bersifat deterministik (dengan pengecualian Aktor, yang tidak sepenuhnya deterministik karena berinteraksi dengan sumber data eksternal, tetapi bahkan dengan itu, cabang yang dijalankan ditentukan oleh data input, dan bukan oleh kondisi eksternal), masing-masing dapat dibungkus dalam Middleware. Pada saat yang sama, perpustakaan sudah berisi solusi yang sudah jadi untuk logging dan debugging perjalanan waktu .
  • Middleware berlaku tidak hanya untuk Fitur, tetapi juga untuk objek lain yang mengimplementasikan antarmuka <T> Konsumen, yang menjadikannya alat debugging yang sangat diperlukan.
  • Saat menggunakan debugger untuk debugging sambil bergerak ke arah yang berlawanan, Anda dapat mengimplementasikan modul DebugDrawer .
  • Pustaka menyertakan plugin IDEA yang dapat digunakan untuk menambahkan templat untuk implementasi Fitur yang paling umum, yang menghemat banyak waktu.
  • Ada kelas pembantu untuk mendukung Android, tetapi perpustakaan itu sendiri tidak terikat dengan Android.
  • Ada solusi yang sudah jadi untuk mengikat komponen ke UI dan satu sama lain melalui API dasar (ini akan dibahas dalam artikel berikutnya).

Kami harap Anda akan mencoba perpustakaan kami dan penggunaannya akan membawa Anda sebanyak kesenangan seperti kami - pembuatannya!

Pada 24 dan 25 November, Anda dapat mencoba dan bergabung dengan kami! Kami akan mengadakan acara perekrutan seluler: dalam satu hari akan memungkinkan untuk melalui semua tahap seleksi dan menerima penawaran. Rekan-rekan saya dari tim iOS dan Android akan datang untuk berkomunikasi dengan para kandidat di Moskow. Jika Anda berasal dari kota lain, Badoo dikenai biaya perjalanan. Untuk mendapatkan undangan, ikuti tes penyaringan di tautan . Semoga beruntung

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


All Articles