Membangun sistem komponen reaktif dengan Kotlin



Halo semuanya! Nama saya Anatoly Varivonchik, saya adalah pengembang Android di Badoo. Hari ini saya akan berbagi dengan Anda terjemahan bagian kedua artikel oleh rekan saya Zsolt Kocsi tentang implementasi MVI, yang kami gunakan setiap hari dalam proses pengembangan. Bagian pertama ada di sini .

Apa yang kita inginkan dan bagaimana kita melakukannya


Di bagian pertama artikel, kami memperkenalkan Fitur , elemen utama MVICore yang dapat digunakan kembali. Mereka dapat memiliki struktur paling sederhana dan hanya menyertakan satu Peredam , atau mereka dapat menjadi alat yang berfungsi penuh untuk mengelola tugas, peristiwa, dan banyak lagi yang tidak sinkron.

Setiap Fitur dapat dilacak - ada peluang untuk berlangganan perubahan dalam statusnya dan menerima pemberitahuan tentang hal itu. Namun, Fitur dapat berlangganan ke sumber input. Dan ini masuk akal, karena dengan dimasukkannya Rx dalam basis kode, kita sudah memiliki banyak objek dan langganan yang dapat diamati di berbagai tingkatan.

Sehubungan dengan peningkatan jumlah komponen reaktif inilah saatnya untuk merenungkan apa yang kita miliki dan apakah mungkin untuk membuat sistem lebih baik.

Kami harus menjawab tiga pertanyaan:

  1. Elemen apa yang harus digunakan ketika menambahkan komponen reaktif baru?
  2. Apa cara termudah untuk mengelola langganan Anda?
  3. Apakah mungkin untuk mengabaikan manajemen siklus hidup / kebutuhan untuk menghapus langganan untuk menghindari kebocoran memori? Dengan kata lain, dapatkah kita memisahkan ikatan komponen dari manajemen berlangganan?

Pada bagian artikel ini, kita akan melihat dasar-dasar dan manfaat membangun sistem menggunakan komponen reaktif dan melihat bagaimana Kotlin membantu dalam hal ini.

Elemen utama


Pada saat kami mulai mengerjakan desain dan standardisasi Fitur kami, kami telah mencoba berbagai pendekatan dan memutuskan bahwa Fitur tersebut akan dalam bentuk komponen reaktif. Pertama, kami fokus pada antarmuka utama. Pertama-tama, kami perlu menentukan jenis input dan output data.

Kami beralasan sebagai berikut:

  • Mari kita tidak menciptakan kembali roda - mari kita lihat antarmuka apa yang sudah ada.
  • Karena kita sudah menggunakan pustaka RxJava, masuk akal untuk merujuk ke antarmuka dasarnya.
  • Jumlah antarmuka harus diminimalkan.

Akibatnya, kami memutuskan untuk menggunakan ObservableSource <T> untuk output dan Konsumen <T> untuk input. Mengapa tidak Teramati / Pengamat , Anda bertanya. Observable adalah kelas abstrak yang harus Anda warisi, dan ObservableSource adalah antarmuka yang Anda implementasikan yang sepenuhnya memenuhi kebutuhan untuk mengimplementasikan protokol reaktif.

package io.reactivex; import io.reactivex.annotations.*; /** * Represents a basic, non-backpressured {@link Observable} source base interface, * consumable via an {@link Observer}. * * @param <T> the element type * @since 2.0 */ public interface ObservableSource<T> { /** * Subscribes the given Observer to this ObservableSource instance. * @param observer the Observer, not null * @throws NullPointerException if {@code observer} is null */ void subscribe(@NonNull Observer<? super T> observer); } 

Pengamat , antarmuka pertama yang terlintas dalam pikiran, menerapkan empat metode: onSubscribe, onNext, onError, dan onComplete. Dalam upaya menyederhanakan protokol sebanyak mungkin, kami lebih suka Konsumen <T> , yang menerima elemen baru menggunakan metode tunggal. Jika kami memilih Pengamat , maka metode yang tersisa akan paling sering menjadi berlebihan atau bekerja secara berbeda (misalnya, kami ingin menyajikan kesalahan sebagai bagian dari Negara , dan bukan sebagai pengecualian, dan tentu saja tidak mengganggu aliran).

 /** * A functional interface (callback) that accepts a single value. * @param <T> the value type */ public interface Consumer<T> { /** * Consume the given value. * @param t the value * @throws Exception on error */ void accept(T t) throws Exception; } 

Jadi, kami memiliki dua antarmuka, yang masing-masing berisi satu metode. Sekarang kita dapat mengikat mereka dengan menandatangani Konsumen <T> ke ObservableSource <T> . Yang terakhir hanya menerima instance dari Pengamat <T> , tetapi kami dapat membungkusnya dengan Observable <T> , yang berlangganan Pelanggan <T> :

 val output: ObservableSource<String> = Observable.just("item1", "item2", "item3") val input: Consumer<String> = Consumer { System.out.println(it) } val disposable = Observable.wrap(output).subscribe(input) 

(Untungnya, fungsi .wrap (output) tidak membuat objek baru jika output sudah menjadi Observable <T> ).

Anda mungkin ingat bahwa komponen Fitur dari bagian pertama artikel menggunakan input data tipe Wish (sesuai dengan Intent dari Model-View-Intent) dan output dari tipe Negara , dan karena itu bisa di kedua sisi bundel:

 // Wishes -> Feature val wishes: ObservableSource<Wish> = Observable.just(Wish.SomeWish) val feature: Consumer<Wish> = SomeFeature() val disposable = Observable.wrap(wishes).subscribe(feature) // Feature -> State consumer val feature: ObservableSource<State> = SomeFeature() val logger: Consumer<State> = Consumer { System.out.println(it) } val disposable = Observable.wrap(feature).subscribe(logger) 

Tautan antara Konsumen dan Produsen ini sudah terlihat cukup sederhana, tetapi ada cara yang bahkan lebih mudah di mana Anda tidak perlu membuat langganan secara manual atau membatalkannya.

Memperkenalkan Binder .

Mengikat steroid


MVICore berisi kelas yang disebut Binder yang menyediakan API sederhana untuk mengelola langganan Rx dan memiliki sejumlah fitur keren.

Mengapa itu dibutuhkan?

  • Buat ikatan dengan berlangganan input ke akhir pekan.
  • Kemampuan untuk berhenti berlangganan pada akhir siklus hidup (ketika itu adalah konsep abstrak dan tidak ada hubungannya dengan Android).
  • Bonus: Binder memungkinkan Anda untuk menambahkan objek perantara, misalnya, untuk logging atau debugging perjalanan waktu.

Alih-alih menandatangani secara manual, Anda dapat menulis ulang contoh di atas sebagai berikut:

 val binder = Binder() binder.bind(wishes to feature) binder.bind(feature to logger) 

Berkat Kotlin, semuanya terlihat sangat sederhana.

Contoh-contoh ini berfungsi jika jenis input dan output sama. Tetapi bagaimana jika tidak? Dengan menerapkan fungsi ekstensi, kita dapat membuat transformasi otomatis:

 val output: ObservableSource<A> = TODO() val input: Consumer<B> = TODO() val transformer: (A) -> B = TODO() binder.bind(output to input using transformer) 

Perhatikan sintaks: bunyinya hampir seperti kalimat normal (dan ini adalah alasan lain mengapa saya suka Kotlin). Tetapi Binder tidak hanya digunakan sebagai gula sintaksis - juga bermanfaat bagi kita untuk menyelesaikan masalah dengan siklus hidup.

Buat Binder


Membuat instance terlihat lebih mudah:

 val binder = Binder() 

Tetapi dalam hal ini, Anda harus berhenti berlangganan secara manual, dan Anda harus menelepon binder.dispose() kapan pun diperlukan untuk menghapus langganan. Ada cara lain: menyuntikkan instance siklus hidup ke dalam konstruktor. Seperti ini:

 val binder = Binder(lifecycle) 

Sekarang Anda tidak perlu khawatir tentang langganan - mereka akan dihapus pada akhir siklus hidup. Pada saat yang sama, siklus hidup dapat diulang berkali-kali (seperti siklus mulai dan berhenti di Android UI) - dan Binder akan membuat dan menghapus langganan untuk Anda setiap waktu.

Dan apa itu siklus hidup?


Sebagian besar pengembang Android, melihat frasa "siklus hidup", mewakili siklus Aktivitas dan Fragmen. Ya, Binder dapat bekerja dengan mereka, berhenti berlangganan di akhir siklus.

Tapi ini hanya permulaan, karena Anda tidak menggunakan antarmuka Android LifecycleOwner dengan cara apa pun - Binder memiliki sendiri, lebih universal. Ini pada dasarnya adalah aliran sinyal BEGIN / END:

 interface Lifecycle : ObservableSource<Lifecycle.Event> { enum class Event { BEGIN, END } // Remainder omitted } 

Anda bisa mengimplementasikan aliran ini menggunakan Observable (by mapping), atau cukup menggunakan kelas ManualLifecycle dari perpustakaan untuk lingkungan non-Rx (lihat persis di bawah).

Bagaimana cara kerja binder ? Menerima sinyal BEGIN, itu membuat langganan untuk komponen yang sebelumnya Anda konfigurasi ( input / output ), dan menerima sinyal AKHIR, menghapusnya. Yang paling menarik adalah Anda bisa mulai dari awal lagi:

 val output: PublishSubject<String> = PublishSubject.create() val input: Consumer<String> = Consumer { System.out.println(it) } val lifecycle = ManualLifecycle() val binder = Binder(lifecycle) binder.bind(output to input) output.onNext("1") lifecycle.begin() output.onNext("2") output.onNext("3") lifecycle.end() output.onNext("4") lifecycle.begin() output.onNext("5") output.onNext("6") lifecycle.end() output.onNext("7") // will print: // 2 // 3 // 5 // 6 

Fleksibilitas dalam menetapkan kembali langganan ini sangat berguna ketika bekerja dengan Android, ketika ada beberapa siklus Start-Stop dan Resume-Pause, di samping Buat-Hancurkan yang biasa.

Android Binder Lifecycles


Ada tiga kelas di perpustakaan:

  • BuatDestroyBinderLifecycle ( androidLifecycle )
  • StartStopBinderLifecycle ( androidLifecycle )
  • ResumePauseBinderLifecycl e ( androidLifecycle )

androidLifecycle adalah nilai yang dikembalikan oleh metode getLifecycle() , yaitu, AppCompatActivity , AppCompatDialogFragment , dll. Semuanya sangat sederhana:

 fun createBinderForActivity(activity: AppCompatActivity) = Binder(   CreateDestroyBinderLifecycle(activity.lifecycle) ) 

Siklus hidup individu


Jangan berhenti di situ, karena kita tidak terikat dengan Android dengan cara apa pun. Apa siklus hidup pengikat ? Secara harfiah apa saja: misalnya, waktu pemutaran dialog atau waktu eksekusi beberapa tugas yang tidak sinkron. Anda dapat, misalnya, ikat ke lingkup DI - dan kemudian langganan apa pun akan dihapus bersamanya. Kebebasan penuh untuk bertindak.

  1. Ingin langganan disimpan sebelum Observable mengirim item? Konversi objek ini ke Lifecycle dan berikan ke Binder . Terapkan kode berikut dalam fungsi ekstensi dan gunakan nanti:

     fun Observable<T>.toBinderLifecycle() = Lifecycle.wrap(this   .first()   .map { END }   .startWith(BEGIN) ) 
  2. Ingin menjaga ikatan Anda sampai Completable selesai? Tidak ada masalah - ini dilakukan dengan analogi dengan paragraf sebelumnya:

     fun Completable.toBinderLifecycle() = Lifecycle.wrap(   Observable.concat(       Observable.just(BEGIN),       this.andThen(Observable.just(END))   ) ) 
  3. Ingin beberapa kode non-Rx lainnya memutuskan kapan harus menghapus langganan? Gunakan ManualLifecycle seperti dijelaskan di atas.

Bagaimanapun, Anda dapat meletakkan aliran reaktif ke aliran elemen Siklus Hidup. Bahkan , atau menggunakan ManualLifecycle jika Anda bekerja dengan kode non-Rx.

Tinjauan Umum Sistem


Binder menyembunyikan detail membuat dan mengelola langganan Rx. Yang tersisa adalah gambaran umum singkat dan umum: "Komponen A berinteraksi dengan komponen B dalam lingkup C".

Misalkan kita memiliki komponen reaktif berikut untuk layar saat ini:



Kami ingin komponen yang akan terhubung dalam layar saat ini, dan kami tahu bahwa:

  • UIEvent dapat diumpankan langsung ke AnalyticsTracker ;
  • UIEvent dapat diubah menjadi Wish for Feature ;
  • Negara dapat diubah menjadi Model View untuk Tampilan .

Ini dapat diungkapkan dalam beberapa baris:

 with(binder) {   bind(feature to view using stateToViewModelTransformer)   bind(view to feature using uiEventToWishTransformer)   bind(view to analyticsTracker) } 

Kami membuat meremas tersebut untuk menunjukkan interkoneksi komponen. Dan karena kami pengembang menghabiskan lebih banyak waktu membaca kode daripada menulisnya, ikhtisar singkat seperti itu sangat berguna, terutama ketika jumlah komponen bertambah.

Kesimpulan


Kami melihat bagaimana Binder membantu dalam mengelola langganan Rx dan bagaimana Binder membantu Anda mendapatkan gambaran umum tentang sistem yang dibangun dari komponen reaktif.

Dalam artikel berikut, kami akan menjelaskan bagaimana kami memisahkan komponen UI reaktif dari logika bisnis dan cara menambahkan objek perantara menggunakan Binder (untuk logging dan debugging perjalanan waktu). Jangan beralih!

Sementara itu, periksa perpustakaan di GitHub .

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


All Articles