Buktikan Kode Android Anda di Masa Depan, Bagian 2: Pemrograman Reaktif Fungsional dalam Tindakan
Diterbitkan: 2022-09-08Pemrograman 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:
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 diberikanEvent
. Implementasi yang mendasari meluncurkan coroutine baru untuk setiap acara. -
stateVal(): S
: Mengembalikan status saat ini. -
updateState((S) -> S): S
Memperbarui statusViewModel
.
Sekarang, mari kita lihat beberapa metode yang berhubungan dengan komposisi fungsi:
-
then
: Menyusun dua fungsi bersama-sama. -
asParamTo
: Menghasilkan fungsig() = f(t)
darif(T)
dan nilait
(dari tipeT
). -
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.
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 kodeonEvent: (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.