原文地址:https://blog.mindorks.com/how-to-make-complex-requests-simple-with-rxjava-in-kotlin-ccec004c5d10
Android开发中经常会遇到这样一个问题:服务端Api接口返回的数据和界面上要展示的数据不一致,这就需要你实现更加复杂的请求。可能你的app需要进行多次请求,有些请求还要依赖于前一个请求的结果。这对使用Java开发会是一个挑战,并且写出来的代码往往可读性比较差,并且不太容易进行测试。
今天我将通过一个简单的例子来展示使用RxJava以简洁地方式解决上述问题。这个例子是使用Kotlin开发的,代码更加简洁、易读。如果你对RxJava和Kotlin还不熟悉,建议你先补习这些知识,这是一些非常好的学习资料
A Complete Guid To Learn RxJava
A Complete Guid To Learn Kotlin For Android Development
我们将使用StackOverflow的开放接口来请求一个指定用户的详情信息,包括用户的前三个问题,前三个被采纳的回答以及用户收藏的前三个问题,最终完成的效果如下所示
如果我们要实现这个功能,我们需要进行三次独立的网络请求。由于这些请求之间互不影响,我们可以并发请求再合并返回的结果,这样处理效率最高。由于当我们请求回答时返回的结果并没有回答对应的问题,所以我们还要多请求一次。我们的请求流程大致可以使用下面的图来表示
为了简单起见,我不会解释整个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
首先我们实现对三个请求的组合,我们将使用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,它提供了大量的用来操作集合的方法。我们来看一下在createDetailsModel
方法用到的方法:
Boolean
。如果返回true,该元素将被返回。在我们的代码中lambda表达式为answer:Answer->answer.accepted
。如果lambda表达式只有一个参数,我们可以省略对参数的声明,直接使用it
关键字代替Answer
对象转换成AnswerViewModel
对象。这里的转换非常简单,我们只是通过Answer
字段创建一个对应的AnswerViewModel
对象。我们之所以需要map是因为我们从服务端拿到的Answer
对象不包含它所属问题的标题。现在我们先标记为TODO
,稍后再处理。最后值得注意的是,当我们链接这些函数的时候,必须要注意顺序.假设我们从服务请求回来的前三个回答没有被采纳,如果我们改变filter
和take
的顺序,我们最终什么也得不到,同样如果我们改变take
和map
的顺序,那么变换操作会被应用到列表的每一个元素,这其实没有必要.
下面的提交对应上述代码的变化
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例子感兴趣,可以下载本项目的代码库
如果感觉本文对你有用不要忘了点击下面的喜欢,这样能让更多人看到.