Buktikan Kode Android Anda di Masa Depan, Bagian 2: Pemrograman Reaktif Fungsional dalam Tindakan

Diterbitkan: 2022-09-08

Pemrograman reaktif fungsional (FRP) adalah paradigma yang menggabungkan reaktivitas dari pemrograman reaktif dengan komposisi fungsi deklaratif dari pemrograman fungsional. Ini menyederhanakan tugas-tugas kompleks, menciptakan antarmuka pengguna yang elegan, dan mengelola status dengan lancar. Karena ini dan banyak manfaat jelas lainnya, penggunaan FRP menjadi arus utama dalam pengembangan seluler dan web.

Itu tidak berarti memahami paradigma pemrograman ini mudah—bahkan pengembang berpengalaman mungkin bertanya-tanya: “Apa sebenarnya FRP itu?” Di Bagian 1 dari tutorial ini, kami mendefinisikan konsep dasar FRP: pemrograman fungsional dan pemrograman reaktif. Angsuran ini akan mempersiapkan Anda untuk menerapkannya, dengan tinjauan pustaka yang berguna dan contoh implementasi yang mendetail.

Artikel ini ditulis dengan mempertimbangkan pengembang Android, tetapi konsepnya relevan dan bermanfaat bagi pengembang mana pun yang berpengalaman dalam bahasa pemrograman umum.

Memulai dengan FRP: Desain Sistem

Paradigma FRP adalah siklus keadaan dan peristiwa tanpa akhir: State -> Event -> State' -> Event' -> State'' -> … . (Sebagai pengingat, ' , diucapkan “prima”, menunjukkan versi baru dari variabel yang sama.) Setiap program FRP dimulai dengan status awal yang akan diperbarui dengan setiap peristiwa yang diterimanya. Program ini mencakup elemen yang sama dengan yang ada di program reaktif:

  • Negara
  • Peristiwa
  • Pipa deklaratif (ditunjukkan sebagai FRPViewModel function )
  • Dapat diamati (ditunjukkan sebagai StateFlow )

Di sini, kami telah mengganti elemen reaktif umum dengan komponen dan pustaka Android asli:

Dua kotak biru utama, "StateFlow" dan "State", memiliki dua jalur utama di antara keduanya. Yang pertama adalah melalui "Mengamati (mendengarkan perubahan)." Yang kedua adalah melalui "Memberitahu (keadaan terbaru)," ke kotak biru "@Composable (JetpackCompose)," yang masuk melalui "Mengubah input pengguna ke" ke kotak biru "Acara," yang masuk melalui "Pemicu" ke kotak biru " fungsi FRPViewModel," dan akhirnya melalui "Menghasilkan (keadaan baru)." "State" kemudian juga menghubungkan kembali ke "FRPViewModel function" melalui "Bertindak sebagai input untuk."
Siklus pemrograman reaktif fungsional di Android.

Menjelajahi Perpustakaan dan Alat FRP

Ada berbagai pustaka dan alat Android yang dapat membantu Anda memulai FRP, dan itu juga relevan dengan pemrograman fungsional:

  • Ivy FRP : Ini adalah perpustakaan yang saya tulis yang akan digunakan untuk tujuan pendidikan dalam tutorial ini. Ini dimaksudkan sebagai titik awal untuk pendekatan Anda terhadap FRP tetapi tidak dimaksudkan untuk penggunaan produksi apa adanya karena tidak memiliki dukungan yang tepat. (Saat ini saya satu-satunya insinyur yang memeliharanya.)
  • Arrow : Ini adalah salah satu library Kotlin terbaik dan terpopuler untuk FP, yang juga akan kami gunakan di aplikasi sampel kami. Ini menyediakan hampir semua yang Anda butuhkan untuk berfungsi di Kotlin sambil tetap relatif ringan.
  • Jetpack Compose : Ini adalah toolkit pengembangan Android saat ini untuk membangun UI asli dan merupakan perpustakaan ketiga yang akan kita gunakan hari ini. Ini penting untuk pengembang Android modern—saya sarankan untuk mempelajarinya dan bahkan memigrasikan UI Anda jika Anda belum melakukannya.
  • Alur : Ini adalah API datastream reaktif asinkron Kotlin; meskipun kami tidak menggunakannya dalam tutorial ini, ini kompatibel dengan banyak perpustakaan Android umum seperti RoomDB, Retrofit, dan Jetpack. Flow bekerja mulus dengan coroutine dan memberikan reaktivitas. Saat digunakan dengan RoomDB, misalnya, Flow memastikan bahwa aplikasi Anda akan selalu bekerja dengan data terbaru. Jika terjadi perubahan dalam tabel, aliran yang bergantung pada tabel ini akan segera menerima nilai baru.
  • Kotest : Platform pengujian ini menawarkan dukungan pengujian berbasis properti yang relevan dengan kode domain FP murni.

Menerapkan Contoh Aplikasi Konversi Kaki/Meter

Mari kita lihat contoh FRP bekerja di aplikasi Android. Kami akan membuat aplikasi sederhana yang mengonversi nilai antara meter (m) dan kaki (ft).

Untuk keperluan tutorial ini, saya hanya membahas bagian-bagian kode yang penting untuk memahami FRP, dimodifikasi demi kesederhanaan dari aplikasi sampel konverter lengkap saya. Jika Anda ingin mengikuti di Android Studio, buat proyek Anda dengan aktivitas Jetpack Compose, dan instal Arrow dan Ivy FRP. Anda memerlukan versi minSdk 28 atau lebih tinggi dan versi bahasa Kotlin 1.6+.

Negara

Mari kita mulai dengan mendefinisikan status aplikasi kita.

 // ConvState.kt enum class ConvType { METERS_TO_FEET, FEET_TO_METERS } data class ConvState( val conversion: ConvType, val value: Float, val result: Option<String> )

Kelas negara bagian kami cukup jelas:

  • conversion : Jenis yang menjelaskan apa yang kita konversi—kaki ke meter atau meter ke kaki.
  • value : Float yang dimasukkan pengguna, yang akan kita konversi nanti.
  • result : Hasil opsional yang menunjukkan konversi yang berhasil.

Selanjutnya, kita perlu menangani input pengguna sebagai sebuah event.

Peristiwa

Kami mendefinisikan ConvEvent sebagai kelas tersegel untuk mewakili input pengguna:

 // ConvEvent.kt sealed class ConvEvent { data class SetConversionType(val conversion: ConvType) : ConvEvent() data class SetValue(val value: Float) : ConvEvent() object Convert : ConvEvent() }

Mari kita periksa tujuan anggotanya:

  • SetConversionType : Memilih apakah kita mengonversi dari kaki ke meter atau dari meter ke kaki.
  • SetValue : Menetapkan nilai numerik, yang akan digunakan untuk konversi.
  • Convert : Melakukan konversi nilai yang dimasukkan menggunakan jenis konversi.

Sekarang, kita akan melanjutkan dengan model tampilan kita.

Pipeline Deklaratif: Event Handler dan Komposisi Fungsi

Model tampilan berisi pengendali acara dan kode komposisi fungsi (pipa deklaratif):

 // ConverterViewModel.kt @HiltViewModel class ConverterViewModel @Inject constructor() : FRPViewModel<ConvState, ConvEvent>() { companion object { const val METERS_FEET_CONST = 3.28084f } // set initial state override val _state: MutableStateFlow<ConvState> = MutableStateFlow( ConvState( conversion = ConvType.METERS_TO_FEET, value = 1f, result = None ) ) override suspend fun handleEvent(event: ConvEvent): suspend () -> ConvState = when (event) { is ConvEvent.SetConversionType -> event asParamTo ::setConversion then ::convert is ConvEvent.SetValue -> event asParamTo ::setValue is ConvEvent.Convert -> stateVal() asParamTo ::convert } // ... }

Sebelum menganalisis implementasinya, mari kita uraikan beberapa objek khusus untuk library Ivy FRP.

FRPViewModel<S,E> adalah basis model tampilan abstrak yang mengimplementasikan arsitektur FRP. Dalam kode kami, kami perlu menerapkan metode berikut:

  • val _state : Mendefinisikan nilai awal status (Ivy FRP menggunakan Flow sebagai aliran data reaktif).
  • handleEvent(Event): suspend () -> S : Menghasilkan status berikutnya secara asinkron diberikan Event . Implementasi yang mendasari meluncurkan coroutine baru untuk setiap acara.
  • stateVal(): S : Mengembalikan status saat ini.
  • updateState((S) -> S): S Memperbarui status ViewModel .

Sekarang, mari kita lihat beberapa metode yang berhubungan dengan komposisi fungsi:

  • then : Menyusun dua fungsi bersama-sama.
  • asParamTo : Menghasilkan fungsi g() = f(t) dari f(T) dan nilai t (dari tipe T ).
  • thenInvokeAfter : Menyusun dua fungsi dan kemudian memanggilnya.

updateState dan thenInvokeAfter adalah metode pembantu yang ditampilkan dalam cuplikan kode berikutnya; mereka akan digunakan dalam kode model tampilan kami yang tersisa.

Pipa Deklaratif: Implementasi Fungsi Tambahan

Model tampilan kami juga berisi implementasi fungsi untuk menetapkan jenis dan nilai konversi kami, melakukan konversi aktual, dan memformat hasil akhir kami:

 // ConverterViewModel.kt @HiltViewModel class ConverterViewModel @Inject constructor() : FRPViewModel<ConvState, ConvEvent>() { // ... private suspend fun setConversion(event: ConvEvent.SetConversionType) = updateState { it.copy(conversion = event.conversion) } private suspend fun setValue(event: ConvEvent.SetValue) = updateState { it.copy(value = event.value) } private suspend fun convert( state: ConvState ) = state.value asParamTo when (stateVal().conversion) { ConvType.METERS_TO_FEET -> ::convertMetersToFeet ConvType.FEET_TO_METERS -> ::convertFeetToMeters } then ::formatResult thenInvokeAfter { result -> updateState { it.copy(result = Some(result)) } } private fun convertMetersToFeet(meters: Float): Float = meters * METERS_FEET_CONST private fun convertFeetToMeters(ft: Float): Float = ft / METERS_FEET_CONST private fun formatResult(result: Float): String = DecimalFormat("###,###.##").format(result) }

Dengan pemahaman tentang fungsi pembantu Ivy FRP kami, kami siap untuk menganalisis kode. Mari kita mulai dengan fungsionalitas inti: convert . convert menerima status ( ConvState ) sebagai input dan menghasilkan fungsi yang mengeluarkan status baru yang berisi hasil dari input yang dikonversi. Dalam pseudocode, kita dapat meringkasnya sebagai: State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option<String>) .

Penanganan acara Event.SetValue sangat mudah; itu hanya memperbarui status dengan nilai dari acara (yaitu, pengguna memasukkan angka yang akan dikonversi). Namun, menangani event Event.SetConversionType sedikit lebih menarik karena melakukan dua hal:

  • Memperbarui status dengan jenis konversi yang dipilih ( ConvType ).
  • Menggunakan convert untuk mengonversi nilai saat ini berdasarkan jenis konversi yang dipilih.

Menggunakan kekuatan komposisi, kita dapat menggunakan fungsi convert: State -> State sebagai input untuk komposisi lain. Anda mungkin telah memperhatikan bahwa kode yang ditunjukkan di atas tidak murni: Kami memutasi protected abstract val _state: MutableStateFlow<S> di FRPViewModel , yang mengakibatkan efek samping setiap kali kami menggunakan updateState {} . Kode FP yang sepenuhnya murni untuk Android di Kotlin tidak layak.

Karena menyusun fungsi yang tidak murni dapat menghasilkan hasil yang tidak terduga, pendekatan hibrida adalah yang paling praktis: Gunakan fungsi murni untuk sebagian besar, dan pastikan setiap fungsi tidak murni memiliki efek samping yang terkontrol. Inilah yang telah kami lakukan di atas.

Dapat diamati dan UI

Langkah terakhir kami adalah menentukan UI aplikasi kami dan menghidupkan konverter kami.

Sebuah persegi panjang abu-abu besar dengan empat panah menunjuk ke sana dari kanan. Dari atas ke bawah, panah pertama, berlabel "Tombol", menunjuk ke dua persegi panjang yang lebih kecil: persegi panjang kiri berwarna biru tua dengan teks huruf besar "Meter ke kaki" dan persegi panjang kanan berwarna biru muda dengan teks "Kaki ke meter." Panah kedua, berlabel "TextField," menunjuk ke persegi panjang putih dengan teks rata kiri, "100.0." Panah ketiga, berlabel "Tombol," menunjuk ke persegi panjang hijau rata kiri dengan teks "Konversi." Panah terakhir, berlabel "Teks," menunjuk ke teks biru rata kiri yang berbunyi: "Hasil: 328.08 kaki."
Sebuah mock-up dari UI aplikasi.

UI aplikasi kita akan sedikit “jelek”, tetapi tujuan dari contoh ini adalah untuk mendemonstrasikan FRP, bukan untuk membangun desain yang indah menggunakan Jetpack Compose.

 // ConverterScreen.kt @Composable fun BoxWithConstraintsScope.ConverterScreen(screen: ConverterScreen) { FRP<ConvState, ConvEvent, ConverterViewModel> { state, onEvent -> UI(state, onEvent) } }

Kode UI kami menggunakan prinsip dasar Jetpack Compose dalam baris kode sesedikit mungkin. Namun, ada satu fungsi menarik yang perlu disebutkan: FRP<ConvState, ConvEvent, ConverterViewModel> . FRP adalah fungsi yang dapat dikomposisi dari kerangka kerja Ivy FRP, yang melakukan beberapa hal:

  • Membuat instance model tampilan menggunakan @HiltViewModel .
  • Mengamati Status model tampilan menggunakan State .
  • Menyebarkan acara ke ViewModel dengan kode onEvent: (Event) -> Unit) .
  • Menyediakan fungsi tingkat tinggi @Composable yang melakukan propagasi peristiwa dan menerima status terbaru.
  • Opsional menyediakan cara untuk lulus initialEvent , yang dipanggil setelah aplikasi dimulai.

Berikut cara fungsi FRP diimplementasikan di library Ivy FRP:

 @Composable inline fun <S, E, reified VM : FRPViewModel<S, E>> BoxWithConstraintsScope.FRP( initialEvent: E? = null, UI: @Composable BoxWithConstraintsScope.( state: S, onEvent: (E) -> Unit ) -> Unit ) { val viewModel: VM = viewModel() val state by viewModel.state().collectAsState() if (initialEvent != null) { onScreenStart { viewModel.onEvent(initialEvent) } } UI(state, viewModel::onEvent) }

Anda dapat menemukan kode lengkap dari contoh konverter di GitHub, dan seluruh kode UI dapat ditemukan di fungsi UI dari file ConverterScreen.kt . Jika Anda ingin bereksperimen dengan aplikasi atau kodenya, Anda dapat mengkloning repositori Ivy FRP dan menjalankan aplikasi sample di Android Studio. Emulator Anda mungkin memerlukan penyimpanan yang lebih besar sebelum aplikasi dapat berjalan.

Arsitektur Android Lebih Bersih Dengan FRP

Dengan pemahaman dasar yang kuat tentang pemrograman fungsional, pemrograman reaktif, dan, terakhir, pemrograman reaktif fungsional, Anda siap untuk menuai manfaat dari FRP dan membangun arsitektur Android yang lebih bersih dan lebih dapat dipelihara.

Blog Toptal Engineering mengucapkan terima kasih kepada Tarun Goyal karena telah meninjau contoh kode yang disajikan dalam artikel ini.