티스토리 뷰

 

Jetpack Compose에서 사용자의 경험을 풍부하게 하고 앱의 스타일에 부합하는 유연한 AppBar 동작을 구현하는 것은 매우 중요한 일입니다.

애초부터 Jetpack compose는 이러한 커스텀 동작들을 구현하는 데 특화되어 있습니다.

 

본론으로 들어가자면 Jetpack Compose는 하위 컴포저블 요소에서 전파하는 스크롤 동작을 감지할 수 있는 Modifier.nestedScroll 이라는 기능을 제공합니다.

 

Modifier.nestedScroll은 NestedScrollConnection 인터페이스를 매개변수로 받습니다.

해당 인터페이스는 하위 컴포저블 요소에서 발생시키는 스크롤 동작을 자신의 동작으로 치환할 수 있는 기능을 제공합니다.

 

먼저 매우 간단한 예제를 살펴보도록 하겠습니다.

// 하위 요소에 스크롤 동작이 발생하게 되면 자신의 스크롤 동작(발생된 스크롤 이동거리)을 소비 또는 치환할 수 있는 기회를 주려고
// 상위 요소에 이를 전파합니다.
//
// 여기서 onPreScroll은 자신이 스크롤 오프셋을 반영하기 전, 이를 소비할 수 있게 하기 위해 제공되는 메서드입니다.
// 반대로 onPostScroll은 자신이 스크롤 오프셋을 반영하고 나서 남은 스크롤 이동거리를 소비할 수 있게 하기 위해 제공되는 메서드입니다.
//
// 또한 반환되는 값은 중첩 스크롤 관련 요소가 최종적으로 발생된 스크롤 이동거리를 소비한 값을 의미합니다.
...(
    modifier = Modifier.nestedScroll(remember { object : NestedScrollConnection {
        override fun onPostScroll ...
        override fun onPreScroll ...
    } })
) {
    // ...
    content()
}

 

앱바의 상태가 변화할 때마다 매번 리컴포지션이 발생하는 것을 방지하고 효율적으로 원하는 앱바 동작을 수행하기 위해서는 상위 요소에서 하위 요소의 몫인 앱바 상태에 따른 렌더링 동작을 상위 요소에서 제어해선 안됩니다.

 

해당 방식을 사용하게 되면 불필요한 리컴포지션이 발생할 수 있습니다, 따라서 관련 앱바 상태는 앱바 컴포저블 요소에 독립적으로 초기화되고 참조되어야 합니다. (여기서 중첩 스크롤 관련 수신은 상위 요소에서 하위 요소로 다시 전파될 필요가 없습니다. 초기 상태가 초기화될 때 상위 요소에서 전파되어 참조 가능한 관련 Controller에 앱바 상태를 연결하고 Controller를 제어하고 있는 상위 요소에서 관련 함수를 호출하여 전체 앱바 동작을 수행하면 됩니다.)

 

@Composable
fun AppBar(
    modifier: Modifier = Modifier,
    alignment: SliverAlignment = SliverAlignment.Scroll,
    behavior: SliverBehavior = DefaultSliverBehavior(),
    state: SliverState = rememberSliverState(),
    content: @Composable (SliverState) -> Unit,
) {
    val controller = AppBarController.Provider.current

    LaunchedEffect(key1 = "Attach state to controller") {
        controller.attach(state, behavior)
    }

    DisposableEffect(key1 = "Detach state to controller") {
        onDispose { controller.detach(state) }
    }

    Box(
        modifier = modifier
            .clipToBounds()
            .layout { measurable, constraints ->
                // ... 생략
            }
    ) {
        content(state)
    }
}

@Composable
fun AppBarConnection(
    appBars: @Composable ColumnScope.() -> Unit,
    scrollableState: ScrollableState? = null,
    content: @Composable () -> Unit,
) {
    val controller = remember { AppBarController() }

    controller.scrollableState = scrollableState

    CompositionLocalProvider(AppBarController.Provider provides controller) {
        Column(
            modifier = Modifier.nestedScroll(remember { object : NestedScrollConnection {
                override fun onPostScroll(
                    consumed: Offset,
                    available: Offset,
                    source: NestedScrollSource
                ): Offset {
                    // 이미 스크롤이 모두 반영되었다면 앱바 동작을 수행할 필요가 없습니다.
                    if (available.y != 0f) {
                        return Offset(
                            x = 0f,
                            y = controller.onPreScroll(available.y, source)
                        )
                    }

                    return Offset.Zero
                }

                override fun onPreScroll(
                    available: Offset,
                    source: NestedScrollSource
                ): Offset {
                    return Offset(
                        x = 0f,
                        y = controller.onPreScroll(available.y, source),
                    )
                }
            } })
        ) {
            ... appBars 생략
            content()
        }
    }
}

 

나머지는 매우 간단하기 때문에 알아서 구현하실 것이라고 믿으면서 저는 이만 사라지도록 하겠습니다. 빠이.

직접 구현 말고 패키지 형태로 간단하게 사용하고 싶다면 아래를 참고해주세요:
https://github.com/MTtankkeo/compose_appbar