使用了 RxJava 之後,並沒有讓這邊的程式碼變得更複雜。但是,這邊有一件事需要被探討,在 LoginRepository
的 login
有一個 side effect operator - doOnSuccess
。我們允許在這邊有 side effect 嗎?為什麼呢?
在之前的篇幅中,我們提到了在 functional programming 我們不喜歡 side effect ,因為他代表了意外,代表了隱含(implicity)的效果。 Pure function 是應該要被遵循的規則,然而在這裏我們破壞了這個規則,這樣對嗎?但其實,我們可以看待 LoginRepository
這個整體就是一個“容器",一個有狀態的"容器",在 functional 的世界裡不會有 LoginRepository
的實作,他是被排除在外的,實作是在物件導向的世界中,所以這邊就有一個非常重要的結論:在一個專案裡, functional programming 跟 object oriented programming 應該是要可以並存的,但我們要劃清界線,搞清楚什麼類別應該要 functional ,什麼類別應該要 object oriented,而且很多時候寫 object oriented 會比 functional 簡單、直覺得多。
接下來看看 ViewModel 要改成怎樣,ViewModel 有兩個不同的 LiveData ,其中 LoginResult
是用來顯示結果,另一個 LoginFormState
用來即時顯示輸入的回饋,像是帳號空白、密碼長度不足等等。但是剛剛在 LoginRepository
已經做了很多修改,這邊也要做相對應的修正才行。
首先是登入,由於LoginRepository
已經將介面修改為 Single<LoggedInUser>
,原來的版本是 Result<LoggedInUser>
,所以要改成非同步的處理方式:
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
private val _loginResult = MutableLiveData<LoginState>()
val loginResult: LiveData<LoginState> = _loginResult
private val disposables = CompositeDisposable()
fun login(username: String, password: String) {
loginRepository.login(username, password)
.subscribe( { user ->
loginResult.postValue(LoginState.Success(user.displayName))
}, {
loginResult.postValue(LoginState.Failed(R.string.login_failed))
})
.addTo(disposables)
}
}
RxJava 標準的使用方式是 subscribe
來接收結果,另外,由於 login
是一個背景任務,所以要使用 postValue
來更新資料。接下來看看 LoginActivity
是怎麼接收資料的:
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
loginViewModel.loginResult.observe(this@LoginActivity, Observer {
val loginResult = it ?: return@Observer
loading.visibility = View.GONE
if (loginResult.error != null) {
showLoginFailed(loginResult.error)
}
if (loginResult.success != null) {
updateUiWithUser(loginResult.success)
}
})
}
}
看吧!如果是 Product Type 再加上 null 的話,就會有這種這種奇怪的 null check 程式碼,就算我們都知道,不可能 error 跟 success 同時都是 null 或是同時都不是 null 。在這邊的程式碼卻不是我們想像中的那樣,可以用一個簡單的 if 或是 when 來完成,還有我們還發現了一個 loading 的狀態,照理說,這個狀態應該要由 ViewModel 來控制,所以我們再把它加入到 LoginResult
的狀態中,並重新命名為 LoginProgress
:
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
// 簡化成只使用一個 LiveData
val loginProgress: MutableLiveData<LoginProgress> = MutableLiveData<LoginProgress>()
private val disposables = CompositeDisposable()
fun login(username: String, password: String) {
loginProgress.postValue(LoginProgress.Loading)
loginRepository.login(username, password)
.subscribe( { user ->
loginProgress.postValue(LoginProgress.Success(user.displayName))
}, {
loginProgress.postValue(LoginProgress.Failed(R.string.login_failed))
})
.addTo(disposables)
}
}
class LoginActivity : AppCompatActivity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
loginViewModel.loginProgress.observe(this@LoginActivity, Observer {
val loginResult = it ?: return@Observer
when (loginResult) {
is LoginProgress.Success -> {
loading.visibility = View.GONE
updateUiWithUser(loginResult.displayName)
}
is LoginProgress.Failed -> {
loading.visibility = View.GONE
showLoginFailed(loginResult.errorMsg)
}
is LoginProgress.Loading -> {
loading.visibility = View.VISIBLE
}
}
})
修改過後,在 LoginActivity
的 loginProgress.observe
從原本的兩個 null check ,變成了單一的 when ,很好的控制了所有的狀態,不用擔心因為修改程式碼而使得不可能的狀態出現(兩個 null 或是兩個都不是 null)。