Mengulik Reactive Programming di Android — bagian 1

Bagi pembaca yang seorang front-end engineer, baik web maupun mobile, mungkin sering bertanya-tanya kenapa belakangan ini banyak pihak berkoar-koar tentang Reactive Programming (RP), beserta beberapa nama library seperti Rx.

Dalam profesi saya sebagai product engineer aplikasi Android di Go-Jek, saya mendapat kesempatan mempelajari dan menggunakannya dalam beberapa proyek, dengan bantuan rekan-rekan saya yang sangat suportif seperti salis muhammad ketika saya mengalami kesulitan.

Seperti bahasa pemrograman atau framework baru, belajar RP adalah hal yang sulit. Salah satunya, ketika kita baru mengenal RP, kita langsung disodori berbagi macam jargon baru yang asing. Untuk itu, dalam postingan ini saya akan jabarkan jargon-jargon tersebut secara berurutan dari hal yang paling dasar.

Mari kita mulai dengan teori.

Apa itu Reactive Programming

I read about reactive programming definition all over the internet but all these complicated words gave me headache, geez…

Hal tersulit dari RP sebenarnya adalah jika kita datang dari dunia pemrograman imperatif dan mencoba mengubah pola pikir kita menjadi reactive thinking.

Dari semua referensi yang menjelaskan tentang definisi RP, definisi yang paling memuaskan bagi saya adalah dari André Staltz:

Reactive programming is programming with asynchronous data stream.

Ada dua istilah yang perlu kita telaah lebih lanjut: asynchronous, dan stream.

Istilah asynchronous sebenarnya tidak asing jika anda lama berkutat di dunia programming. Jika anda merasa asing, silahkan membaca postingan saya berikut untuk mengetahui asynchronous di Android.

Asynchronous data adalah data yang tidak dapat diprediksi kapan ia dapat diterima. Contohnya ketika kita melakukan pemanggilan request ke suatu endpoint API. Di dalam pemrograman android, OnClickListener() adalah salah satu contoh asynchronous event, kita tidak dapat memprediksi kapan suatu tombol akan ditekan. Maka kita memberikan sebuah event listener yang akan mendeteksi event ketika sebuah tombol ditekan, misalnya.

Stream adalah sekumpulan data yang dikirim berurutan sesuai waktu. Ia seperti array yang ketambahan dimensi waktu. Jika di array biasa kita dapat mengambil value tiap anggotanya seketika saat array itu dibuat serta memiliki ukuran yang pasti, stream sedikit berbeda. Jika kita membuat sebuah stream, kita dapat mengiriminya value setiap saat sebanyak mungkin sampai kita menutup stream itu. Proses mengirim value ke stream tersebut disebut emitting, sedangkan proses menunggu value dikirim disebut subscribing.

“Reactive Thinking” adalah Tentang Konsep Bahwa Hampir Segala Sesuatu adalah Stream.

Jika diilustrasikan, bayangkan sebuah perosotan di mana anda berdiri di atasnya beserta sekeranjang penuh berisi bola. Kemudian ada anak kecil di bawah dengan keranjang lain yang kosong. Anda ingin agar anak tersebut menerima bola dari atas dari anda, untuk kemudian dimasukkan ke keranjang yang satunya. Anda dapat menggelindingkan bola dari atas setiap saat, sebanyak yang anda mau, kemudian anak yang di bawah akan menunggu sampai bola terebut diterimanya untuk kemudian ia masukkan ke keranjang satunya.

  • Perosotan tersebut adalah sebuah stream, atau kalau di Rx disebut observable, yang berisi bola.
  • Anda di atas sedang melakukan emitting bola ke prosotan tersebut.
  • Sedangkan anak yang menerima bola di bawah adalah observer yang sedang melakukan subscribing terhadap perosotan tersebut untuk bola yang datang.
  • Seluruh proses observer-melakukan-subscribing-terhadap-observable ini disebut subscription

Jika digambarkan dalam bentuk diagram maka kira-kira berbentuk seperti berikut:

Diagram untuk stream bola
  • Lingkaran menggambarkan value (bola) yang di-emit ke dalam stream (perosotan)
  • Anak panah menunjukkan arah urutan stream tersebut. Value-value yang di-emit ke stream tersebut berurutan sesuai waktu dari yang pertama di-emit berada di pangkal anak panah menuju ke ujung anak panah.
  • Jarak antar value tersebut adalah jarak waktu ketika mereka di-emit.
  • Garis yang melintang di dekat ujung anak panah menandakan stream tersebut sudah selesai (closed/completed).

Dalam RP, diagram ini disebut Marble Diagram.

Jika kita anggap bola-bola yang kita kirim adalah sebuah integer, kemudian kita mengirimkannya dengan urutan pertama 4, kemudian ada jeda sekitar beberapa detik, dilanjutkan 6,2,1 lalu terakhir 7 , maka diagramnya akan berbentuk:

Diagram untuk stream integer

Jika kita lihat, value-value yang kita emit tersebut tidak dapat kita prediksi kapan dapat diterima, dengan kata lain asynchronous. Kita berurusan dengan sebuah asynchronous data stream.

Transformasi Data

Hal yang menarik dari stream adalah kita dapat melakukan transformasi terhadap data yang dikirim ke dalam stream ke dalam bentuk yang kita inginkan. Hal ini tidak berhenti di sini, kita kemudian dapat menyambungkan stream tersebut dengan stream lain berikutnya yang dapat melakukan hal yang sama. Dalam hal ini, kita telah melakukan chaining antar stream.

Sebuah contoh kasus, sebuah aplikasi di mana user dapat memasukkan serangkaian angka ke dalam EditText. Kita ingin menampilkan berapa jumlah angka genap dalam serangkaian text tersebut secara langsung ketika user mengetik. Bagaimana kita dapat melakukannya?

Operator akan mempermudah kita. Anggap user tersebut memasukkan rangkaian angka “8394126”. Setiap user tersebut mengetik sebuah angka di EditText, proses itu adalah sebuah event yang dapat di-emit ke dalam stream. Sehingga jika digambarkan dalam diagram :

Diagram stream untuk deteksi angka genap

Fungsi yang ada di dalam kotak kuning disebut operator. Operator akan melakukan transformasi sebuah stream, dengan memasukkan fungsi sebagai sebuah parameter.

  1. map()— Gampangnya map() seperti “for eachloop. Ia akan melakukan loop ke sebuah stream, kemudian untuk setiap value, ia akan melakukan transformasi ke sebuah value yang baru. Operator ini membutuhkan masukan berupa sebuah fungsi yang berisi bagaimana transformasi value itu dilakukan.
  2. filter()— filter() membutuhkan parameter berupa fungsi kriteria. Sama halnya seperti map(), operator filter() akan melakukan loop ke sebuah stream. Bedanya filter() tidak akan mengikutsertakan value yang tidak sesuai fungsi kriteria.
  3. reduce()— reduce() akan mengambil value pertama yang di-emit sebuah stream kemudian mengambil lagi seterusnya sampai value terakhir yang di-emit, kemudian ia akan menghasilkan sebuah value terakhir. Operator reduce() membutuhkan parameter berupa fungsi untuk melakukan akumulasi semua value tadi.

Operator map(), filter(), dan reduce() ini sebenarnya berasal dari dunia functional programming. Anda dapat mempelajarinya lebih lanjut di sini.

Perlu diperhatikan bahwa, operator tidak memodifikasi stream, namun menghasilkan stream baru. Keempat stream di contoh di atas adalah empat stream yang berbeda. Hal ini juga berasal dari dunia functional programming yang disebut immutability.

Kenapa Reactive Programming

Okay, I got reactive programming and stream, but why should i give a damn about it?

Salah satu prinsip saya ketika bekerja dalam proyek software adalah sebelum memutuskan untuk memakai teknologi/framework/library tertentu, saya harus tahu alasan kenapa ia harus dipakai. Keuntungan apa yang akan saya peroleh dengan memakainya? Apa kelebihannya dibandingkan dengan metode lain yang sama-sama menyelesaikan problem yang sama?

Saya skeptis dengan RP ketika pertama kali saya dihadapkan dengan sebuah proyek yang mengimplementasikan library RxJava. Berangkat dari dunia imperative, saya merasa bahwa tanpa RP-pun saya bisa menyelesaiakan sebagian besar permasalahan dalam pembuatan software.

Turns out, i’m right…

…and wrong.

RP adalah salah satu hal yang menyenangkan dilakukan dalam menyelesaikan beberapa problem pembuatan software ketika kita sudah tahu untuk apa ia dibuat.

Reactive Programming Membantu Kita Menyelesaikan Permasalahan Real-Time UX yang Kompleks

Dalam hal ini kita harus menilik sejarah.

Satu dekade yang lalu, interaksi manusia-komputer sebagian besar berupa proses user mengirim data form, kemudian server akan mengolah data tersebut, untuk selanjutnya dikirim kembali ke user.

Namun, jika melihat belakangan ini, kebutuhan manusia akan respon informasi yang cepat dari komputer meningkat. Aplikasi real-time mendominasi. Front-end telah berevolusi menjadi interaksi berat yang harus berurusan dengan banyak data secara real-time:

  • Ketikan di sebuah chatbox akan memicu notifikasi untuk user lain di device-nya,
  • Ketikan tulisan di halaman new story Medium akan memicu save di database,
  • Sebuah halaman dashboard harus melakukan log tracking dari server dan memvisualisasikannya menjadi diagram,
  • dan sebagainya.

Front-end mulai mengenal dimensi baru bernama “waktu” saat mengolah data asynchronous. Kita membutuhkan alat untuk berurusan dengannya secara benar. Saat itulah konsep stream/observable dan RP lahir.

Wait, aren’t there bunch of Callbacks, Listeners, or Event Buses to handle asynchrony in Android? Why don’t we use these instead?

We haven’t used Otto (an event bus library for Android) in a year and a half, if not more…We think we found a better mechanism. That mechanism is…RxJava where we can create a much more specific pipeline of events than a giant generic bus that just shoves any event across it.
— Jake Wharton, in Fragmented podcast, Episode 6, 50:26–51:00

Sebagai contoh saya akan berikan kasus di mana kita ingin membuat aplikasi untuk mendeteksi double tap di Android. Aplikasi tersebut harus menghitung berapa jumlah tap yang dilakukan user ketika ia melakukan double tap atau lebih (triple tap, quadruple tap, dst…).

Pertanyaannya bagaimana kita mendefinisikan “double tap” secara imperative di Android?

Well, solusi secara reactive akan memudahkan kita.

Dalam RP ada empat langkah dasar untuk menyelesaikan masalah :

  1. Buat model dengan stream (observable)
  2. Tentukan bentuk akhir stream
  3. Lakukan transformasi dengan operator bila perlu
  4. Buat observer & lakukan subscribing terhadap stream

Untuk memodelkan permasalahan tadi dengan stream, kita gunakan lagi marble diagram. Kita bisa membuat stream yang meng-emittapuser.

Stream of taps.

Setiap lingkaran tersebut mewakili satu tap event dari user. Dengan model ini, kita bisa mendefinisikan “double tap” sebagai dua buah event tap yang memiliki jarak maksimal 1 detik.

Karena kita ingin menghitung jumlah tap, maka hasil akhir dari stream yang kita butuhkan adalah stream of Integer.

Dengan operator, maka proses transformasi dapat kita gambarkan sebagai berikut:

Operator buffer(x.debounce(1 second)) akan melakukan transformasi stream awal (stream x) ketika ada dua atau lebih value yang di-emit berdekatan (berjarak kurang dari 1 detik), akan menghasilkan stream baru dengan value berupa list dari value-value yang berdekatan tadi.

Operasi berikutnya adalah mengubah stream kedua dari bentuk stream of list of taps, menjadi stream of integer. Integer tersebut berupa size dari list of taps tadi. Kemudian operasi terakhir adalah melakukan operasi filter() untuk value yang bernilai 2 atau lebih.

Implementasi kode dari seluruh kasus ini kira-kira sebagai berikut:

Dan hasilnya kira-kira sebagai berikut:

Multiple taps app

But, why the heck would i create some double tap app in real life? Show me actual implementation!

Mari kita terjun langsung ke dalam masalah di dunia nyata.

Sebuah aplikasi membutuhkan halaman untuk melakukan validasi form registrasi dengan ketentuan sebagai berikut:

  • Dalam state (keadaan) default, form memiliki 3 field untuk email, password, dan konfirmasi password, serta sebuah tombol “Submit
  • Dalam state default, tombol “Submit” di-disable.
  • User harus mengisi email yang belum terdaftar, jika sudah terdaftar tampilkan notifikasi peringatan.
  • Pengecekan email hanya dilakukan ketika user mengetikkan lebih dari 3 karakter di field email.
  • User harus mengisi password sepanjang minimal 6 karakter, jika kurang, tampilkan notifikasi peringatan.
  • User harus mengisi konfirmasi password, jika tidak sama dengan password yang sudah diisi, tampilkan notifikasi peringatan.
  • User harus mengisi semua field, jika masih ada yang kosong, tombol “Submit” tidak akan di-enable.
  • Jika semua ketentuan sudah terpenuhi, enable button “Submit”.

Bagaimana kita cara kita menyelesaikan permasalahan ini tanpa RP? Pendekatan yang kita lakukan mungkin seperti ini:

  • Membuat tiga TextWatcher untuk masing-masing field.
  • Membuat flag untuk me-monitor ketiga field tersebut valid atau tidak.
  • Setiap user mengetik email, panggil API kemudian lakukan validasi terhadap email, update flag valid email, dan cek flag valid untuk field yang lain.
  • Setiap user mengetik password, cek validasi, update flag valid password, cek validasi field konfirmasi password, dan cek flag valid untuk field yang lain.
  • Setiap user mengetik konfirmasi password, cek validasi, update flag valid konfirmasi password, dan cek flag valid untuk field yang lain.
  • Jika semua flag bernilai valid, enable tombol “Submit” dan sebaliknya.

Jika dilihat dalam kode, maka kira-kira akan seperti ini:

Implementasi tanpa RP ini mudah dipahami, tapi:

  • Ada tiga TextWatcher yang semuanya memiliki method kosong hanya untuk memenuhi syarat.
  • Menyimpan state untuk valid tidaknya suatu field di beberapa flag yang tersebar di beberapa fungsi asynchronous. Di mana hal ini dapat menyebabkan bug sulit dicari.
  • Pada field email, aplikasi akan melakukan pemanggilan API setiap user mengetik satu huruf. Bukannya menunggu sampai user berhenti mengetik.
  • Kode untuk mengupdate UI dan logic bercampur di banyak tempat.

Ada cara yang lebih baik. Mari kita selesaikan masalah ini dengan RP.

Langkah-langkah yang akan saya jelaskan berikut akan disertai kode dari RxJava, RxAndroid dan RxBinding. Namun jangan khawatir jika anda masih asing. Tujuan kita disini adalah mengetahui bagaimana RP menyelesaikan sebuah masalah.

Dari aturan-aturan untuk aplikasi registrasi kita tadi, mari kita mulai dengan aturan untuk validasi email:

(1) User harus mengisi email yang belum terdaftar, apabila apabila sudah terdaftar tampilkan notifikasi peringatan.

(2) Pengecekan email hanya dilakukan ketika user mengetikkan lebih dari 3 karakter di field email.

Dari aturan di atas kita bisa menyimpulkan kalau kita ingin hasil akhir dari proses ini adalah boolean yang menyatakan apakah email user sudah ada atau belum. Maka kita ingin hasil akhir stream kita nanti adalah stream of boolean.

Sebagai tambahan dalam aturan ke (2) kita juga ingin agar aplikasi tidak melaukan pemanggilan API setiap user mengetik karakter. Kita ingin agar ia hanya melakukannya jika user berhenti mengetik minimal selama 100ms.

Untuk menyelesaikan ini, kita akan membuat dua buah stream. Stream pertama berupa stream of string untuk melakukan trigger kapan saat yang tepat untuk melakukan pemanggilan API. Stream kedua berupa stream of boolean berisi apakah email yang diketik user sudah dipakai.

Kita mulai dengan stream pertama.

Kita bisa memodelkan ketikan user menjadi stream dengan diagram sebagai berikut:

Steam untuk ketikan email “ab@cd.com”

Stream ketikan user tersebut berupa stream of CharSequence, karena itu kita harus mengubahnya menjadi string dulu, hal ini bisa kita lakukan dengan operator map():

Langkah selanjutnya, kita ingin agar pengecekan hanya dilakukan jika user mengetikan lebih dari 3 karakter. Kita bisa menggunakan operator filter(). Ketika user mengetik “ab@”, operator filter() akan menghasilkan stream dengan isi kosong :

Dan ketika user mengetik “ab@cd.com”, karena karakternya lebih dari 6, maka semua string tersebut akan lolos:

Selanjutanya kita ingin agar aplikasi hanya melakukan pemanggilan API ke backend ketika user berhenti minimal 100ms saat mengetik. Kita bisa melakukan chaining operator debounce().

debounce() hanya akan mengembalikan value yang memiliki jarak waktu minimal tertentu dengan value sesudahnya. Jadi jika kita menggambarkan aturan kita tadi dengan diagram:

Dalam diagram tersebut, ketika user mengetik karakter “.” dilanjutkan “c” jarak waktunya tidak ada 100ms sehingga “.” tidak di-return. Sedangkan ketika user mengetik “c” dilanjutkan dengan “0” user telah melakukan jeda lebih dari 100ms, sehingga karakter “c” di-return, begitu juga dengan “o” dan “m”. Aplikasi akan melakukan pemanggilan API sebanyak 3 kali, yaitu ketika user berhenti di karakter “c”, “o” dan “m”.

Dari semua langkah di atas, hasil sementara dari stream ini adalah :

Stream trigger email.

Langkah berikutnya, dari value yang lolos dari semua operator tadi kita jadikan trigger untuk melakukan pemanggilan API di stream kedua.

Dalam hal ini, lagi-lagi RxJava sangat membantu kita. Ia mengijinkan kita untuk membuat data response hasil Retrofit berupa stream, sehingga kita bisa mengolahnya dengan operator. Awesome!

Interface Retrofit untuk API kita bisa dibuat seperti berikut:

Dalam endpoint /emails contoh tersebut, data response-nya berupa list of all emails, jadi kita harus melakukan pengecekan apakah email sudah ada atau belum di client.

Untuk sementara mari kita beranjak sejenak dari stream pertama yang merupakan hasil filter dan untuk melakukan triggering tadi. Kita beralih ke stream kedua, stream hasil pengecekan input email user.

Seperti biasa langkah awal adalah memodelkan stream menjadi diagram:

Stream of list of emails

Bukan seperti bentuk yang kita inginkan untuk melakukan pengecekan input. Kita ingin bentuknya berupa stream of email, bukan list-nya. Operator flatMap() akan menolong kita:

Mengubah stream of list of emails menjadi stream of email

flatMap() — akan melakukan transformasi dari value yang berupa collection (Array, List, dll) menjadi value berupa anggotanya, kemudian menggabungkannya menjadi satu stream.

Langkah terkahir dari stream ini adalah mengecek apakah input user ada di dalam stream ini. Kita dapat melakukannya dengan operator contains() :

contains() — akan me-return true jika suatu value di-emit oleh sebuah stream, dan me-return false jika stream tersebut completed tanpa meng-emit value tersebut.

Bentuk pembuatan stream kedua ini dalam kode kira-kira sebagai berikut:

Langkah terakhir adalah men-trigger checkIfEmailExistFromAPI() setelah membuat stream pertama. Kita tinggal memanggil fungsi tersebut di dalam operator flatMap() dari stream pertama. Sehingga implementasinya dalam kode kira-kira sebagai berikut:

Langkah selanjutnya setelah membuat stream adalah membuat observer.

Ketika stream selesai dan ketika terjadi error, aplikasi cukup mengeluarkan log. Fungsi showEmailExistAlert() akan menampilkan TextView berdasarkan output boolean dari stream yang kita buat:

Langkah terakhir adalah melakukan subcribing ke stream yang telah kita buat.

Dengan ini validasi email selesai. Kita tinggal melanjutkan ke rule aplikasi untuk password, konfirmasi password, dan empty field.

Validasi Email

Validasi password cukup sederhana, kita cukup menggunakan operator map() untuk mengubah input user menjadi boolean. Ketika panjang password kurang dari 6, return true. Untuk observer, kita buat mirip dengan validasi email.

Validasi Password

Untuk validasi konfirmasi password, kita ingin agar trigger terjadi baik ketika user mengetik di field password maupun di field konfirmasi password.

Untuk itu kita membutuhkan dua stream untuk masing-masing field tersebut, kemudian kita gabung dengan operator merge() menjadi satu stream. Masing-masing stream tersebut sebelumnya akan mengapliaksikan operator map() untuk mengubah charSequence menjadi string.

Ilustrasi merge()

Rule berikutnya, kita ingin ketika ada field yang kosong atau ketika salah satu dari validasi email, password, dan konfirmasi password gagal, tombol submit akan di-disable.

Untuk merealisasikan hal tersebut, pertama, kita buat tiga buah stream untuk masing-masing field (email, password, dan konfirmasi password). Kemudian kita aplikasikan operator combineLatest() Operator tersebut akan mengambil value terakhir yang di-emit masing-masing stream, kemudian menggabungkannya menjadi satu stream.

Fungsi yang akan kita pakai dengan operator combineLatest() adalah

emailEmpty || passwordEmpty || passwordConfirmationEmpty

Dengan demikian, stream hasil combineLatest() hanya akan me-return false jika ketiga stream di atas me-return false semua.

Implementasi kodenya kira-kira sebagai berikut:

Terakhir, kita aplikasikan lagi operator combineLatest() untuk stream tadi dengan stream validasi email, password dan konfirmasi password.

Fungsi yang kita pakai untuk operator combineLatest() di atas adalah:

!emailInvalid && !passwordInvalid && !passwordConfirmationInvalid && !emptyFieldExist

Sehingga hanya ketika semua stream meng-emit false, tombol submit akan di-enable.

Hasil implementasi dari stream ini berikut observer-nya kira-kira sebagai berikut:

Implementasi hasil akhir dari seluruh proses ini dapat dilihat di sini.


Solusi dari penggunaan reactive programming ini memang terlihat memusingkan dan memiliki banyak line of code (LOC). Hal ini sebenarnya dikarenakan tidak terdapatnya fitur lambda expression di Java 7. Beberapa developer Android yang expert seperti Dan Lew menggunakan retrolambda dalam solusinya.

Di luar hal tersebut, solusi reactive programming ini menghilangkan kekurangan solusi imperative yang kita pakai sebelumnya.

Source code dari aplikasi di postingan ini dapat dilihat di repositori saya di sini.


Simpulan

Reactive programming memudahkan kita sebagai developer untuk menyelesaikan masalah real-time di ranah UI.

Reactive programming tidak diciptakan untuk menggantikan imperative programming, namun lebih sebagai pelengkap.

Bagi anda yang masih bingung dengan kode-kode implementasi RxJava, di postingan berikutnya akan saya ajak untuk mempelajarinya dari hal dasar.

Terima kasih telah membaca postingan saya kali ini, semoga bermanfaat. Jika anda memiliki pertanyaan seputar postingan ini silahkan ajukan di kolom komentar. Saya dengan senang hati akan menjawab.

Terakhir, jika anda merasa bahwa postingan ini membantu anda, silahkan klik tombol 💚

Happy coding!