iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 27
0
Software Development

Functional Programming in Kotlin系列 第 27

Functional Programming in Practice - Part 1

接下來的三篇,打算要來完成一個登入頁面,使用的技術如下:

  • Android framework
  • Android Architecture component: LiveData and ViewModel
  • RxJava
  • MVVM

以上這些框架以及技術,如果是有 Android 背景的話應該非常熟悉,網路上已經有非常多豐富的資源在介紹他們,也礙於篇幅的關係,就不會詳細解釋他們的用途跟概念了。

登入畫面

在這裡直接使用 Android Studio 提供的範本來進行修改:

https://user-images.githubusercontent.com/7949400/94893237-46e7f680-04b9-11eb-902f-c3a3e4504a66.png

打開專案後會發現裡面已經有基本的 MVVM 架構了,還有一些簡單的登入邏輯判斷,基本的架構圖如下:

https://user-images.githubusercontent.com/7949400/94893675-561b7400-04ba-11eb-8c00-a9a17705970d.png

打開專案結構,其中有兩個不同的資料型別,用來當作資料的回傳值,分別是 LoginResultResult 。在這兩個資料型別中,一個是 Sum Type 、另一個是 Product Type。

Algebraic Data Types

sealed class Result<out T : Any> {

    data class Success<out T : Any>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()

    override fun toString(): String {
        return when (this) {
            is Success<*> -> "Success[data=$data]"
            is Error -> "Error[exception=$exception]"
        }
    }
}

data class LoginResult(
    val success: LoggedInUserView? = null,
    val error: Int? = null
)

根據之前的文章,我們偏好使用 Sum Type 而不是 Product Type ,更不用說 LoginResult 還有 Null 的情況了。所以接下來的任務就是將他修改成 Sum Type :

sealed class LoginState {
    class Success(val displayName: String): LoginState()
    class Failed(val errorMsg: Int): LoginState()
}

接下來看看 Result 用在什麼地方吧,我們發現到在 LoginRepositoryLoginDataSource 都有他的蹤跡:

class LoginDataSource {

    fun login(username: String, password: String): Result<LoggedInUser> {
        try {
            // TODO: handle loggedInUser authentication
            val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe")
            return Result.Success(fakeUser)
        } catch (e: Throwable) {
            return Result.Error(IOException("Error logging in", e))
        }
    }
}

class LoginRepository(val dataSource: LoginDataSource) {
		fun login(username: String, password: String): Result<LoggedInUser> {
		    // handle login
		    val result = dataSource.login(username, password)
		
		    if (result is Result.Success) {
		        setLoggedInUser(result.data)
		    }
		
		    return result
		}
}

Result 在這邊是一個用來包裝登入狀態的 Sum Type,可能是成功或是失敗。但是我們又發現,在這個 Android studio 提供的範本中,使用的是同步的作法,但是一般來說,登入都是一個非同步的操作,所以接下來,引入一個第三方的非同步框架: RxJava

//build.gradle

dependencies {
    ...
    implementation "io.reactivex.rxjava3:rxjava:3.0.6"
    implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
}

在原來的回傳值之上,如果再加入非同步的概念的話,就會是 Single<Result<LoggedInUser>> ,是兩層的“容器”。但是一樣,之前的篇幅有提到過,RxJava 是內建有錯誤處理機制的,所以我們不需要有 Result 這層的容器,使用 Single 本身就可以表達全部的狀態了:

class LoginDataSource {
    // 只有一組假的帳密
    // User: yanbin, Password: 1234
    fun login(username: String, password: String): Single<LoggedInUser> {
        return Single.just(Pair(username, password))
             // delay 3 秒來模擬非同步操作
            .delay(3, TimeUnit.SECONDS)
            .map { (username, password) ->
                when {
                    username == "yanbin" && password == "123456" -> {
                        LoggedInUser(java.util.UUID.randomUUID().toString(), "Yanbin")
                    }
                    else -> throw IllegalArgumentException("User not exist")
                }
            }
    }
}

class LoginRepository(val dataSource: LoginDataSource) {
		fun login(username: String, password: String): Single<LoggedInUser> {
		    return dataSource.login(username, password)
            // side effect operator
		        .doOnSuccess { setLoggedInUser(it) }
		}

		private fun setLoggedInUser(loggedInUser: LoggedInUser) {
        this.user = loggedInUser
    }
}

上一篇
所以 Monad 到底哪裡好用了?
下一篇
Functional Programming in Practice - Part 2
系列文
Functional Programming in Kotlin30

尚未有邦友留言

立即登入留言