stateIn 无法收集 Result.Success 状态

stateIn not able to collect the Result.Success state

提问人:Jay 提问时间:11/15/2023 最后编辑:Jay 更新时间:11/15/2023 访问量:38

问:

我是 Kotlin Flow 和 StateFlow 的绝对新手。我无法弄清楚为什么对于其中一个 REST API,即使 API 成功,Flow 也不会发出 Success 结果(我可以看到 HTTP 日志记录拦截器打印带有有效响应正文的 200 状态代码)。

我的应用程序有 2 个 REST API,这些 API 正在被调用:ServiceViewModel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.domain.usecase.GetServiceTypesUseCase
import com.example.domain.usecase.GetServicesUseCase
import com.example.ui.util.Result
import com.example.ui.util.asResult
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

class ServiceViewModel(
    private val getServicesUseCase: GetServicesUseCase,
    private val getServiceTypesUseCase: GetServiceTypesUseCase,
) : ViewModel() {

    val serviceTypeUiState = getServiceTypesUseCase()
        .asResult()
        .map { it: Result<List<FilterableServiceType>> ->
            // This never receives the Success emission from asResult() call; only Loading
            println("Result state is : $it")
            when (it) {
                is Result.Error -> ServiceTypesUiState.Error(it.exception)
                is Result.Loading -> ServiceTypesUiState.Loading
                is Result.Success -> ServiceTypesUiState.Success(it.data)
            }
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = Result.Loading
        )

    val servicesUiState = getServicesUseCase()
        .asResult()
        .map {
            when (it) {
                is Result.Error -> ServicesUiState.Error(it.exception)
                is Result.Loading -> ServicesUiState.Loading
                is Result.Success -> ServicesUiState.Success(it.data)
            }
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = Result.Loading
        )
}


结果.kt

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart

sealed interface Result<out T> {
    data class Success<T>(val data: T) : Result<T>
    data class Error(val exception: Throwable) : Result<Nothing>
    data object Loading : Result<Nothing>
}

fun <T> Flow<T>.asResult(): Flow<Result<T>> {
    return this
        .map<T, Result<T>> {
            Result.Success(data = it)
        }
        .onStart {
            emit(Result.Loading)
        }
        .catch {
            emit(Result.Error(exception = it))
        }
}

以下是从域和存储库层获取数据的 2 个用例:

import com.example.domain.ServiceRepository
import com.example.domain.model.Service
import com.example.domain.util.DataStoreManager
import com.example.domain.model.FilterableServiceType
import kotlinx.coroutines.flow.Flow

class GetServicesUseCase(
    private val serviceRepository: ServiceRepository,
    private val dataStoreManager: DataStoreManager,
) {
    operator fun invoke(): Flow<List<Service>> =
        serviceRepository.getServices(dataStoreManager.getUserToken())
}

class GetServiceTypesUseCase(
    private val serviceRepository: ServiceRepository,
    private val dataStoreManager: DataStoreManager,
) {
    operator fun invoke(): Flow<List<FilterableServiceType>> =
        serviceRepository.getServiceTypes(
            tokenFlow = dataStoreManager.getUserToken()
        )
}

ServiceRepositoryImpl.kt:

import com.example.domain.ServiceRepository
import com.example.domain.model.FilterableServiceType
import com.example.domain.model.Service
import com.example.repository.api.model.ServiceType
import com.example.repository.api.model.toResponse
import com.example.repository.api.model.toService
import com.example.repository.util.toBearerToken
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow

class ServiceRepositoryImpl(private val apiDataSource: ApiDataSource) : ServiceRepository {
    override fun getServices(tokenFlow: Flow<String>): Flow<List<Service>> {
        return flow {
            val token = tokenFlow.first().toString().toBearerToken()
            val services = apiDataSource.getServices(token).map {
                it.toService()
            }
            emit(services)
        }
    }

    override fun getServiceTypes(tokenFlow: Flow<String>): Flow<List<FilterableServiceType>> =
        flow {
            val token = tokenFlow.first().toString().toBearerToken()
            apiDataSource.getServiceTypes(token)
                .map(ServiceType::toResponse)
                .map(::FilterableServiceType)
        }
}

ApiDataSourceImpl.kt

class ApiDataSourceImpl(
    private val apiService: ApiService,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ApiDataSource {
       override suspend fun getServices(token: String): List<ServiceModel> =
        withContext(ioDispatcher) {
            apiService.getServices(token = token)
        }

    override suspend fun getServiceTypes(token: String): List<ServiceType> =
        withContext(ioDispatcher) {
            apiService.getServiceTypes(token)
        }
}

ServiceScreen.kt 的可组合 UI

@Composable
fun ServiceScreen(
    serviceViewModel: ServiceViewModel,
    onServiceClick: (serviceId: String) -> Unit,
) {

    // Collect the UI states from the view model.
    val servicesUiState by serviceViewModel.servicesUiState.collectAsStateWithLifecycle()
    val serviceTypesUiState by serviceViewModel.serviceTypeUiState.collectAsStateWithLifecycle()

    println("serviceTypesUiState: $serviceTypesUiState")

    Column(
        modifier = Modifier.fillMaxSize()
    ) {

        when (val serviceTypesUiStateOb = serviceTypesUiState) {
            is ServiceTypesUiState.Success -> FilterRow(
                serviceTypesUiStateOb.serviceTypeResponses,
                serviceViewModel
            )

            is ServiceTypesUiState.Loading -> {
                CircularProgressBar()
            }

            is ServiceTypesUiState.Error -> {
                Text(text = "Error: ${serviceTypesUiStateOb.exception.localizedMessage}")
            }
        }

        when (val servicesUiStateOb = servicesUiState) {
            is ServicesUiState.Loading -> CircularProgressBar()

            is ServicesUiState.Success -> ServiceList(
                servicesUiStateOb.services,
                onServiceClick = onServiceClick
            )

            is ServicesUiState.Error -> servicesUiStateOb.exception.localizedMessage?.let {
                Text(
                    text = it
                )
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FilterRow(
    filterableServiceTypes: List<FilterableServiceType>,
    serviceViewModel: ServiceViewModel,
) {
    println("FilterRow: serviceTypesUiState : $filterableServiceTypes")

    var isSelected by remember { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        filterableServiceTypes.forEachIndexed { index, filterableServiceType ->
            ElevatedFilterChip(
                onClick = {
                    isSelected = !isSelected
                },
                label = {
                    Text(filterableServiceType.serviceType.type)
                },
                selected = isSelected,
                leadingIcon = when {
                    isSelected -> {
                        {
                            Icon(
                                imageVector = Icons.Filled.Done,
                                contentDescription = "Filter services for ${filterableServiceType.serviceType.type}",
                                modifier = Modifier.size(FilterChipDefaults.IconSize)
                            )
                        }
                    }

                    else -> {
                        null
                    }
                }
            )
        }
    }
}

sealed interface ServicesUiState {
    data object Loading : ServicesUiState
    data class Success(val services: List<Service>) : ServicesUiState
    data class Error(val exception: Throwable) : ServicesUiState
}

sealed interface ServiceTypesUiState {
    data object Loading : ServiceTypesUiState
    data class Success(val serviceTypeResponses: List<FilterableServiceType>) : ServiceTypesUiState
    data class Error(val exception: Throwable) : ServiceTypesUiState
}

服务类型业务逻辑的模型数据类:

data class ServiceType(
    val id: String,
    val type: String,
)

@Parcelize
data class ServiceTypeResponse(
    val id: String,
    val type: String,
) : Parcelable

class FilterableServiceType(
    val serviceType: ServiceTypeResponse,
    initialChecked: Boolean = false,
) {
    var isSelected: Boolean by mutableStateOf(initialChecked)
}

fun ServiceType.toResponse() = ServiceTypeResponse(
    id = id, type = type
)

当我启动应用程序并使用 NavHostController 从适当的路由导航到 ServiceScreen 时,我可以获取 /api/services 的数据,它正确地显示了 s 列表,但它不显示可组合项的 /api/services/types 的数据。我正在使用可组合项中的collectAsStateWithLifecycle收集状态。ServiceFilterRowServiceScreen

Android kotlin kotlin-stateflow

评论

0赞 Jay 11/15/2023
我真是一只傻鹅!我从未在我的流构建器方法中发出任何内容——它应该从调用中发出 API 响应。ServiceRepositoryImpl#getServiceTypes(Flow<String>)ApiDataSource#getServiceTypes(String)

答:

0赞 Jay 11/15/2023 #1

主要问题出在我在课堂上使用的流程构建器中。我正在调用 API,但从未从此流构建器发出任何内容。解决方案如下:ServiceRepositoryImpl


import com.example.domain.ServiceRepository
import com.example.domain.model.FilterableServiceType
import com.example.domain.model.Service
import com.example.repository.api.model.ServiceType
import com.example.repository.api.model.toResponse
import com.example.repository.api.model.toService
import com.example.repository.util.toBearerToken
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow

class ServiceRepositoryImpl(private val apiDataSource: ApiDataSource) : ServiceRepository {
    override fun getServices(tokenFlow: Flow<String>): Flow<List<Service>> {
        return flow {
            val token = tokenFlow.first().toString().toBearerToken()
            val services = apiDataSource.getServices(token).map {
                it.toService()
            }
            emit(services) // emitted here
        }
    }

    override fun getServiceTypes(tokenFlow: Flow<String>): Flow<List<FilterableServiceType>> =
        flow {
            val token = tokenFlow.first().toString().toBearerToken()
            emit( // missing emit call for this flow builder
                apiDataSource.getServiceTypes(token)
                    .map(ServiceType::toResponse)
                    .map(::FilterableServiceType)
            )
        }
}