接下來處理另一個狀態, LoginFormState
,他也是一個擁有眾多不可能狀態的 Product Type :
data class LoginFormState(val usernameError: Int? = null,
val passwordError: Int? = null,
val isDataValid: Boolean = false)
仔細分析後發現,如果 usernameError 或是 passwordError 不是 null 的話,isDataValid 就會是 false ,usernameError 跟 passwordError 都是 null 的話,isDataValid 就會是 true。其他任何組合都會是奇怪、不應該存在的狀態,這樣實在是太多雜訊了,在閱讀、理解上會浪費很多時間,犯錯的可能性也很高,但其實,他也可以改成簡單的 Sum Type:
enum class LoginUiState {
Valid,
UserNameError,
PasswordError
}
改過之後,上面的 enum 就非常容易理解,這三個狀態都是互斥的,不會有其他不可能的狀態發生,接下來的修改就只剩 LoginViewModel
跟 LoginActivity
了:
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
val loginState: MutableLiveData<LoginUiState> = MutableLiveData<LoginUiState>()
fun loginDataChanged(username: String, password: String) {
if (!isUserNameValid(username)) {
loginState.postValue(LoginUiState.UserNameError)
} else if (!isPasswordValid(password)) {
loginState.postValue(LoginUiState.PasswordError)
} else {
loginState.postValue(LoginUiState.Valid)
}
}
}
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
loginViewModel.loginState.observe(this@LoginActivity, Observer {
val loginState = it ?: return@Observer
when(loginState) {
LoginUiState.Valid -> login.isEnabled = true
LoginUiState.UserNameError -> {
login.isEnabled = false
username.error = getString(R.string.invalid_username)
}
LoginUiState.PasswordError -> {
login.isEnabled = false
password.error = getString(R.string.invalid_password)
}
}
})
}
}
剛剛已經看過 Repository 跟 ViewModel 了,但是其實嚴格來說,這些都不是非常“純”,還是有 side effect ,Repository 有登入狀態,ViewModel 有 LiveData 的狀態。那我們的 pure functional 程式在哪裡呢?其實以目前的需求來說,只要 Domain 是純 functional 就足夠了,而這個 Domain 呢,在這個專案中其實只有兩個 function ,就是 isUserNameValid
跟 isPasswordValid
。
// A placeholder username validation check
val isUserNameValid = { username: String ->
if (username.contains('@')) {
Patterns.EMAIL_ADDRESS.matcher(username).matches()
} else {
username.isNotBlank() && !username.contains(" ")
}
}
// A placeholder password validation check
val isPasswordValid = { password: String ->
password.length > 5
}
這幾篇稍微修改了官方所提供的範例程式碼,運用了之前所說的概念,將一些 error prone 、相對難閱讀的程式碼改成表達能力比較好的程式碼了,還有,functional programming 是可以跟 object oriented programming 是可以並存的,不需要全部的專案程式碼都不能有 side effect 。
最後附上今天的 Repo: