읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

Jetpack Compose의 레이아웃 코드랩에서 실습한 내용들을 정리합니다. Compose에서 목록형 레이아웃에 아이템들을 담아 출력하는 LazyList 관련된 컴포저블에 대해 실습합니다. 이미지를 네트워크에 요청하여 렌더링해본 뒤 코루틴을 사용하여 스크롤 제어까지 다룹니다. 코드랩 실습에 요구되는 시간은 대략 1시간 정도로 학습 목적을 고려하면 대략 2 - 3시간 정도를 투자해야 합니다. 분량이 너무 많아 챕터 별로 분할하여 업로드합니다.

목록 사용

아이템 목록 출력은 앱 구성 시 많은 곳에서 사용된다. 아이템을 Row로 구성하고 Column에 쌓아서 리스팅을 구현할 수 있을 것이라 간단하게 예상할 수 있다. Jetpack Compose에서는 리스트 출력에 최적화된 Lazy List를 제공한다.

Column 컴포저블을 사용하여 100개의 목록을 출력하는 컴포저블 함수는 다음과 같이 작성할 수 있다.

@Composable
fun SimpleList() {
    Column {
        repeat(100) {
            Text("Item #$it")
        }
    }
}

다만, Column 컴포저블은 기본적으로 스크롤 속성이 부여되지 않았기에 화면 밖 아이템을 표시할 수 없다. 스크롤이 가능하도록 verticalScroll Modifier를 추가하자.

@Composable
fun SimpleList() {
    // 해당 state로 스크롤 위치를 저장할 수 있으며 코드로 리스트를 스크롤할 수 있게끔 해준다.
    val scrollState = rememberScrollState()

    Column(Modifier.verticalScroll(scrollState)) {
        repeat(100) {
            Text("Item #$it")
        }
    }
}

Lazy List

앞서 작성했듯이 Column 컴포저블을 사용하여 리스트를 출력하면 화면에 표시되지 않는 아이템들도 모두 한번에 렌더링하여 성능 이슈가 발생한다. 뷰에서의 RecyclerView처럼 화면에 등장할 때만 재사용하여 렌더링하는 컴포저블에서는 LazyColumn에 해당한다. LazyColumn은 Column과는 달리 scroll Modifier없이 즉시 사용할 수 있다.

@Composable
fun LazyList() {

    val scrollState = rememberLazyListState()

    LazyColumn(state = scrollState) {
        items(100) {
            Text("Item #$it")
        }
    }
}

Codelab_Jetpack_Compose_layout_019

이미지 출력

원격으로 이미지를 받아서 리스트에 출력해보자. Image 컴포저블을 사용하연 비트맵이나 벡터 이미지를 출력할 수 있다. Coil 라이브러리를 사용하여 Image에 이미지 리소스를 입력하게끔 활용하였다. Coil 라이브러리 사용을 위해 사전 작업이 필요하다.

Build.gradle에 라이브러리를 import 한다.

// build.gradle
implementation 'io.coil-kt:coil-compose:1.4.0'

네트워크를 이용하여 이미지 리소스를 요청할 예정으로 AndroidManifest에 권한을 추가한다.

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />

Row 컴포저블을 사용하여 출력할 아이템을 구성한다. 차례로 이미지, 구분 간격, 텍스트를 정의하였다.

@Composable
fun ImageListItem(index: Int) {
    Row(verticalAlignment = Alignment.CenterVertically) {

        Image(
            painter = rememberImagePainter(
                data = "https://developer.android.com/images/brand/Android_Robot.png"
            ),
            contentDescription = "Android Logo",
            modifier = Modifier.size(50.dp)
        )
        Spacer(Modifier.width(10.dp))
        Text("Item #$index", style = MaterialTheme.typography.subtitle1)
    }
}

LazyColumn 컴포저블의 하위 컴포저블로 ImageListItem 컴포저블을 입력한다.

@Composable
fun ImageList() {

    val scrollState = rememberLazyListState()

    LazyColumn(state = scrollState) {
        items(100) {
            ImageListItem(it)
        }
    }
}

Codelab_Jetpack_Compose_layout_020

스크롤 제어

앞서 state를 사용하여 스크롤 위치를 기억하게끔 만들었다. 이걸 직접 코드를 작성하여 수동으로 제어할 수도 있다. 목록 상단과 하단으로 스크롤할 수 있도록 버튼 두 개를 작성한다. 스크롤이 수행되는 동안 중간의 리스트 렌더링 방지를 위해 스크롤 API는 suspend 함수로 구현되어 있다. 따라서, suspend 함수 호출을 위해 코루틴을 생성해야 한다. 컴포즈에서 사용할 수 있도록 rememberCoroutineScope 함수를 사용하여 CorotineScope를 생성한 뒤 해당 scope에서 동작을 수행하면 되겠다.

@Composable
fun ImageList() {
    val listSize = 100 // 지정된 위치로 스크롤할 수 있도록 전체 아이템 개수 fix
    // 스크롤 위치를 기억하는 state
    val scrollState = rememberLazyListState()
    // 스크롤링 액션이 실행될 수 있도록 coroutine scope 저장
    val coroutineScope = rememberCoroutineScope()
    Column {
        Row {
            Button(onClick = {
                coroutineScope.launch {
                    // 0번째 원소가 첫번째 아이템
                    scrollState.animateScrollToItem(0)
                }
            }) {
                Text("Scroll to the top")
            }
            Button(onClick = {
                coroutineScope.launch {
                    // 0좌표 기준에 따라 listSize - 1는 마지막
                    scrollState.animateScrollToItem(listSize - 1)
                }
            }) {
                Text("Scroll to the bottom")
            }
        }
        LazyColumn(state = scrollState) {
            items(listSize) {
                ImageListItem(it)
            }
        }
    }
}

Codelab_Jetpack_Compose_layout_021

Floating Button 사용

앞서 작성한 예제처럼 버튼을 고정적으로 작성하여 스크롤 제어를 구현하는 경우는 많지 않다. 대부분 Floating Button을 작성하여 클릭 시 top position으로 이동하게끔 작성한다. Scaffold를 사용하여 Floating Button을 작성한 뒤 스크롤 이벤트를 정의하였다.

@Composable
fun ImageList() {
    val listSize = 100
    // 스크롤 위치를 기억하는 state
    val scrollState = rememberLazyListState()
    // 스크롤링 액션이 실행될 수 있도록 coroutine scope 저장
    val coroutineScope = rememberCoroutineScope()
    Scaffold(
        floatingActionButton = {
            ExtendedFloatingActionButton(
                icon = { Icon(Icons.Filled.Favorite, contentDescription = null) }, 
                text = { Text("TOP") }, 
                onClick = { 
                    coroutineScope.launch { 
                        // 0좌표는 첫번째 아이템 좌표
                        scrollState.animateScrollToItem(0) 
                    } 
                }
            )
        }
    ) {
        LazyColumn(state = scrollState) {
            items(listSize) {
                ImageListItem(it)
            }
        }
    }
}

Codelab_Jetpack_Compose_layout_022

좀 더 고도화를 한다면 top position이 화면에 렌더링된 경우는 Floating Button을 숨겼다가 scroll이 하단으로 이동하여 top position이 화면에서 사라진 경우 Floating Button을 등장시키는 방법도 있겠다.

참고자료

  1. Google Codelab - Jetpack Compose Layout #6 목록 사용

+ Recent posts