gpt4 book ai didi

android - 与 Kt Flow 和 Retrofit 的用例或交互

转载 作者:行者123 更新时间:2023-12-05 00:14:41 25 4
gpt4 key购买 nike

上下文
我开始从事一个新项目,并决定从 RxJava 迁移到 Kotlin Coroutines。我正在使用 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())))
}
}
}
}
最后, 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用户首次登录时的端点,该端点将检查我在正文中发送的电子邮件是否已经存在,如果不存在,它将返回 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()))
}
}
}
..
所以我可以使用 catch()像我使用 RxJava 的 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() 中发出项目。 .
即使我可以让这个工作,仍然有太多的样板代码。我虽然用 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
基于 @乔弗里响应,我已经更改了代码,所以它的工作方式如下:
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)
}
}
}
}
}
}
这看起来更干净,而且效果很好。我仍然想知道这里是否还有改进的空间。

最佳答案

我在这里看到的最明显的问题是您正在使用 Flow对于单个值而不是 suspend功能。
协程通过使用返回普通值或抛出异常的挂起函数使单值用例变得更加简单。你当然也可以让他们返回 Result类来封装错误而不是实际使用异常,但重要的部分是使用 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 }

关于android - 与 Kt Flow 和 Retrofit 的用例或交互,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70246942/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com