В моем проекте время от времени возникает ошибка "Unsupported concurrent change during composition" -> Вероятно, это вызвано одновременным обновлением объекта State из разных потоков.
Я пытаюсь понять, почему это могло произойти.
На данный момент это мои знания с помощью пользователей stackoverflow @Leviathan и @chuckj.
Мой псевдокод/логика выглядит следующим образом (в конце я добавлю больше кода):
HomeState для модели, используемой в HomeViewModel, а затем я передаю данные в HomeScreen.mutableStateOf<HomeState> для хранения данных и разрешения на передачу.ViewModel {
val userId = mutableStateOf<Int?>(null) // I know it could be in HomeState and I will refactor it during these changes
val homeState = mutableStateOf(HomeState(isAddressAddedFromOnboarding))
}
FlowCallbacks для обратной связи с HomeScreen. В ViewModel я передаю значение в поток, а затем собираю его на экране.ViewModel {
private val _ageVerificationSuccessCallback = MutableSharedFlow<AgeVerificationEventSource>()
val ageVerificationSuccessCallback = _ageVerificationSuccessCallback.asSharedFlow()
[...]
fun emitAge = _ageVerificationSuccessCallback.emit(value)
}
В HomeScreen (составной)
fun HomeScreen(
homeState: HomeState,
onAgeVerificationSuccess: Flow<AgeVerificationEventSource>) {
LaunchedEffect("AgeVerificationSuccess") {
onAgeVerificationSuccess.collectLatest { source ->
actionLogicWithSourceValue()
}
}
viewModel, а затем обновляю свой HomeState. У меня около 15 таких интеракторов.
то есть class GetStoreOpenInfoInteractor(
) {
suspend fun invoke(): Flow<StoreAvailability> {
return combine(
storeRepository.getCurrentDarkStore().distinctUntilChanged(),
basketInteractor.invoke().distinctUntilChanged()
)
[...]
class GetAccountTypeInteractor(
private val accountTypeRepository: AccountRepository
) {
fun invoke(): Flow<AccountType> {
return accountTypeRepository.getAccountType()
}
Одно из предложений заключалось в том, что это плохой подход и мне следует вместо сбора использовать combine(). Итак, я попробовал, но, к сожалению, не могу объединить функции приостановки, но в любом случае я попробовал это с 10 интеракторами, которые не являются приостановкой и обратным потоком.
Это мой код, но разве я не могу просто собрать состояние обновления? Комбинировать видимо проблематично и не работает не знаю почему именно.
[...]
: ViewModel() {
private val _homeState = MutableStateFlow(HomeState(isAddressAddedFromOnboarding))
val homeState: StateFlow<HomeState> = _homeState.asStateFlow()
init {
viewModelScope.launch {
combine(
getAccountTypeInteractor.invoke(),
getDeliveryEstimatedTimeInteractor.invoke(),
getInventoryInteractor.invoke(),
getSliderItemsInteractor.invoke(),
getBestSellersProductsInteractor.invoke(),
getSpecialCategoriesInteractor.invoke(),
getCurrentAddressInteractor.invoke(),
getVerificationAgeStateInteractor.invoke(),
getIsNewDarkStoreSyncInteractor.invoke(),
getAdditionalMenuOptionsInteractor.invoke(),
getBasketTotalValueWithoutDiscountInteractor.invoke(),
checkIsStoriesDataLoadedInteractor()
) { accountType, deliveryEstimatedTime, inventory, sliders, bestSellers, specialCategories, currentAddress, ageVerificationState, isLoading, additionalMenuOptions, basketTotalValueWithoutDiscount, storiesDataLoaded, ->
// it doesn't log anything, not even null
Log.d("homeStateVALUES", """
accountType: $accountType,
deliveryEstimatedTime: $deliveryEstimatedTime,
inventory: $inventory,
sliders: $sliders,
bestSellers: $bestSellers,
specialCategories: $specialCategories,
currentAddress: $currentAddress,
ageVerificationState: $ageVerificationState,
isLoading: $isLoading,
additionalMenuOptions: $additionalMenuOptions,
basketTotalValueWithoutDiscount: $basketTotalValueWithoutDiscount,
storiesDataLoaded: $storiesDataLoaded
""")
HomeState(
isAddressAddedFromOnboarding = isAddressAddedFromOnboarding,
isLogged = accountType == AccountType.STANDARD_ACCOUNT,
chipTime = deliveryEstimatedTime,
categories = inventory.map {
it.copy(subCategories = emptyList())
},
sliderItems = sliders,
bestsellers = bestSellers,
specialCategories = specialCategories,
currentAddress = currentAddress,
ageVerificationState = ageVerificationState,
isLoading = isLoading,
additionalMenuOptions = additionalMenuOptions,
basketTotalValueWithoutDiscount = basketTotalValueWithoutDiscount,
storiesDataLoaded = storiesDataLoaded
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(1000L),
initialValue = _homeState.value
)
}
}
onTriggeredEvent~
is HomeEvent.OnFreeCouponAccepted -> {
viewModelScope.launch {
_homeState.updateFlow {
copy(isAddressAddedFromOnboarding = false)
}
}
}
HomeScreen
fun HomeScreen(
state: StateFlow<HomeState> // I'm using here viewModel.homeState
) {
val homeState by state.collectAsStateWithLifecycle()
Чакж пишет:
Вам нужно искать записи в объекты MutableState непосредственно в @Компонуемая функция. Почти всегда это неправильный поступок. А MutableState следует изменять либо только в составе, либо только вне композиции, никогда и то и другое. Нарушение этого правила приведет к исключение одновременного изменения выше.
И я пошел искать любой код, который мог бы обеспечить композицию внутри, и я понимаю, что такой код, который у меня есть в моем HomeScreen другом компонуемом DoubleBackToLeaveApp, который использует внутренний mutableStateOf, может обеспечить рекомпозицию, поэтому я представляю ситуацию, в которой я меняю свой HomeState на в то же время в DoubleBackToLeaveAppmutableStateOf изменениях и это вызывает ошибку concurrent change во время композиции, я прав, не могли бы вы подтвердить? Но с другой стороны, это была бы ерунда: каждая другая библиотека, другой компонент компоновки с самосознанием работает и создается таким образом, так что, по моему мнению, это не может быть проблемой. В любом случае, почему compose просто не создает очередь композиций и вместо сбоя просто добавляет рекомпозицию в очередь, для меня это кажется ошибкой и недостаточной заботой о рекомпозиции.
@Composable
fun HomeScreen() {
val scaffoldState = rememberScaffoldState()
val screenHeightDp = LocalConfiguration.current.screenHeightDp.dp
val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp
val screenHeight by remember { mutableStateOf(screenHeightDp) }
val screenWidth by remember { mutableStateOf(screenWidthDp) }
val scope = rememberCoroutineScope() // it's not needed in refactor during this change I will delete and use only one coroutineScope
val focusManager = LocalFocusManager.current
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
[...]
@Composable
fun DoubleBackToLeaveApp(drawerState: DrawerState) {
val doubleBackToExitPressedOnce = remember { mutableStateOf(true) }
val backScope = rememberCoroutineScope()
val backContext = LocalContext.current
BackHandler(doubleBackToExitPressedOnce.value) {
[...]
Composable используется в HomeScreen
@Composable
fun DoubleBackToLeaveApp(drawerState: DrawerState) {
val doubleBackToExitPressedOnce = remember { mutableStateOf(true) }
val backScope = rememberCoroutineScope()
val backContext = LocalContext.current
Этот пример неправильный? Может ли это вызвать эту ошибку? Здесь состояние создается в компонуемом виде, и в то же время у меня есть логика viewModel с моим external state.
Должен ли я изменить его на:
@Composable
fun DoubleBackToLeaveApp(
drawerState: DrawerState,
doubleBackToExitPressedOnce: MutableState<Boolean>, // take care in my HomeState as other params?
backScope: CoroutineScope
) {
val backContext = LocalContext.current
Another example is it wrong here in your opinion? ` isAsked` is created in `@Composable` so again I should use `HomeState` and deliver it to this component, right?
@OptIn(ExperimentalComposeUiApi::class, ExperimentalPermissionsApi::class)
@Composable
fun TurnNotificationOn(
onFinish: () -> Unit
) {
val notificationPermissionState: PermissionState
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermissionState = rememberPermissionState(
Manifest.permission.POST_NOTIFICATIONS
)
} else {
onFinish()
return
}
val isAsked = remember { mutableStateOf(false) }
LaunchedEffect(notificationPermissionState.status) {
if (isAsked.value) {
onFinish()
}
isAsked.value = true
}
Видите ли вы какую-либо проблему в моем коде, которая могла бы вызвать этот сбой? Если да, то напишите, потому что воспроизвести это действительно сложно и я не вижу в этом никакой проблемы. Например, с моей точки зрения, я не обновляюсь напрямую из HomeScreen бизнес-логики, которая сохраняется в mutableStateOf в ViewModel.
Является ли объединение лучшим подходом, чем сбор? Это было представлено здесь Как исправить «Неподдерживаемое одновременное изменение во время композиции»?
Могу ли я использовать LaunchedEffect, а затем собирать внутри этого запущенного эффекта callbackflow из модели представления?
ИМХО, да. Это не должно вызвать проблем с перекомпозицией.
LaunchedEffect запускается один раз, когда первоначальная композиция занимает место, и он защищен от повторного запуска от количество потенциальных рекомпозиций (если оно не параметризовано и не некоторые параметры изменились). Поэтому это хорошо для бега анимации, закусочная и т. д.
LaunchedEffect также является компонуемым и запускает сопрограмму. за кулисами.
помните, что CoroutineScope также предоставляет вам сопрограмму, но это не Сборный. И это по уважительной причине. В противном случае он будет работать каждая рекомпозиция независимо от того. Поэтому его можно использовать, например, внутри обратных вызовов событий (например, onClick) для запуска функций приостановки.
Вот мои изменения на данный момент:
в viewModel
private val _homeState = MutableStateFlow(HomeState(isAddressAddedFromOnboarding))
val homeState: StateFlow<HomeState> = _homeState.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = _homeState.value,
)
в HomeScreen
val homeStateCollector = homeStateFlow.collectAsState()
val homeState = homeStateCollector.value
Используйте свой собственный комбайн с hot flow, например stateIn, вероятно, когда вы используете комбайн потока, он не подписывается на него с самого начала.
private val inventoryFlow = getInventoryInteractor.invoke()
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
Остальное создайте таким же образом.
val homeState: StateFlow<HomeState> =
combine(
accountTypeFlow,
deliveryEstimatedTimeFlow,
inventoryFlow,
[...]
private val _homeState = MutableStateFlow(HomeState(isAddressAddedFromOnboarding))
Тогда он должен работать правильно.
Этот пост слишком длинный и несфокусированный, чтобы на него можно было ответить. Нам не нужна история того, что вы сделали и что сказали другие. Пожалуйста, опишите только текущую проблему и удалите все остальное. Сделайте его как можно более кратким и сосредоточьтесь на одной проблеме. И убедитесь, что вы: Не используете
MutableStateв модели представления. Не собирайте данные в модели представления или LaunchedEffect. Не используйте блокinitмодели представления для чего-либо, связанного с потоком. Вы строго придерживаетесь схемы, которую я показал в ответе на другой вопрос. Этот вопрос должен быть новым, а не повторением старого вопроса.