Compose: запуск обратного вызова навигации из Composable с изменениями состояния, вызывающими бесконечные вызовы при навAndroid

Форум для тех, кто программирует под Android
Ответить Пред. темаСлед. тема
Anonymous
 Compose: запуск обратного вызова навигации из Composable с изменениями состояния, вызывающими бесконечные вызовы при нав

Сообщение Anonymous »

У меня есть экран-заставка (или любой другой экран), который вызывает API и меняет состояние с «Загрузка» на «Завершено» или «Не удалось».
Это моя UiModel:

Код: Выделить всё

@Immutable
data class SplashUiModel(
var state: SplashLoadingState = SplashLoadingState.InProgress,
)

sealed interface SplashLoadingState {

object InProgress: SplashLoadingState

object Valid: SplashLoadingState

object Invalid: SplashLoadingState

}
В моей ViewModel это выглядит так:

Код: Выделить всё

private val _state: MutableStateFlow = MutableStateFlow(SplashUiModel())
val state = _state.asStateFlow()
Я создал функцию расширения, которая должна просто обновлять свое значение, только если новое значение отличается:

Код: Выделить всё

inline fun  MutableStateFlow.updateIfDifferent(function: (T) -> T) {
while (true) {
val prevValue = value
val nextValue = function(prevValue)

// Check if the value is actually different before trying to update
if (prevValue == nextValue) {
// No need to update if the value hasn't changed
return
}

// Atomically update the value using compareAndSet
if (compareAndSet(prevValue, nextValue)) {
return
}
}
}
В ответе API внутри ViewModel я, например, звоню, что ответ недействителен:

Код: Выделить всё

_state.updateIfDifferent { it.copy(state = SplashLoadingState.Invalid) }
В пределах моего составного объекта я собираю состояние:

Код: Выделить всё

val state = viewModel.state.collectAsStateWithLifecycle()
А дальше я решаю, что делать/показывать:

Код: Выделить всё

        when(state.value) {
SplashLoadingState.Valid -> {
listener.onNavigateValid()
}
SplashLoadingState.Invalid -> {
listener.onNavigateInvalid()
}
SplashLoadingState.InProgress -> {
BaseLoadingIndicator()
}
}
Слушатель:

Код: Выделить всё

 var hasNavigated by remember { mutableStateOf(false) }

val listener = object : SplashScreenListener {
override fun onNavigateValid() {
if (!hasNavigated) {
hasNavigated = true
onValid.invoke()
}
}

override fun onNavigateInvalid() {
if (!hasNavigated) {
hasNavigated = true
onInvalid.invoke()
}
}
}

Код: Выделить всё

onValid.invoke()
и onInvalid.invoke() — это функции обратного вызова внутри составного конструктора SplashScreen, вызывающего navController.navigate на другой экран. Я использую навигацию TypeSafe, поэтому иногда эти обратные вызовы содержат даже данные, которые передаются на другой экран.

Код: Выделить всё

SplashScreen
— это базовый простой пример, но обычно класс UiModel содержит больше вещей, таких как загруженные данные, которые отображаются в Composable (списки, строки и т. д.), а не просто состояние.
Что сейчас со мной происходит то, что onValid вызывается бесконечно, если я меняю состояние на основе ответа на вызов API. И экран постоянно мерцает. Я не понимаю, почему это тот случай, когда я даже добавил флаг hasNavigated, чтобы вызвать этот обратный вызов навигации только один раз. Также updateIfDifferent также должен предотвращать обновление значения, если оно одинаковое (в большинстве случаев это не так, но этот вызов API выполняется только один раз при запуске приложения).
I даже попробуйте использовать LaunchedEffect, чтобы запустить его хотя бы один раз:

Код: Выделить всё

when(state.value) {
SplashLoadingState.Valid -> {
LaunchedEffect(Unit){
listener.onNavigateValid()
}
}
SplashLoadingState.Invalid -> {
LaunchedEffect(Unit){
listener.onNavigateInvalid()
}
}
SplashLoadingState.InProgress -> {
BaseLoadingIndicator()
}
}
Результат все тот же.
ОБНОВЛЕНИЕ:
Хорошо, это звучит безумно, но у меня есть другой экран, на котором переданы пользовательские NavTypes типа Parcelable. (передача объектов данных, содержащих объекты данных, поэтому мне пришлось создать для него собственный NavType). И даже если я не использую его в этой функции обратного вызова, по какой-то причине (возможно, это ошибка NavGraphBuilder или что-то в этом роде), он вызывает рекомпозицию каждого отдельного экрана в моем навигационном графе.
Пример :

Код: Выделить всё

dialog(
typeMap = mapOf(
typeOf
() to CustomNavTypes.parcelableType(),
typeOf() to CustomNavTypes.parcelableType(),
typeOf() to CustomNavTypes.parcelableType()
)
) {
RandomScreenComposable(
viewModel = hiltViewModel(),
)
}

inline fun  parcelableType(
isNullableAllowed: Boolean = false,
json: Json = Json,
) = object : NavType(isNullableAllowed = isNullableAllowed) {
override fun get(bundle: Bundle, key: String) =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
bundle.getParcelable(key, T::class.java)
} else {
@Suppress("DEPRECATION")
bundle.getParcelable(key)
}

override fun parseValue(value: String): T = json.decodeFromString(value)

override fun serializeAsValue(value: T): String = json.encodeToString(value)

override fun put(bundle: Bundle, key: String, value: T) = bundle.putParcelable(key, value)
}
Если кто-нибудь знает, почему это происходит, дайте мне знать, потому что это кажется таким случайным.
Если я не могу использовать в этом пользовательские объекты новая навигация TypeSafe, это усложнит мою жизнь, поскольку мне придется передавать простые типы, такие как Strings и Ints, только между экранами.
Когда я закомментировал эти NavTypes и удалил разделяемый класс данных , на моем SplashScreen все работает нормально.

Подробнее здесь: https://stackoverflow.com/questions/790 ... nges-causi
Реклама
Ответить Пред. темаСлед. тема

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение

Вернуться в «Android»