시배's Android

TiTi Side Project | Infinite Recomposition Issue 본문

Android/TiTi Side Project

TiTi Side Project | Infinite Recomposition Issue

si8ae 2024. 6. 16. 18:26

문제 

TiTi에는 사용자가 자신의 공부 시간을 확인할 수 있는 TimeTable 기능이 있습니다. 어느 날 우연히 Layout Inspector를 사용하던 중, TimeTable에서 지속적으로 Recomposition 발생하고 있다는 사실을 발견했습니다.

문제의 원인

@Composable
fun TdsTimeTable(
    modifier: Modifier = Modifier,
    timeTableData: List<TdsTimeTableData>,
    colors: List<Color>,
) {
    var hour by remember {
        mutableStateOf("0")
    }
    var fontSize by remember {
        mutableStateOf(7.sp)
    }
    val textStyle = TdsTextStyle
        .SEMI_BOLD_TEXT_STYLE
        .getTextStyle(fontSize = fontSize)
        .copy(color = TdsColor.TEXT.getColor())
    val hourTextMeasurer = rememberTextMeasurer()
    val hourTextLayoutResult = remember(hour) {
        hourTextMeasurer.measure(hour, textStyle)
    }

    Canvas(
        modifier = modifier
            .drawWithContent {
                drawContent()
                drawGrid()
            },
    ) {
        val itemWidth = size.width / 7
        val itemHeight = size.height / 24
        fontSize = (itemHeight / 5).sp

        repeat(24) { idx ->
            hour = (idx + 6).let { if (it >= 24) it - 24 else it }.toString()

            val startX = if (hour.length > 1) {
                itemWidth / 2 - hourTextLayoutResult.size.width
            } else {
                itemWidth / 2 - hourTextLayoutResult.size.width / 2
            }
            val startY = itemHeight * (idx + 0.5f) - hourTextLayoutResult.size.height / 2

            drawText(
                textMeasurer = hourTextMeasurer,
                text = hour,
                style = textStyle,
                topLeft = Offset(
                    x = startX,
                    y = startY,
                ),
            )
        }

        timeTableData.forEachIndexed { index, timeTableData ->
            val idx = (timeTableData.hour - 6).let { if (it < 0) it + 24 else it }
            val startX = itemWidth + itemWidth * 6 * timeTableData.start / 3600f
            val startY = itemHeight * idx
            val barWidth = itemWidth * 6 * (timeTableData.end - timeTableData.start) / 3600

            drawRoundRect(
                color = colors[index % colors.size],
                cornerRadius = CornerRadius(itemHeight / 5, itemHeight / 5),
                topLeft = Offset(
                    x = startX,
                    y = startY,
                ),
                size = Size(
                    width = barWidth,
                    height = itemHeight,
                ),
            )
        }
    }
}

위 코드는 TdsTimeTable 컴포저블 함수를 통해 타임 테이블을 그리는 예시입니다. drawGrid 함수로 격자 모양을 그린 후, 첫 번째 칸에는 시간을 표시하고 나머지 칸에는 사용자의 공부 시간을 막대 그래프로 나타냅니다. 막대 그래프는 각 시간대에 맞춰 표시되며, timeTableData 리스트와 colors 리스트를 이용해 다양한 색상으로 표현됩니다.

 var hour by remember { mutableStateOf("0") }
 
 drawText(
 	textMeasurer = hourTextMeasurer,
 	text = hour,
 	style = textStyle,
 	topLeft = Offset(
 		x = startX,
 		y = startY,
 	),
 )

recomposition이 지속적으로 발생하는 이유는 hour가 remember를 통해 상태가 관리되다 보니, Canvas를 통해 onDraw 될 때마다 hour가 변경되어 recomposition이 발생하는 문제였습니다.

해결

repeat(24) { idx ->
    val hour = (idx + 6).let { if (it >= 24) it - 24 else it }.toString()
    val hourTextLayoutResult = hourTextMeasurer.measure(hour, textStyle)

    val startX = if (hour.length > 1) {
        itemWidth / 2 - hourTextLayoutResult.size.width
    } else {
        itemWidth / 2 - hourTextLayoutResult.size.width / 2
    }
    val startY = itemHeight * (idx + 0.5f) - hourTextLayoutResult.size.height / 2

    drawText(
        textMeasurer = hourTextMeasurer,
        text = hour,
        style = textStyle,
        topLeft = Offset(
            x = startX,
            y = startY,
        ),
    )
}

 

 

recomposition이 지속적으로 발생하는 문제는 hour의 상태를 remember로 관리하면서 발생했습니다. 이를 해결하기 위해 hour를 일반 변수로 변경하여 recomposition이 발생하지 않도록 수정할 수 있었습니다.

수정 후, Layout Inspector에서도 recomposition이 발생하지 않는 것을 확인할 수 있었습니다.