誤入Android開發-Day5:RxJava/RxKotlin

影山小麥機
13 min readOct 29, 2024

事件流的543

Rx

前言

這幾天在宜蘭玩的同時,想了一下接下來要寫什麼樣的主題來深入Android這個Domain,不過工作上應該是會用到事件流,我想我就盡量讓自己的文章與Demo接近我自己接下來的這份工作會用到的技術,前兩份開發工作帶給我的工作啟發,就是每天的工作的加速源於你的思考、工作習慣還有對原理的了解程度,大多數的Senior能比Junior在處理問題上更快,是因為他們累積了大量的Know How。

接下來會來用RxJava簡單的接一下惠來社區通在ViewModel上的使用,不過,這邊會大方向的帶一下RxJava的用法,畢竟過去我曾經簡單的探討過RxSwift的作法,這邊就不會過度針對事件流的基本原理做描述。

所以本篇文章主要內容有三:

  1. RxJava/RxKotlin Basic
  2. 建構一個ViewModel
  3. 結合LoginScreen 串接 ViewModel

正文

何謂Rx,為什麼需要Rx?

想來想去,還是簡單談談Rx對於開發上的特色,大部分已經入門,或是已經開發過一陣子Mobile的開發者,我相信都知道所謂的OOP,也就是物件導向(Object-oriented programming),這個概念基本上就是基於class的操作,可以想像class就是一個被定義好的物品,我們對它做各式各樣的使用。

但在開發的過程其實會有很多不一樣的需求,也就導致了開發階段會有許多的方法需要處理,這時候程式語言就會不只是OOP了,而是更基於OOP與語法的建構,而延伸出不一樣的設計模式,設計模式的介入,也就讓開發者們針對現有OOP設計方式去要怎麼讓程式碼更加的簡潔。

FRP(Functional Reactive Programming)的誕生,就是要讓開發的過程更加的簡要,但也可以說是複雜跟可能概念性更高一點。所以,基於迭代器模式(Iterator Pattern)、觀察者模式(Observer Pattern)以及函數式編程(Functional Programming)三者的建構就變成了我們今日看到的Rx,晚點會再多講講關於這三者的特性。

那Rx系列與一般的OOP主要差異在哪裡呢?

我想下面匯集幾點以及針對程式碼的特徵作為比較:

  1. 數據流的處理
  • OOP: 一般使用迴圈和條件語句來處理數據。例如,遍歷一個集合並對每個元素進行處理。
  • Rx: 使用 Observable 來處理數據流,數據以流的形式被處理和傳遞。操作符(如 mapfilter 等)用於轉換和處理數據。
// OOP 風格
val list = listOf(1, 2, 3, 4, 5)
for (item in list) {
if (item % 2 == 0) {
println(item * 2)
}
}

// Rx 風格
Observable.fromIterable(list)
.filter { it % 2 == 0 }
.map { it * 2 }
.subscribe { println(it) }

2.異步編程

  • OOP: 通常使用線程、回調和未來(Future)來處理異步操作。
  • Rx: 提供了簡單而強大的異步編程模型,通過 Schedulers 來控制在哪個線程上執行操作,並且可以輕鬆地在不同的線程之間切換。
// OOP 風格
fun fetchData(callback: (String) -> Unit) {
Thread {
// 模擬網絡請求
Thread.sleep(1000)
callback("Data from network")
}.start()
}

fetchData { data ->
println(data)
}

// Rx 風格
Observable.create<String> { emitter ->
// 模擬網絡請求
Thread.sleep(1000)
emitter.onNext("Data from network")
emitter.onComplete()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { data ->
println(data)
}

3.錯誤處理

  • OOP: 使用 try-catch 塊來處理異常。
  • Rx: 錯誤處理是數據流的一部分,可以使用操作符來處理錯誤,例如 onErrorResumeNextretry 等。
// OOP 風格
fun fetchData(): String {
try {
// 模擬網絡請求
return "Data from network"
} catch (e: Exception) {
return "Error occurred"
}
}

val data = fetchData()
println(data)

// Rx 風格
Observable.create<String> { emitter ->
try {
// 模擬網絡請求
emitter.onNext("Data from network")
emitter.onComplete()
} catch (e: Exception) {
emitter.onError(e)
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ data -> println(data) },
{ error -> println("Error occurred: ${error.message}") }
)

稍微結論一下兩者差異:

  • 宣告式 vs 命令式:Rx 更加宣告式,你描述的是「做什麼」,而不是「怎麼做」。OOP 更加命令式,你需要明確地指示每一步該怎麼操作。
  • 異步處理:Rx 提供了更為簡單且一致的方式來處理異步數據流,而不用管理繁瑣的線程和回調。
  • 可組合性:Rx 的操作符可以很方便地組合和鏈接在一起,使得代碼更加簡潔和易於維護。

那這個時候我們稍微看過了程式碼,我們再回到所謂的觀察者模式、迭代器模式以及函數式編程的意義:

Observer Pattern(觀察者模式):

  • 概念:這是一種一對多的依賴關係,當一個對象的狀態改變時,所有依賴它的對象都會得到通知並自動更新。
  • 在 FRP 中的應用:FRP 系統中的 ObservableStream 就像是觀察者模式中的 "Subject",而訂閱者(Observer)則相當於觀察者模式中的 "Observer"。當數據流發生變化時,訂閱者會被通知並更新。

Iterator Pattern(迭代器模式):

  • 概念:這種模式提供了一種方法,能夠順序訪問一個聚合對象中的各個元素,而不需要暴露其內部表示。
  • 在 FRP 中的應用:在 FRP 中,數據流可以被看作是一個連續的數據序列,類似於迭代器模式中的集合。操作符(如 mapfilter 等)允許我們逐個處理這些元素,就像迭代器允許我們遍歷集合中的每個元素一樣。

Functional Programming(函數式編程):

  • 概念:這是一種編程範式,它將計算視為數學函數的求值,避免使用可變狀態和可變數據。強調純函數、不可變性和高階函數。
  • 在 FRP 中的應用:FRP 深受函數式編程影響,使用純函數來處理數據流,保證不變性和高階函數操作(如 mapflatMapreduce 等),這使得數據流操作更加簡潔和易於推理。

所以,綜合上面所描述的特質,FRP的使用特徵與意義如下:

  • 觀察者模式 提供了一個機制,允許數據流中的變化被觀察和處理。
  • 迭代器模式 提供了一個方法,允許我們順序處理數據流中的每個元素。
  • 函數式編程 提供了一套工具,允許我們使用純函數和不可變數據來處理數據流,從而提高代碼的可測試性和可維護性。

RxJava、RxKolin

那我們接下來講講Android平台開發的Rx,也就是RxJava跟RxKolin,不過目前看起來RxJava在Android開發上也就支援Java與Kotlin。

那Rx在Android上要怎麼使用呢?這編會理所當然的先從數據流開始說起,不過這邊就不畫圖了。

數據流

數據流分成五個種類:

1. Flowable

類型io.reactivex.rxjava3.core.Flowable

特性

  • 流的範圍:0 到 N 個元素。
  • 反壓支持:支持背壓(Backpressure),用於處理生產者速度超過消費者速度的情況。

用途

  • 當需要處理大量數據並且需要管理背壓時,使用 Flowable
  • 適用於需要處理快速或不均勻數據流的場景,如從網絡或磁盤讀取大量數據。
import io.reactivex.rxjava3.core.Flowable

fun main() {
Flowable.range(1, 100)
.onBackpressureBuffer() // 配置背壓策略
.subscribe { item ->
println(item)
}
}

2. Observable

類型io.reactivex.rxjava3.core.Observable

特性

  • 流的範圍:0 到 N 個元素。
  • 反壓支持:不支持背壓。

用途

  • 當不需要處理大量數據或不需要管理背壓時,使用 Observable
  • 適用於處理簡單的事件流或較小的數據集。
import io.reactivex.rxjava3.core.Observable

fun main() {
Observable.just("Hello", "RxJava", "Observable")
.subscribe { item ->
println(item)
}
}

3. Single

類型io.reactivex.rxjava3.core.Single

特性

  • 流的範圍:恰好 1 個元素或一個錯誤。

用途

  • 當需要確保只發送一個值或一個錯誤時,使用 Single
  • 適用於網絡請求、數據庫查詢等只返回單一結果的操作。
import io.reactivex.rxjava3.core.Single

fun main() {
Single.just("Hello, Single")
.subscribe({ item ->
println(item)
}, { error ->
error.printStackTrace()
})
}

4. Completable

類型io.reactivex.rxjava3.core.Completable

特性

  • 流的範圍:不發送任何元素,只發送完成或錯誤信號。

用途

  • 當需要表示一個沒有返回值的操作(例如寫入文件、刪除數據)的完成或錯誤狀態時,使用 Completable
import io.reactivex.rxjava3.core.Completable

fun main() {
Completable.fromAction {
println("Completable action")
}.subscribe({
println("Completed")
}, { error ->
error.printStackTrace()
})
}

5. Maybe

類型io.reactivex.rxjava3.core.Maybe

特性

  • 流的範圍:不發送任何元素、發送恰好 1 個元素,或者發送一個錯誤。

用途

  • 當操作可能不返回任何結果、可能返回一個結果或可能出錯時,使用 Maybe
  • 適用於數據庫查詢可能找到或找不到記錄的情況。
import io.reactivex.rxjava3.core.Maybe

fun main() {
Maybe.just("Hello, Maybe")
.subscribe({ item ->
println(item)
}, { error ->
error.printStackTrace()
}, {
println("Completed without any item")
})
}

做一個LoginPage的ViewModel

功能:輸入框的輸入文字變化監聽、按下登入鍵的按鈕變化、

先建構一個簡單的ViewModel來看看:

先簡單時做一個登入介面(Interface)

interface UserRepository {
fun login(account: String, password: String): Single<Boolean>
}

接下來把它串接到UI上,大概就會下面這個樣子:

class LoginViewModel(private val userRepository: UserRepository) : ViewModel() {

private val _account = MutableLiveData<String>()
val account: LiveData<String> = _account

private val _password = MutableLiveData<String>()
val password: LiveData<String> = _password

private val _isObscureText = MutableLiveData<Boolean>(true)
val isObscureText: LiveData<Boolean> = _isObscureText

private val _loginResult = MutableLiveData<Result<Boolean>>()
val loginResult: LiveData<Result<Boolean>> = _loginResult

private val disposables = CompositeDisposable()

fun onAccountChange(newAccount: String) {
_account.value = newAccount
}

fun onPasswordChange(newPassword: String) {
_password.value = newPassword
}

fun togglePasswordVisibility() {
_isObscureText.value = !_isObscureText.value!!
}

fun login(account: String, password: String) {
// 使用 RxJava 進行登入操作
val disposable = userRepository.login(account, password)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
_loginResult.value = Result.success(result)
}, { error ->
_loginResult.value = Result.failure(error)
})

disposables.add(disposable)
}

override fun onCleared() {
super.onCleared()
// 清理 RxJava 的訂閱,防止內存洩漏
disposables.clear()
}
}

這邊還沒有做MVVM架構的分離,有機會的話會再多寫些RxJava的用法。

小結

這邊簡單描述了所有關於事件流的基本概念與使用,以及如何將事件流的事件送出給UI並起發生變化,如果是OOP的使用方式,呈現的範例大概會再多出蠻多程式碼,Rx的精髓就是減少程式碼的寫作,讓波動拳消失(笑
這邊送給正在關注Android技術的朋友們!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

影山小麥機
影山小麥機

Written by 影山小麥機

本職為Mobile工程師,熱愛分享視野,也樂意站在ChatGPT的肩膀上。訂閱小麥機,收割技術、職涯、人生的難題。

No responses yet

Write a response