使用了 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)。