用例或与 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 架构,这意味着我的 ViewModelsUseCases 类通信,而这些 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

Kotlin Flow无缝衔接Retrofit——FlowCallAdapter

UML建模

作业4

unittest