首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >具有Kt流和改造的UseCases或Interactors

具有Kt流和改造的UseCases或Interactors
EN

Stack Overflow用户
提问于 2021-12-06 14:19:37
回答 1查看 1.6K关注 0票数 3

上下文

我开始做一个新的项目,我决定从RxJava搬到Kotlin。我使用的是MVVM干净的体系结构,这意味着我的ViewModelsUseCases类通信,而这些UseCases类使用一个或多个Repositories从网络中获取数据。

让我给你举个例子。假设我们有一个显示用户配置文件信息的屏幕。所以我们有了UserProfileViewModel

代码语言:javascript
复制
@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用例看起来如下所示:

代码语言:javascript
复制
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存储库如下所示:

代码语言:javascript
复制
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())))
            }
        }
    }
}

最后,用于建模后端API响应的RetrofitApi和响应类如下所示:

代码语言:javascript
复制
data class ApiUserProfileResponse(
    @SerializedName("user_name") val userName: String
    // ...
)

interface RetrofitApi {
    @GET("api/user/profile")
    suspend fun getUserProfileFromApi(): Response<ApiUserProfileResponse>
}

到目前为止,一切都很好,但是在实现更复杂的特性时,我开始遇到一些问题。

例如,有一个用例,当用户第一次登录时,我需要将(1) post发送到POST /send_email_link端点,这个端点将检查我在正文中发送的电子邮件是否已经存在,如果它不存在,它将返回一个404错误代码,如果一切顺利,则返回(2),我应该点击一个POST /peek端点,该端点将返回有关用户帐户的一些信息。

到目前为止,这是我为这个UserAccountVerificationUseCase实现的

代码语言:javascript
复制
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会使事情比使用RxJava更简单,但目前还没有这样的结果。我很确定这是因为我遗漏了一些东西,或者我还没有完全学会如何正确地使用流。

我到目前为止尝试过的

我试图改变从存储库中发出元素的方式,如下所示:

代码语言:javascript
复制
...
    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())))
            }
        }
    }
...

像这样的事情:

代码语言:javascript
复制
...
    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的catch()那样使用onErrorResume()函数

代码语言:javascript
复制
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()中发出项。

即使我能做到这一点,这里仍然有太多的样板代码。我认为使用这样做意味着拥有更简单、更易读的用例。类似于:

代码语言:javascript
复制
...
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)
                    }
                }
            }
        }
    }
}
...

这就是我和科特林合作的想法。放弃RxJava的zip()contact()delayError()onErrorResume()和所有这些Observable函数,转而使用更具可读性的东西。

问题

如何减少样板代码的数量,并使我的用例看起来更像Coroutine?

Notes

我知道有些人只是直接从ViewModel层调用存储库,但是我喜欢在中间设置这个UseCase层,这样我就可以在这里包含与切换流和处理错误相关的所有代码。

任何反馈都是非常感谢的!谢谢!

编辑#1

基于@Joffrey响应,我更改了代码,因此它的工作方式如下:

Retrofit层继续返回可挂起的函数。

代码语言:javascript
复制
data class ApiUserProfileResponse(
    @SerializedName("user_name") val userName: String
    // ...
)

interface RetrofitApi {
    @GET("api/user/profile")
    suspend fun getUserProfileFromApi(): Response<ApiUserProfileResponse>
}

存储库现在返回一个可挂起的函数,我已经删除了Flow包装器:

代码语言:javascript
复制
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的调用:

代码语言:javascript
复制
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

代码语言:javascript
复制
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)
                    }
                }
            }
        }
    }
}

这个看起来干净多了,而且工作得很完美。我仍然在想,这里是否还有改进的余地。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2021-12-06 14:46:33

我在这里看到的最明显的问题是,您将Flow用于单个值,而不是suspend函数。

Coroutines通过使用返回普通值或抛出异常的挂起函数,使单值用例更加简单。当然,您也可以让它们返回Result-like类来封装错误,而不是实际使用异常,但重要的是,使用suspend函数时,您正在公开一个看似同步(因此很方便)的API,同时仍然受益于异步运行时。

在所提供的示例中,您没有订阅任何地方的更新,所有流实际上只提供一个元素并完成,因此没有真正的理由使用流,它使代码复杂化。它还使习惯于协同工作的人更难阅读,因为看起来多个值正在出现,而且collect可能是无限的,但事实并非如此。

每次编写flow { emit(x) }时,都应该是x

按照以上所述,有时使用flatMapMerge,在lambda中使用单个元素创建流。除非您正在寻找计算的并行化,否则只需选择.map { ... }。因此,将其替换为:

代码语言:javascript
复制
val resultingFlow = sourceFlow.flatMapMerge {
    if (something) {
        flow { emit(x) }
    } else {
        flow { emit(y) }
    }
}

在这方面:

代码语言:javascript
复制
val resultingFlow = sourceFlow.map { if (something) x else y }
票数 5
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/70246942

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档