Я сделал viewmodel
с Flow
и Room
, который соединил мои переменные на UiState
с Room
, используя .first()
, как мне показали Codelabs. Я заметил, что использование .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
)
}
Лаборатория кода устарела. Вместо этого это следует сделать так:
collect
их.Вот как должна выглядеть ваша модель представления:
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
d объединить в один Flow. Затем этот поток преобразуется в StateFlow (связанный с MutableStateFlow, а не с Compose State) и публикуется.
Любой потребитель модели представления теперь может собрать этот поток и получить UiState, которое всегда будет актуальным. Используйте это в своей композиции:
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Это мост между миром потоков Kotlin и миром состояний Compose. Он преобразует StateFlow в объект Compose State. Используя делегирование by
, uiState
на самом деле имеет тип UiState
, и остальная часть вашего кода будет работать так же, как и раньше.
Для этого вам понадобится зависимость Gradle androidx.lifecycle:lifecycle-runtime-compose
.
Последние мысли:
Вы применяете filterNotNull()
к потокам getAirportsByIatOrName
и getFavorites
. Это означает, что потоки могут содержать не только список, но и null
. Это кажется неправильным, они должны содержать emptyList()
вместо null
. Тогда вам следует удалить filterNotNull()
.
Я удалил checkIfFavoriteExists
из insertFavorite
. Вместо этого это следует сделать в репозитории. Согласованность данных не должна зависеть от того, что делает модель представления, это ответственность репозитория (или даже источника данных, если он у вас разделен). В идеале это должно быть сделано еще до того, как данные будут помещены в поток. Все зависит от того, как это реализовано на самом деле.
Функцию также можно немного упростить:
fun List<Favorite>.checkIfFavoriteExists(
depart: String,
arrive: String,
): Boolean = any {
it.departureCode == depart && it.destinationCode == arrive
}
Вместо написания явной фабрики модели представления вы можете автоматизировать ее, используя среду внедрения зависимостей, например Hilt.
(*): Хотя MutableStateFlow
и MutableState
имеют схожие названия, это совершенно разные типы, совершенно не связанные друг с другом. Первый является частью стандартной библиотеки Kotlin, а второй — частью платформы Compose. Они имеют некоторую общую семантику, поскольку оба хранят наблюдаемое значение, к которому можно получить доступ с помощью свойства value
, но это все.
также мне нужно получить объект uiState с обычными переменными, а не поток с потоками внутри, потому что необходимо рисовать эти значения на компонуемых объектах, а в вашем коде этого тоже нет.
Мне жаль слышать это. Я думаю, вам не хватает всего лишь кое-чего, чтобы осмыслить это, вы, кажется, уже хорошо разбираетесь во многих основных концепциях, необходимых для этого подхода (например, потоков). Этот код на самом деле значительно менее сложен по сравнению с тем, что у вас было раньше: есть лишь некоторые статические преобразования потока как часть свойств модели представления и простые функции установки. Я также считаю, что код легче читать, поскольку все компоненты лучше инкапсулированы. Внутренние переменные модели представления теперь гораздо менее переплетены.
Но позвольте мне попытаться затронуть поднятые вами вопросы, надеюсь, это прояснит ситуацию. 1.) Значение TextField не должно храниться в модели представления. Сохраните его в своих составных объектах, используя mutableStateOf<String>
или rememberTextFieldState
(последний вариант несколько более сложный, но перспективный). Независимо от этого вы можете решить сохранить значение, как и в своем репозитории saveSearchTextPreference
.
2.) Я удалил блок инициализации, потому что он больше не нужен, поскольку потоки больше не собираются в модели представления. 3.) Вы получаете «объект uiState с обычными переменными». Тип val uiState by viewModel.uiState.collectAsStateWithLifecycle()
— это простой класс данных UiState
. Ни в одном месте кода нет «потока с потоками внутри». -- На самом деле пункты 2) и 3) даже не должны вас беспокоить, потому что они просто сработают. Более актуальным вопросом будет то, что на самом деле мешает вам запустить код. Могу ли я чем-нибудь конкретным помочь?
большое спасибо, после двух дней обучения я думаю, что теперь я могу понять ваш код и заставить его работать.
Есть одна вещь, которую я до сих пор не понимаю: почему вы должны передавать ::UiState в качестве последнего параметра в поток состояний uiState?
может быть ты поможешь и с этим? stackoverflow.com/questions/78615077/…
Последний параметр combine
— это функция преобразования, которая создает содержимое объединенного потока. ::UiState
— это упрощенный синтаксис для создания объекта UiState
из содержимого потоков. Нажмите на него и нажмите Alt+Enter, затем выберите «Преобразовать ссылку в лямбду», чтобы увидеть его полную форму. Он гораздо более подробный, но, возможно, это поможет вам лучше понять, что здесь на самом деле происходит.
Причина, по которой переключение на collect
остановило обновление, заключается в том, что collect()
— это функция, которая не возвращается к вызывающей стороне до тех пор, пока не будет использован весь поток. Поток из Room бесконечен (поскольку он всегда ожидает, произойдет ли еще одно изменение в базе данных, чтобы она могла снова отправить запрос), поэтому вызов collect
в потоке из Room никогда не вернет результат. Следовательно, ваша строка updateSearchResults()
является недоступным кодом.
На самом деле вы обнаружите, что большинство приложений Flows предназначены для бесконечных потоков, поскольку они имеют тенденцию представлять непрерывный поток данных, отслеживая что-то на предмет изменений. Кроме того, все SharedFlow и StateFlow по определению бесконечны (никогда не завершаются без отмены).
Если вам нужно вызвать collect
, вам нужно изменить порядок вызовов в вашей сопрограмме, чтобы после вызова collect()
не было вызовов, либо я переместил этот вызов в лямбду сбора, если это необходимо, либо запустил несколько параллельных сопрограмм. Однако, как указано в ответе Левиафана, обычно вы не собираете потоки в ViewModel, особенно если вы используете Compose.
Привет @Leviathan, спасибо за твой ответ, но должен сказать, что для меня это очень продвинуто. Я считаю ваш ответ очень сложным. Не могу понять этого на самом деле. Я не могу поверить, что не существует более простого и понятного способа, который был бы просто удобен для чтения человеком, для хранения некоторых простых значений текущего состояния пользовательского интерфейса. С другой стороны, я даже не могу заставить ваш код работать. Я думаю, возможно, это потому, что вы удалили переменную mutablestate searchText, и это необходимо, потому что она связана с TextField, который должен работать с переменной mutablestate, а не напрямую с репозиторием. Также вы удалили блок инициализации.