Как правильно подойти к вопросу: compose: Откуда берется рассинхрон в mutableState при его изменении в LaunchedEffect?
Сейчас ситуация такая: друзья, только изучаю compose, и столкнулся с проблемой:
Есть объект
SomeState(val result : SomeObject? = null)
По вводным: есть ViewModel, в которой:
val state = mutableStateOf<SomeState>(SomeState(null)) fun load() { state.value = SomeState(null) /// some operation state.value = SomeState(someNotNullResult) }
По вводным: и есть Composable функция
val vm = hiltViewModel<SomeViewModel>(key = "123") LaunchedEffect(Unit) { vm.load() } if(vm.state.value.result != null) { Show(vm.state.value.result) }
Сейчас ситуация такая: все это у меня показывается в модальном диалоге, то есть активити / фрегмент не меняется, соответсвенно ViewModel переиспользуется.
По вводным: наблюдаю следующее поведение.
По вводным: открываю модальное окно первый раз
По вводным: все работает нормально, метод load в LaunchedEffect загружает объект, и потом его успешно отображает.
По вводным: но при втором открытии модального окна происходит следующее (судя по дебагу):
Сейчас ситуация такая: изначально vm.state.value.result не равно NULL - так как он подгружен при прошлом открытии.
Это понятно.
По вводным: далее метод load в LaunchedEffect вызывает state.value = SomeState(null), то есть vm.state.value.result теперь должно быть null
По вводным: но в Composable функции условие vm.state.value.result!= null все равно проходит, ну и приложение падает, так как в этот момент vm.state.value.result в действительности равно null
Как такое может быть?
Опишите проблему, и специалист поможет с настройкой, исправлением ошибки или доработкой сайта. Подберём понятный план работ без лишней переписки.
Пока нет других ответов. Будьте первым, кто поможет автору.
Ответить на вопрос
В Jetpack Compose у вас не “рассинхрон” mutableState, а нормальное поведение переиспользуемой ViewModel. Вы открываете модальное окно повторно, но ViewModel с ключом
123остаётся той же. Поэтому в момент первой композиции состояние ещё содержит прошлыйresult, UI успевает увидеть старое значение, а уже потомLaunchedEffectвызываетload()и сбрасывает его в null.LaunchedEffect(Unit)запускается после входа composable в композицию, а проверкаif(vm.state.value.result != null)происходит во время композиции. Поэтому порядок такой: сначала рисуется старый result, потом стартует эффект, потом state становится null, потом приходит новый result.Первый вариант — очищать состояние до открытия модального окна, а не внутри
LaunchedEffect. Например, при нажатии на кнопку:fun openDialog() { viewModel.reset() showDialog = true }
Второй вариант — сделать состояние с явным статусом загрузки:
data class SomeState( val isLoading: Boolean = false, val result: SomeObject? = null ) fun load() { state.value = SomeState(isLoading = true) viewModelScope.launch { val result = repository.load() state.value = SomeState(result = result) } }
В UI тогда не нужно угадывать по null:
val state by vm.state when { state.isLoading -> Loader() state.result != null -> Show(state.result) }
Третий вариант — использовать уникальный key для ViewModel/диалога, если каждый запуск должен быть полностью новым экземпляром. Но это не всегда правильно: часто ViewModel специально сохраняют, чтобы не терять данные при пересоздании UI.
Ещё лучше заменить
mutableStateOfв ViewModel наStateFlow, если состояние асинхронное:private val _state = MutableStateFlow(SomeState()) val state = _state.asStateFlow()
Итог: старое значение видно потому, что ViewModel переиспользуется, а
LaunchedEffectне выполняется до первой проверки UI. Сбрасывайте состояние до открытия, используйте loading-состояние или создавайте новый key, если вам нужен полностью новый цикл данных.