上下文
我开始做一个新的项目,我决定从RxJava搬到Kotlin。我使用的是MVVM干净的体系结构,这意味着我的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())))
}
}
}
}最后,用于建模后端API响应的RetrofitApi和响应类如下所示:
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实现的
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更简单,但目前还没有这样的结果。我很确定这是因为我遗漏了一些东西,或者我还没有完全学会如何正确地使用流。
我到目前为止尝试过的
我试图改变从存储库中发出元素的方式,如下所示:
...
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的catch()那样使用onErrorResume()函数
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()中发出项。
即使我能做到这一点,这里仍然有太多的样板代码。我认为使用这样做意味着拥有更简单、更易读的用例。类似于:
...
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层继续返回可挂起的函数。
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)
}
}
}
}
}
}这个看起来干净多了,而且工作得很完美。我仍然在想,这里是否还有改进的余地。
发布于 2021-12-06 14:46:33
我在这里看到的最明显的问题是,您将Flow用于单个值,而不是suspend函数。
Coroutines通过使用返回普通值或抛出异常的挂起函数,使单值用例更加简单。当然,您也可以让它们返回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 }https://stackoverflow.com/questions/70246942
复制相似问题