1. 程式人生 > >(譯)使用Kotlin和RxJava處理複雜的請求

(譯)使用Kotlin和RxJava處理複雜的請求

logo.png

Android開發中經常會遇到這樣一個問題:服務端Api介面返回的資料和介面上要展示的資料不一致,這就需要你實現更加複雜的請求。可能你的app需要進行多次請求,有些請求還要依賴於前一個請求的結果。這對使用Java開發會是一個挑戰,並且寫出來的程式碼往往可讀性比較差,並且不太容易進行測試。

今天我將通過一個簡單的例子來展示使用RxJava以簡潔地方式解決上述問題。這個例子是使用Kotlin開發的,程式碼更加簡潔、易讀。如果你對RxJava和Kotlin還不熟悉,建議你先補習這些知識,這是一些非常好的學習資料

我們要達成的目標

我們將使用StackOverflow的開放介面來請求一個指定使用者的詳情資訊,包括使用者的前三個問題,前三個被採納的回答以及使用者收藏的前三個問題,最終完成的效果如下所示

1.gif

如果我們要實現這個功能,我們需要進行三次獨立的網路請求。由於這些請求之間互不影響,我們可以併發請求再合併返回的結果,這樣處理效率最高。由於當我們請求回答時返回的結果並沒有回答對應的問題,所以我們還要多請求一次。我們的請求流程大致可以使用下面的圖來表示

2.png

為了簡單起見,我不會解釋整個app的程式碼和架構,我們只實現UserRepository類,用於給presenter提供資料。如果你想檢視完整的程式碼,可以訪問GitHub倉庫

開始寫程式碼

我們首先實現UserRepository類中的getDetail方法,這個方法用來給DetailPresenter提供使用者的詳細資訊,程式碼如下

class UserRepository(private val userService: UserService) {

    fun getUsers(page: Int) = userService.getUsers(page)

    fun getDetails(userId: Long) : Single<DetailsModel> {
        // TODO We will implement this method
        return Single.create { emitter ->
            val detailsModel = DetailsModel
(emptyList(), emptyList(), emptyList()) emitter.onSuccess(detailsModel) } } }

你可能會對Single.create方法的作用存在疑問,當前只是為了讓App能夠編譯成功。create方法建立一個Single物件,用來傳遞一個空的DetailsModel。我們使用Single代替Observable,因為它更適合我們的場景

Single類似於Observable,不同在於Single只返回一個值或者一個錯誤,這正是我們期望一個網路請求所返回的結果。

使用zip操作組合請求結果

首先我們實現對三個請求的組合,我們將使用RxJava提供的zip操作。響應式程式設計文件解釋了關於zip的內容

通過一個指定的函式將多個Observable傳遞的值組合成一個,並將組合後的結果以Observable的形式傳遞下去

這句話該怎麼理解呢?它取每個Observable的值,並傳遞給一個函式,再將函式返回的結果傳遞下去。在我們的例子中更簡單,因為Single只專遞一個值,所以我們只需要定義一個函式將各個獨立的物件轉換成一個

class UserRepository(
        private val userService: UserService) {

    fun getUsers(page: Int) = userService.getUsers(page)

    fun getDetails(userId: Long) : Single<DetailsModel> {
        return Single.zip(
                userService.getQuestionsByUser(userId),
                userService.getAnswersByUser(userId),
                userService.getFavoritesByUser(userId),
                Function3<QuestionListModel, AnswerListModel, QuestionListModel, DetailsModel>
                { questions, answers, favorites ->
                    createDetailsModel(questions, answers, favorites) })
    }

    private fun createDetailsModel(questionsModel: QuestionListModel, answersModel: AnswerListModel,
                                   favoritesModel: QuestionListModel): DetailsModel {
        val questions = questionsModel.items
                .take(3)

        val favorites = favoritesModel.items
                .take(3)

        val answers = answersModel.items
                .filter { it.accepted }
                .take(3)
                .map { AnswerViewModel(it.answerId, it.score, it.accepted, "TODO") }

        return DetailsModel(questions, answers, favorites)
    }
}

程式碼是不是看起來很簡單?zip方法的前三個引數為Retrofit的請求結果,第四個引數為一個lambda表示式,該表示式有接收三個型別同為response的引數。在lambda表示式中我們需要通過一些操作使用傳遞進來的引數建立一個DetailsModel

使用Kotlin集合Api建立DetailsModel

我認為Kotlin的優勢之一就是集合Api,它提供了大量的用來操作集合的方法。我們來看一下在createDetailsModel方法用到的方法:

  • take:這個方法最簡單,它需要一個整型引數n,並返回集合的前n個元素。我們使用它,因為我們只需要前三個元素
  • filter:根據字面意思,這個函式通過我們給定的斷言來過濾元素。它只有一個型別lambda表示式的引數,接收集合中的元素並返回一個Boolean。如果返回true,該元素將被返回。在我們的程式碼中lambda表示式為answer:Answer->answer.accepted。如果lambda表示式只有一個引數,我們可以省略對引數的宣告,直接使用it關鍵字代替
  • map:使用map方法可以對集合進行變換,在這個例子中我們使用map將Answer物件轉換成AnswerViewModel物件。這裡的轉換非常簡單,我們只是通過Answer欄位建立一個對應的AnswerViewModel物件。我們之所以需要map是因為我們從服務端拿到的Answer物件不包含它所屬問題的標題。現在我們先標記為TODO,稍後再處理。

最後值得注意的是,當我們連結這些函式的時候,必須要注意順序.假設我們從服務請求回來的前三個回答沒有被採納,如果我們改變filtertake的順序,我們最終什麼也得不到,同樣如果我們改變takemap的順序,那麼變換操作會被應用到列表的每一個元素,這其實沒有必要.

下面的提交對應上述程式碼的變化

獲取回答對應的問題

如果我們想要展示每個回答對應的問題,我們需要在獲取到回答之後再做一次請求.通常使用RxJava的flatMap操作.關於Single的flatMap方法文件描述如下:

將源Single傳遞的值通過指定函式進行處理,並將處理後的值以SingleSource的形式返回

讓我們通過一個例子來說明:

userService.getAnswersByUser(userId)
        .flatMap { answerListModel: AnswerListModel ->
            questionService.getQuestionById("1234;2345;3456") }

正如你所看到的,flatMap接收一個lambda表示式,表示式以源Single傳遞的值為引數,返回值為一個新的Single,可以傳遞一個和原來不同型別的值.

在上面的例子中我們通過id來請求指定問題,但是這還不夠,我們需要從回答中取得問題id,然後再請求問題,最後通過一些變換來建立一個AnswerViewModel物件列表.下面的程式碼展示瞭如何實現這些

    private fun getAnswers(userId: Long) : Single<List<AnswerViewModel>> {
       return userService.getAnswersByUser(userId)
                .flatMap { answerListModel: AnswerListModel ->
                   mapAnswersToAnswerViewModels(answerListModel.items) }
    }
    private fun mapAnswersToAnswerViewModels(answers: List<Answer>): Single<List<AnswerViewModel>> {
         val ids = answers
                 .map { it.questionId.toString() }
                 .joinToString(separator = ";")

         val questionsListModel = questionService.getQuestionById(ids)

         return questionsListModel
                 .map { questionListModel: QuestionListModel? ->
                     addTitlesToAnswers(answers, questionListModel?.items ?: emptyList()) }
    }
   private fun addTitlesToAnswers(answers: List<Answer>, questions: List<Question>) : List<AnswerViewModel> {
         return answers.map { (answerId, questionId, score, accepted) ->
             val question = questions.find { it.questionId == questionId }
             AnswerViewModel(answerId, score, accepted, question?.title ?: "Unknown")
         }
    }

我們首先關注getAnswers方法和它呼叫的另外兩個方法,其他程式碼和之前的類似.

getAnswer方法看起來和flatMap例子相似,但是在這裡我們沒有執行請求而是呼叫了一個方法,在這個方法裡構造和執行請求.這裡我們再次使用集合的Api將answer列表轉換為以;分隔的ids字串(這個格式是StackOverflow需要的).

這裡出現一個新方法joinToString,我們可以在任何集合中使用它根據集合的元素建立一個字串.

我們有了ids之後就可以請求question了,這部分程式碼可能不太好理解

val questionsListModel = questionService.getQuestionById(ids)

return questionsListModel
        .map { questionListModel: QuestionListModel? ->
            addTitlesToAnswers(answers, questionListModel?.items ?: emptyList()) }

由於使用了map操作,你可能認為questionsListModel是一個集合,其實它是一個Single,在RxJava中Single也可以使用map操作,和用在集合上一樣.它可以將Single傳遞的值進行變換.所以我們可以通過呼叫addTitlesToAnswers方法對它進行變換.

addTitlesToAnswers方法是另一個可以用來證明Kotlinq強大的集合處理能力的例子.我們也使用了Kotlin其他特性.我們沒有使用Answer型別作為lambda表示式的引數而是使用解構宣告,如果你對這個還不熟悉可以參考這裡

需要優化的地方

如果你仔細看最後的程式碼,可能會注意到我們有一個錯誤.錯誤並不嚴重但可能會導致我們的請求變慢.我們為每一個Answer都請求對應的Question,但我們最終只取了前三個Answer.如果我們只請求前三個Question會更好.這點優化在我手機上提高了1秒的效率.關於這點優化的程式碼在這裡

總結

儘管RxJava的學習曲線非常陡峭,但是花一些時間去學習基礎用法,尤其是對Android開發有用的方面是值得的.其中一些我們在上面已經討論過了.通過Kotlin的集合Api融合RxJava可以簡化你的資料流,但如果你仍使用Java,這可能會增加你程式碼庫的負擔.

非常感謝閱讀本文,如果你有什麼問題或者意見可以進行留言.如果你對其他Kotlin例子感興趣,可以下載本專案的程式碼庫

如果感覺本文對你有用不要忘了點選下面的喜歡,這樣能讓更多人看到.