(译)使用Kotlin和RxJava处理复杂的请求

原文地址:https://blog.mindorks.com/how-to-make-complex-requests-simple-with-rxjava-in-kotlin-ccec004c5d10

(译)使用Kotlin和RxJava处理复杂的请求_第1张图片

Android开发中经常会遇到这样一个问题:服务端Api接口返回的数据和界面上要展示的数据不一致,这就需要你实现更加复杂的请求。可能你的app需要进行多次请求,有些请求还要依赖于前一个请求的结果。这对使用Java开发会是一个挑战,并且写出来的代码往往可读性比较差,并且不太容易进行测试。

今天我将通过一个简单的例子来展示使用RxJava以简洁地方式解决上述问题。这个例子是使用Kotlin开发的,代码更加简洁、易读。如果你对RxJava和Kotlin还不熟悉,建议你先补习这些知识,这是一些非常好的学习资料

A Complete Guid To Learn RxJava

A Complete Guid To Learn Kotlin For Android Development

我们要达成的目标

我们将使用StackOverflow的开放接口来请求一个指定用户的详情信息,包括用户的前三个问题,前三个被采纳的回答以及用户收藏的前三个问题,最终完成的效果如下所示

如果我们要实现这个功能,我们需要进行三次独立的网络请求。由于这些请求之间互不影响,我们可以并发请求再合并返回的结果,这样处理效率最高。由于当我们请求回答时返回的结果并没有回答对应的问题,所以我们还要多请求一次。我们的请求流程大致可以使用下面的图来表示

(译)使用Kotlin和RxJava处理复杂的请求_第2张图片

为了简单起见,我不会解释整个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只返回一个值或者一个错误,这正是我们期望一个网络请求所返回的结果。

如果你编译整个项目看一下结果,或者跟着例子自己写代码实现的话可以检出本次提交的代码https://github.com/kozmi55/Kotlin-Examples/commit/388cc185ced48481964202aa63bad252f81c3c9d

使用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 {
        return Single.zip(
                userService.getQuestionsByUser(userId),
                userService.getAnswersByUser(userId),
                userService.getFavoritesByUser(userId),
                Function3
                { 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的顺序,那么变换操作会被应用到列表的每一个元素,这其实没有必要.

下面的提交对应上述代码的变化

https://github.com/kozmi55/Kotlin-Examples/commit/61de3640a2ed2fd8f6c2605be78ba8a76e01fc37

获取回答对应的问题

如果我们想要展示每个回答对应的问题,我们需要在获取到回答之后再做一次请求.通常使用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表达式的参数而是使用解构声明,如果你对这个还不熟悉可以参考这里

现在通过多次请求我们构造出了DetailsModel,你可以下载完整代码通过本次提交
https://github.com/kozmi55/Kotlin-Examples/commit/1f5c587387eeebcebc65ab9b2ffc66190f5490ba

需要优化的地方

如果你仔细看最后的代码,可能会注意到我们有一个错误.错误并不严重但可能会导致我们的请求变慢.我们为每一个Answer都请求对应的Question,但我们最终只取了前三个Answer.如果我们只请求前三个Question会更好.这点优化在我手机上提高了1秒的效率.关于这点优化的代码在这里

https://github.com/kozmi55/Kotlin-Examples/commit/1a6f0ef46d3e7edd1d816081dc1a6e7ab2de7d4a

总结

尽管RxJava的学习曲线非常陡峭,但是花一些时间去学习基础用法,尤其是对Android开发有用的方面是值得的.其中一些我们在上面已经讨论过了.通过Kotlin的集合Api融合RxJava可以简化你的数据流,但如果你仍使用Java,这可能会增加你代码库的负担.

非常感谢阅读本文,如果你有什么问题或者意见可以进行留言.如果你对其他Kotlin例子感兴趣,可以下载本项目的代码库

如果感觉本文对你有用不要忘了点击下面的喜欢,这样能让更多人看到.

你可能感兴趣的:(Android)