시배's Android

Compose | AnimatedContent를 사용할때 발생한 에러와 해결과정 본문

Android/Compose

Compose | AnimatedContent를 사용할때 발생한 에러와 해결과정

si8ae 2023. 11. 21. 22:16

저희 프로젝트에서 발생한 Android Jetpack Compose AnimatedContent를 사용하면서 발생한 에러와 그에 대한 해결 과정을 공유하려 합니다.

저희 프로젝트는 생활지수와 관련된 정보를 백엔드에서 받아와 이를 롤링되는 이미지로 시각적으로 제공하고 있습니다.

fun <T> VerticalRollingContent(
    modifier : Modifier = Modifier,
    enterDurationMillis: Int = 300,
    enterDelayMillis: Int = 500,
    exitDurationMillis: Int = 300,
    exitDelayMillis: Int = 0,
    items: List<T>,
    content: @Composable (T) -> Unit,

) {
    var index by rememberSaveable { mutableIntStateOf(0) }
    var state by remember { mutableStateOf(false) }

    LaunchedEffect(index) {
        delay(2000)
        state = !state
        delay(500)
        index = (index + 1) % items.size
    }

    AnimatedContent(
        modifier = modifier,
        targetState = state,
        label = "",
        transitionSpec = {
            (fadeIn(tween(enterDurationMillis, enterDelayMillis)) + slideInVertically(
                tween(
                    enterDurationMillis,
                    enterDelayMillis
                )
            ) { it / 3 }).togetherWith(
                fadeOut(tween(exitDurationMillis, exitDelayMillis)) + slideOutVertically(
                    tween(
                        exitDurationMillis,
                        exitDelayMillis
                    )
                ) { -it / 3 }
            )
        }
    ) {
        content(items[index])
    }
}

이를 위해 VerticalRollingContent라는 컴포넌트를 개발하여 백엔드에서 받아온 리스트를 활용해 각 아이템을 화면에 표시하고 있었습니다.

 

java.lang.IndexOutOfBoundsException : Index : 8, Size : 2

최근 특정 사용자로부터 앱이 강제 종료되는 문제를 나타내는 Crashlytics 보고를 받았습니다. 문제의 원인은 현재 롤링되고 있는 생활지수 리스트가 새로고침을 통해 새로운 사이즈의 리스트로 갱신되었을 때, 기존의 index 값이 state 변경 없이 그대로 남아 있어 IndexOutOfBoundException 에러가 발생한 것이었습니다.

val maxSize by rememberUpdatedState(newValue = items.size)
var index by rememberSaveable { mutableIntStateOf(0) }
var state by remember { mutableStateOf(false) }

LaunchedEffect(index) {
    delay(2000)
    state = !state
    delay(500)
    index = (index + 1) % items.size
}

LaunchedEffect(maxSize){
    state = !state
    index = 0
}

초기에는 리스트 사이즈 변경 시 index 값을 0으로 초기화하는 코드를 작성하여 문제를 해결하려 했지만, 이 코드 역시 문제가 발생했습니다. AnimatedContent가 이미 애니메이션을 진행하는 동안에는 index 값이 초기화되기 전에 상태를 가지고 있어 에러가 발생했던 것이었습니다.

@SuppressLint("UnusedContentLambdaTargetStateParameter")
@Composable
fun <T> KdsVerticalRollingContent(
    modifier : Modifier = Modifier,
    enterDurationMillis: Int = 300,
    enterDelayMillis: Int = 500,
    exitDurationMillis: Int = 300,
    exitDelayMillis: Int = 0,
    item: T,
    onStateChange : () -> Unit,
    content: @Composable (T) -> Unit,
) {
    var state by remember{ mutableStateOf(false)}

    LaunchedEffect(state){
        delay(2000)
        state = !state
        onStateChange()
    }

    AnimatedContent(
        modifier = modifier,
        targetState = state,
        label = "",
        transitionSpec = {
            (fadeIn(tween(enterDurationMillis, enterDelayMillis)) + slideInVertically(
                tween(
                    enterDurationMillis,
                    enterDelayMillis
                )
            ) { it / 3 }).togetherWith(
                fadeOut(tween(exitDurationMillis, exitDelayMillis)) + slideOutVertically(
                    tween(
                        exitDurationMillis,
                        exitDelayMillis
                    )
                ) { -it / 3 }
            )
        }
    ) {
        content(item)
    }
}

그래서 해결 방법으로, 컴포넌트 내부에서 index 상태값을 관리하는 것이 아니라 부모 컴포넌트에서 index 상태값을 가지는 state hoisting을 진행하고, 컴포넌트 내부에서는 item 객체만을 바라보도록 수정했습니다. 이로써 AnimatedContent가 item이 변경되었을 때에만 새로운 상태를 반영하도록 하여 에러를 방지할 수 있었습니다.

 

이와 같은 해결책을 통해 사용자들에게 더욱 안정적인 앱 서비스를 제공할 수 있게 되었습니다. 계속해서 더 많은 개발 이슈와 해결 과정을 공유해 나가겠습니다! 🚀