시배's Android

TiTi Side Project | Subcompose 1 frame Lag Issue 본문

Android/TiTi Side Project

TiTi Side Project | Subcompose 1 frame Lag Issue

si8ae 2024. 4. 1. 19:58

문제

Timer TiTi를 개발하며 겪었던 흥미로운 이슈와 그 해결 과정을 공유하려 합니다. 개발 과정 중 데이터는 정상적으로 업데이트되었지만, 특정 그래프가 화면에 표시되지 않는 문제에 직면했습니다.

문제의 원인

@Composable
private fun TdsPieChart(
    modifier: Modifier = Modifier,
    taskData: List<TdsTaskData>,
    colors: List<Color>,
    progress: Float,
    containsDonut: Boolean,
    totalTimeString: String?,
) {
    var startAngle = 270f
    val density = LocalDensity.current

    BoxWithConstraints(
        modifier = modifier,
        contentAlignment = Alignment.Center,
    ) {
        val radius = with(density) { (min(maxWidth, maxHeight) / 3).toPx() }
        val centerX = with(density) { (maxWidth / 2).toPx() }
        val centerY = with(density) { (maxHeight / 2).toPx() }
        val holeRadiusPercent = if (containsDonut || totalTimeString != null) {
            0.5f
        } else {
            0f
        }
        val holeRadius = radius * holeRadiusPercent
        val holeRadiusDp = with(density) { holeRadius.toDp() }

        Canvas(modifier = Modifier.fillMaxSize()) {
            taskData.forEachIndexed { index, pie ->
                val sweepAngle = (pie.progress * 360 - 1) * progress

                drawArc(
                    color = colors[index % colors.size],
                    startAngle = startAngle,
                    sweepAngle = sweepAngle,
                    useCenter = false,
                    topLeft = Offset(centerX - radius, centerY - radius),
                    size = Size(radius * 2, radius * 2),
                    style = Stroke(width = radius - holeRadius),
                )

                startAngle += sweepAngle

                drawArc(
                    color = Color.Black,
                    startAngle = startAngle,
                    sweepAngle = 1 * progress,
                    useCenter = false,
                    topLeft = Offset(centerX - radius, centerY - radius),
                    size = Size(radius * 2, radius * 2),
                    style = Stroke(width = radius - holeRadius),
                )

                startAngle += 1
            }
        }

        if (containsDonut || totalTimeString != null) {
            if (totalTimeString == null) {
                TdsTaskResultList(
                    modifier = Modifier.size(holeRadiusDp * 2),
                    taskData = taskData,
                    isSpacing = false,
                    height = holeRadiusDp * 2 / 5,
                    colors = colors,
                )
            } else {
                Box(
                    modifier = Modifier.size(holeRadiusDp * 2),
                    contentAlignment = Alignment.Center,
                ) {
                    TdsText(
                        text = totalTimeString,
                        textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE,
                        fontSize = with(density) { (holeRadius).toSp() },
                        color = TdsColor.TEXT,
                    )
                }
            }
        }
    }
}

문제의 원인을 파악하기 위해 발생한 컴포넌트들을 자세히 살펴보니, 모두 BoxWithConstraints로 감싸진 공통점이 있었습니다.

 

Google Issue Tracker

 

issuetracker.google.com

이슈 트래커를 뒤져보니 저와 동일한 문제를 겪고 있는 다른 개발자의 글을 발견했습니다. 이를 통해 문제가 이미 알려져 있으며 수정은 되었지만 아직 새 버전으로 배포되지 않았음을 알 수 있었습니다.

임시 방편

val currentColors by rememberUpdatedState(newValue = colors)
val currentTaskData by rememberUpdatedState(newValue = taskData)

이슈의 댓글에서 제안된 대로, rememberUpdatedState를 사용하여 임시로 문제를 해결할 수 있었습니다. 이 방법을 통해 변경된 값이 BoxWithConstraints로 감싸진 컴포넌트에도 정상적으로 반영되어 그래프가 올바르게 그려지기 시작했습니다.

변경사항 분석

 

https://android-review.googlesource.com/c/platform/frameworks/support/+/2938339/5/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt#b216

 

android-review.googlesource.com

이 문제의 근본적인 해결을 위해 변경사항을 자세히 살펴보았습니다. 변경된 내용을 요약하자면 다음과 같습니다:

  1. compositionInvalidations의 유효성 검사 추가: 이는 Compose에서 사용되는 특별한 값의 집합으로, 일반적인 값들과 다르게 스냅샷 시스템에 의해 관찰되지 않습니다. 대신, 이 값들은 직접적으로 구성 스코프를 무효화합니다. 이 변경으로 인해 compositionInvalidations가 비어있지 않은 경우에도 구성을 다시 생성할 수 있게 되었습니다.
  2. compositionInvalidations의 처리 방식 변경: 이 값들은 이제 knownCompositions과 함께 사용되며, 필요한 경우 toRecompose에 추가됩니다. 이 변경은 alreadyComposed에 없는 경우에만 실행됩니다.

이러한 변경을 통해, modifiedValues가 비어있거나 compositionInvalidations가 비어있지 않은 상황에서도, 또는 이 둘이 모두 비어있지 않은 경우에도 코드가 실행될 수 있게 되었습니다. 이는 compositionInvalidations를 통해 직접적으로 구성을 다시 생성할 수 있게 하며, 필요한 경우 toRecompose에 추가됩니다.