在 Flow 上从 .first() 切换到 .collect{} 会停止数据出现在屏幕上

问题描述 投票:0回答:1

我用

viewmodel
Flow
做了一个
Room
,将
UiState
上的变量与
Room
使用
.first()
连接起来,正如代码实验室向我展示的那样。我注意到使用
.first()
不是一个好主意,因为当我在数据库中插入某些内容时,屏幕不会自动刷新以显示新值。所以我了解到使用
collect{}
是自动不断更新uistate变量的正确方法。现在更改后,我的屏幕没有显示任何数据,我无法找出原因。请问您能帮我找出问题出在哪里吗?我在这里添加
viewmodel

你可以看到旧代码的注释。我的意思是带有

.first()
的代码可以工作,但不会自动更新。注释的代码后面是带有
collect{}
的新代码,但根本不起作用。

data class UiState(
    val searchResult: List<Airport> = listOf(),
    val favorites: List<Favorite> = listOf(),
    val selectedAirport: Airport? = null,
    val flightsForSelectedAirport: List<Airport> = listOf()
)

class FlightsScreenViewModel(
    private val flightRepository: FlightsRepository,
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    var uiState by mutableStateOf(UiState())
        private set

    var searchText by mutableStateOf("")
        private set

    init {
        viewModelScope.launch {
//          searchText = userPreferencesRepository.searchText.first()
            userPreferencesRepository.searchText.collect{ searchText = it }
            updateSearchResults()
        }
    }

    private suspend fun updateSearchResults() {
//        val searchResult = if (searchText != "")
//            flightRepository.getAirportsByIatOrName(searchText).filterNotNull().first()
//        else
//            emptyList()
//
//        val favorites = flightRepository.getFavorites().filterNotNull().first()
//
//        uiState = uiState.copy(
//            searchResult = searchResult,
//            favorites = favorites
//        )

        if (searchText != "") {
            flightRepository.getAirportsByIatOrName(searchText).filterNotNull().collect{ uiState = uiState.copy(searchResult = it) }
        } else {
            uiState = uiState.copy(favorites = emptyList())
        }

        flightRepository.getFavorites().filterNotNull().collect{ uiState = uiState.copy(favorites = it) }
    }

    fun updateSearchText(searchText: String) {
        this.searchText = searchText
        viewModelScope.launch {
            userPreferencesRepository.saveSearchTextPreference(searchText)
            updateSearchResults()
        }
    }

    fun selectAirport(airport: Airport?) {
        viewModelScope.launch {
//            val flightsForSelectedAirport = if (airport == null) {
//                emptyList()
//            } else {
//                flightRepository.getAllDifferentAirports(airport.id).first()
//            }
//
//            uiState = uiState.copy(
//                selectedAirport = airport,
//                flightsForSelectedAirport = flightsForSelectedAirport
//            )

            if (airport != null) {
                flightRepository.getAllDifferentAirports(airport.id).collect{ uiState = uiState.copy(flightsForSelectedAirport = it) }
            } else {
                uiState = uiState.copy(flightsForSelectedAirport = emptyList())
            }

            uiState = uiState.copy(selectedAirport = airport)
        }
    }

    fun insertFavorite(depart: String, arrive: String) {
        if (!uiState.favorites.checkIfFavoriteExists(depart, arrive)) {
            val favorite = Favorite(departureCode = depart, destinationCode = arrive)
            viewModelScope.launch {
                flightRepository.insertFavorite(favorite)
            }
        }
    }

    companion object {
        val factory : ViewModelProvider.Factory = viewModelFactory {
            initializer {
                FlightsScreenViewModel(
                    FlightSearchApplication().container.flightRepository,
                    FlightSearchApplication().container.userPreferencesRepository
                )
            }
        }
    }
}

fun List<Favorite>.checkIfFavoriteExists(depart: String, arrive: String): Boolean{
    for (favorite in this){
        if (favorite.departureCode == depart && favorite.destinationCode == arrive)
            return true
    }
    return false
}

这是应该在屏幕上显示内容的代码,但从

.first()
更新到
.collect{}

后没有显示任何内容
LazyColumn(
    modifier = Modifier.padding(8.dp)
) {
    items(uiState.searchResult) { airport ->
        AirportDetail(airport, onAirportSelected)
    }
}
uiState.selectedAirport?.let {
    FlightsForAirport(
        airport = uiState.selectedAirport,
        arrivals = uiState.flightsForSelectedAirport,
        favorites = uiState.favorites,
        onFavoriteSelected = onFavoriteSelected
    )
}
android kotlin mvvm android-jetpack-compose kotlin-flow
1个回答
0
投票

代码实验室已经过时了。应该这样做:

  • 不要在视图模型中使用 MutableState,而是使用私有 MutableStateFlow。
  • 只有 transform 在视图模型中流动,不要
    collect
    它们。
  • 将转换后的流作为 StateFlow 公开,而不是 MutableState。

这就是你的视图模型的样子:

class FlightsScreenViewModel(
    private val flightRepository: FlightsRepository,
    private val userPreferencesRepository: UserPreferencesRepository,
) : ViewModel() {
    private val searchResult: Flow<List<Airport>> =
        userPreferencesRepository.searchText.flatMapLatest {
            if (it.isEmpty()) flowOf(emptyList())
            else flightRepository.getAirportsByIatOrName(it).filterNotNull()
        }

    private val favorites: Flow<List<Favorite>> =
        flightRepository.getFavorites().filterNotNull()

    private val selectedAirport = MutableStateFlow<Airport?>(null)
    private val flightsForSelectedAirport: Flow<List<Airport>> =
        selectedAirport.flatMapLatest {
            if (it == null) flowOf(emptyList())
            else flightRepository.getAllDifferentAirports(it.id)
        }

    val uiState: StateFlow<UiState> = combine(
        searchResult,
        favorites,
        selectedAirport,
        flightsForSelectedAirport,
        ::UiState,
    ).stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = UiState(),
    )

    fun updateSearchText(searchText: String) {
        viewModelScope.launch {
            userPreferencesRepository.saveSearchTextPreference(searchText)
        }
    }

    fun selectAirport(airport: Airport?) {
        selectedAirport.value = airport
    }

    fun insertFavorite(depart: String, arrive: String) {
        val favorite = Favorite(departureCode = depart, destinationCode = arrive)
        viewModelScope.launch {
            flightRepository.insertFavorite(favorite)
        }
    }

    companion object { /* ... */ }
}

流量永远不会被收集,它们只是被转换。例如,新转换的

searchResult
流是通过使用
userPreferencesRepository.searchText
流并将其映射到
flightRepository.getAirportsByIatOrName
流创建的。

另一方面,新的

flightsForSelectedAirport
流程基于新的
MutableStateFlow<Airport?>
,用于存储当前选定的机场。它的使用方式与 MutableState(*) 类似,因为它的
value
属性可以直接设置,如函数
fun selectAirport()
中所示。但它仍然是一个流,所以这里可以将其转换为
Flow<List<Airport>>

现在创建

UiState
所需的所有值都存在于流中,这些流可以
combine
合并为单个流。然后将该流转换为 StateFlow(这与 MutableStateFlow 相关,与 Compose State 无关)并公开公开。

视图模型的任何使用者现在都可以收集该流以获得始终最新的 UiState。在您的可组合项中使用它:

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

这是 Kotlin Flows 世界和 Compose State 世界之间的桥梁。它将 StateFlow 转换为 Compose State 对象。通过使用

by
委托,
uiState
实际上是
UiState
类型,其余代码的工作方式将与之前相同。


最后的想法:

  1. 您将

    filterNotNull()
    应用于
    getAirportsByIatOrName
    getFavorites
    流。这意味着流不仅可以包含列表,还可以包含
    null
    。这似乎不对,它们应该包含
    emptyList()
    而不是
    null
    。那么你应该删除
    filterNotNull()

  2. 我从

    checkIfFavoriteExists
    中删除了
    insertFavorite
    。这应该在存储库中完成。数据的一致性不应该依赖于视图模型的作用,这是存储库的责任(或者甚至是数据源的责任,如果您将其分开的话)。理想情况下,这应该在数据包装到流中之前完成。这一切都取决于实际如何实施。
    功能还可以精简一点:

    fun List<Favorite>.checkIfFavoriteExists(
        depart: String,
        arrive: String,
    ): Boolean = any {
        it.departureCode == depart && it.destinationCode == arrive
    }
    
  3. 您可以使用像Hilt这样的依赖注入框架来自动化它,而不是编写显式视图模型工厂。


(*):虽然

MutableStateFlow
MutableState
具有相似的名称,但它们是完全不同的类型,完全不相关。前者是 Kotlin 标准库的一部分,后者是 Compose 框架的一部分。它们共享一些语义,因为它们都存储可通过
value
属性访问的可观察值,但仅此而已。

© www.soinside.com 2019 - 2024. All rights reserved.