用例或与 Kt Flow 和 Retrofit 的交互
Posted
技术标签:
【中文标题】用例或与 Kt Flow 和 Retrofit 的交互【英文标题】:UseCases or Interactors with Kt Flow and Retrofit 【发布时间】:2022-01-11 17:56:52 【问题描述】:上下文
我开始从事一个新项目,并决定从 RxJava 迁移到 Kotlin 协程。我正在使用 MVVM clean 架构,这意味着我的 ViewModels
与 UseCases
类通信,而这些 UseCases
类使用一个或多个 Repositories
从网络获取数据。
让我举个例子。假设我们有一个应该显示用户个人资料信息的屏幕。所以我们有UserProfileViewModel
:
@HiltViewModel
class UserProfileViewModel @Inject constructor(
private val getUserProfileUseCase: GetUserProfileUseCase
) : ViewModel()
sealed class State
data SuccessfullyFetchedUser(
user: ExampleUser
) : State()
// ...
val state = SingleLiveEvent<UserProfileViewModel.State>()
// ...
fun fetchUserProfile()
viewModelScope.launch
// ⚠️ We trigger the use case to fetch the user profile info
getUserProfileUseCase()
.collect
when (it)
is GetUserProfileUseCase.Result.UserProfileFetched ->
state.postValue(State.SuccessfullyFetchedUser(it.user))
is GetUserProfileUseCase.Result.ErrorFetchingUserProfile ->
// ...
GetUserProfileUseCase
用例如下所示:
interface GetUserProfileUseCase
sealed class Result
object ErrorFetchingUserProfile : Result()
data class UserProfileFetched(
val user: ExampleUser
) : Result()
suspend operator fun invoke(email: String): Flow<Result>
class GetUserProfileUseCaseImpl(
private val userRepository: UserRepository
) : GetUserProfileUseCase
override suspend fun invoke(email: String): Flow<GetUserProfileUseCase.Result>
// ⚠️ Hit the repository to fetch the info. Notice that if we have more
// complex scenarios, we might require zipping repository calls together, or
// flatmap responses.
return userRepository.getUserProfile().flatMapMerge
when (it)
is ResultData.Success ->
flow emit(GetUserProfileUseCase.Result.UserProfileFetched(it.data.toUserExampleModel()))
is ResultData.Error ->
flow emit(GetUserProfileUseCase.Result.ErrorFetchingUserProfile)
UserRepository
存储库如下所示:
interface UserRepository
fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>>
class UserRepositoryImpl(
private val retrofitApi: RetrofitApi
) : UserRepository
override fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>>
return flow
val response = retrofitApi.getUserProfileFromApi()
if (response.isSuccessful)
emit(ResultData.Success(response.body()!!))
else
emit(ResultData.Error(RetrofitNetworkError(response.code())))
最后,RetrofitApi
和用于对后端 API 响应建模的响应类如下所示:
data class ApiUserProfileResponse(
@SerializedName("user_name") val userName: String
// ...
)
interface RetrofitApi
@GET("api/user/profile")
suspend fun getUserProfileFromApi(): Response<ApiUserProfileResponse>
到目前为止一切正常,但是在实现更复杂的功能时我开始遇到一些问题。
例如,有一个用例,当用户首次登录时,我需要 (1) 发布到 POST /send_email_link
端点,该端点将检查我发送的电子邮件是否在body 已经存在,如果不存在,它将返回 404
错误代码,如果一切正常,(2) 应该点击 POST /peek
端点,该端点将返回一些有关用户帐户的信息。
这是我到目前为止为UserAccountVerificationUseCase
实现的:
interface UserAccountVerificationUseCase
sealed class Result
object ErrorVerifyingUserEmail : Result()
object ErrorEmailDoesNotExist : Result()
data class UserEmailVerifiedSuccessfully(
val canSignIn: Boolean
) : Result()
suspend operator fun invoke(email: String): Flow<Result>
class UserAccountVerificationUseCaseImpl(
private val userRepository: UserRepository
) : UserAccountVerificationUseCase
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result>
return userRepository.postSendEmailLink().flatMapMerge
when (it)
is ResultData.Success ->
userRepository.postPeek().flatMapMerge
when (it)
is ResultData.Success ->
val canSignIn = it.data?.userName == "Something"
flow emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully(canSignIn))
else
flow emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
is ResultData.Error ->
if (it.exception is RetrofitNetworkError)
if (it.exception.errorCode == 404)
flow emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist)
else
flow emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
else
flow emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
问题
上述解决方案按预期工作,如果对 POST /send_email_link
的第一次 API 调用返回 404,则用例将按预期运行并返回 ErrorEmailDoesNotExist
响应,因此 ViewModel
可以将其传递回UI 并显示预期的 UX。
如您所见,问题是这个解决方案需要大量样板代码,我认为使用 Kotlin Coroutines 会比使用 RxJava 更简单,但事实并非如此.我很确定这是因为我遗漏了一些东西,或者我还没有完全学会如何正确使用 Flow。
到目前为止我所做的尝试
我试图改变我从存储库中发出元素的方式,来自:
...
override fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>>
return flow
val response = retrofitApi.getUserProfileFromApi()
if (response.isSuccessful)
emit(ResultData.Success(response.body()!!))
else
emit(ResultData.Error(RetrofitNetworkError(response.code())))
...
这样的:
...
override fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>>
return flow
val response = retrofitApi.getUserProfileFromApi()
if (response.isSuccessful)
emit(ResultData.Success(response.body()!!))
else
error(RetrofitNetworkError(response.code()))
..
所以我可以像使用 RxJava 的 onErrorResume()
一样使用 catch()
函数:
class UserAccountVerificationUseCaseImpl(
private val userRepository: UserRepository
) : UserAccountVerificationUseCase
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result>
return userRepository.postSendEmailLink()
.catch e ->
if (e is RetrofitNetworkError)
if (e.errorCode == 404)
flow emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist)
else
flow emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
else
flow emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
.flatMapMerge
userRepository.postPeek().flatMapMerge
when (it)
is ResultData.Success ->
val canSignIn = it.data?.userName == "Something"
flow emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully(canSignIn))
else ->
flow emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
这确实减少了样板代码,但我无法让它工作,因为一旦我尝试像这样运行用例,我就会开始收到错误消息说我不应在 catch()
中发出项目。
即使我可以让它工作,但这里的样板代码太多了。我虽然用 Kotlin Coroutines 做这样的事情意味着拥有更简单、更易读的用例。比如:
...
class UserAccountVerificationUseCaseImpl(
private val userRepository: AuthRepository
) : UserAccountVerificationUseCase
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result>
return flow
coroutineScope
val sendLinksResponse = userRepository.postSendEmailLink()
if (sendLinksResponse is ResultData.Success)
val peekAccount = userRepository.postPeek()
if (peekAccount is ResultData.Success)
emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully())
else
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
else
if (sendLinksResponse is ResultData.Error)
if (sendLinksResponse.error == 404)
emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist)
else
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
else
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
...
这就是我对使用 Kotlin Coroutines 的想象。放弃 RxJava 的 zip()
、contact()
、delayError()
、onErrorResume()
和所有那些 Observable
函数,以支持更具可读性的东西。
问题
如何减少样板代码的数量并使我的用例看起来更像协程?
备注
我知道有些人只是直接从ViewModel
层调用存储库,但我喜欢将这个UseCase
层放在中间,这样我就可以在此处包含与切换流和处理错误相关的所有代码。
感谢任何反馈!谢谢!
编辑#1
根据 @Joffrey 的回复,我更改了代码,使其工作方式如下:
Retrofit API 层不断返回可挂起函数。
data class ApiUserProfileResponse(
@SerializedName("user_name") val userName: String
// ...
)
interface RetrofitApi
@GET("api/user/profile")
suspend fun getUserProfileFromApi(): Response<ApiUserProfileResponse>
存储库现在返回一个可挂起的函数,我已经删除了 Flow
包装器:
interface UserRepository
suspend fun getUserProfile(): ResultData<ApiUserProfileResponse>
class UserRepositoryImpl(
private val retrofitApi: RetrofitApi
) : UserRepository
override suspend fun getUserProfile(): ResultData<ApiUserProfileResponse>
val response = retrofitApi.getUserProfileFromApi()
return if (response.isSuccessful)
ResultData.Success(response.body()!!)
else
ResultData.Error(RetrofitNetworkError(response.code()))
用例保持返回Flow
,因为我也可能在这里插入对 Room DB 的调用:
interface GetUserProfileUseCase
sealed class Result
object ErrorFetchingUserProfile : Result()
data class UserProfileFetched(
val user: ExampleUser
) : Result()
suspend operator fun invoke(email: String): Flow<Result>
class GetUserProfileUseCaseImpl(
private val userRepository: UserRepository
) : GetUserProfileUseCase
override suspend fun invoke(email: String): Flow<GetUserProfileUseCase.Result>
return flow
val userProfileResponse = userRepository.getUserProfile()
when (userProfileResponse)
is ResultData.Success ->
emit(GetUserProfileUseCase.Result.UserProfileFetched(it.toUserModel()))
is ResultData.Error ->
emit(GetUserProfileUseCase.Result.ErrorFetchingUserProfile)
这看起来更干净。现在,将同样的事情应用到UserAccountVerificationUseCase
:
interface UserAccountVerificationUseCase
sealed class Result
object ErrorVerifyingUserEmail : Result()
object ErrorEmailDoesNotExist : Result()
data class UserEmailVerifiedSuccessfully(
val canSignIn: Boolean
) : Result()
suspend operator fun invoke(email: String): Flow<Result>
class UserAccountVerificationUseCaseImpl(
private val userRepository: UserRepository
) : UserAccountVerificationUseCase
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result>
return flow
val sendEmailLinkResponse = userRepository.postSendEmailLink()
when (sendEmailLinkResponse)
is ResultData.Success ->
val peekResponse = userRepository.postPeek()
when (peekResponse)
is ResultData.Success ->
val canSignIn = peekResponse.data?.userName == "Something"
emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully(canSignIn)
else ->
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
is ResultData.Error ->
if (sendEmailLinkResponse.isNetworkError(404))
emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist)
else
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
这看起来更干净,而且效果很好。我仍然想知道这里是否还有改进的空间。
【问题讨论】:
【参考方案1】:我在这里看到的最明显的问题是您将 Flow
用于单个值而不是 suspend
函数。
协程通过使用返回普通值或抛出异常的挂起函数使单值用例变得更加简单。你当然也可以让它们返回Result
-like 类来封装错误而不是实际使用异常,但重要的部分是使用suspend
函数,你公开了一个看似同步(因此很方便)的 API,同时仍然受益于异步运行时。
在提供的示例中,您没有在任何地方订阅更新,所有流程实际上只提供一个元素并完成,因此没有真正的理由使用流程并且它使代码复杂化。这也使习惯于协程的人更难阅读,因为它看起来像多个值,并且可能 collect
是无限的,但事实并非如此。
每次您写flow emit(x)
时,它应该只是x
。
根据上述情况,您有时会使用flatMapMerge
,并在 lambda 中创建具有单个元素的流。除非您正在寻找计算的并行化,否则您应该直接使用.map ...
。所以替换这个:
val resultingFlow = sourceFlow.flatMapMerge
if (something)
flow emit(x)
else
flow emit(y)
有了这个:
val resultingFlow = sourceFlow.map if (something) x else y
【讨论】:
感谢您的回复!是的,我肯定缺乏一些协程知识,在推进您提出的更改之后,我能够实现一个更清洁的解决方案。请让我知道,如果您看到我可以在这里改进的任何其他内容,我会将其标记为已完成。再次感谢您的帮助! @4gus71n 我在问题代码中发现的唯一另一件事有点令人困惑的是,用例是通过invoke
用作函数的接口。我认为如果将invoke
替换为域中的名称并因此与常规.call()
语法一起使用,会更清楚(至少对我而言)。也就是说,看起来这些用例实际上是在表示函数,但是为什么不使用函数类型呢?我想可能只是我不熟悉这个用例架构:D
谢谢!实际上我曾经有一个execute()
函数而不是invoke()
老实说,我没有任何强有力的论据来覆盖invoke()
运算符,除了它使用例调用看起来像someUseCase()
而不是@987654340 @ 或 someUseCase.execute()
。这是几年前我提出这个想法的SO post。
关于函数类型,我实际上不太确定如何在这种情况下我能够应用它。我的意思是,我习惯于使用函数类型作为 UI 的回调,但我不确定如何在这里应用它们,并通过 Dagger 注入它们。
@4gus71n 依赖注入的好处。老实说,我不知道 dagger 将如何处理函数类型。无论如何,这只是在我身边挑剔以上是关于用例或与 Kt Flow 和 Retrofit 的交互的主要内容,如果未能解决你的问题,请参考以下文章
Android Kotlin Retrofit 与Flow。两个Flow用LiveData来进行分解
Kotlin Flow无缝衔接Retrofit——FlowCallAdapter