본문 바로가기

Android/Jetpack Compose

[Compose] Compose 환경에서 Pull To Refresh를 구현해보자.

728x90

예전에 업무를 진행하다 위에서 아래로 잡아 당기면서 화면을 갱신하는 pull to refresh를 xml 환경에서 구현해 본 경험이 있다.

하지만 이번에는 Compose 환경이다보니 어떻게 해야 하나 잠깐 찾아보았는데,

 

이처럼 해당 기능 자체를 제공해주고 있었다.

 

이번 글에서는 해당 기능을 사용하여 아주 간단하게 Pull to Refresh 기능을 구현해보고자 한다.


우선, 해당 기능은 Material에서 제공하기 때문에 해당 dependency를 추가해 준다.

implementation "androidx.compose.material:material:$material_version"

 

다음으로는,

pull to refresh를 수행하기 위한 값을 선언해 줘야 하는데,

맨 위에 pullRefresh 함수를 보면, State는 PullRefreshState가 필요한 것을 알 수 있다.

 

val pullRefreshState = rememberPullRefreshState(
    refreshing = true or false
    onRefresh = refresh 시 실행 될 코드
)

 

해당 State는 Compose환경에서 위와 같이 선언해 준다.

이미 제공하고 있는 기능이기 때문에, 해당 State를 위한 remember 함수도 제공되고 있는 것을 사용하면 된다.

 

State를 선언했으면,

해당 State를 통해 pullRefresh를 수행시키기만 하면 된다.

 

Box(
    modifier = Modifier
        .fillMaxSize()
        .pullRefresh(pullRefreshState)
        .background(color = Color.White)
) {
    ...
}

 

이처럼 pullRefreshState를 넣어주고,

PullRefreshIndicator(
    refreshing = true or false,
    state = pullRefreshState,
    modifier = Modifier,
)

 

refresh를 위한 UI를 그려주기만 하면  우리가 알고 있는 위에서 아래로 잡아 당기면서 화면을 갱신할 수 있게 된다.

 

일단 이렇게 적용만을 위한 코드를 작성했으니,

실제 예시를 통해 직접 구현을 해보도록 하겠다.


우선, 데이터 부분은 ViewModel에 넣어서 사용하기 때문에 ViewModel을 선언해 주도록 한다.

viewModelScope를 사용할 예정이기 때문에 compose에서 viewModel을 사용할 수 있도록 dependency를 추가해 준다.

implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:$viewmodel_version'

 

다음 viewModel에는 Refresh 시 수행 할 함수를 작성해 준다.

class RefreshViewModel(application: Application) : AndroidViewModel(application) {

    private val _refreshState = mutableStateOf(PullToRefreshState())
    val refreshState: State<PullToRefreshState> = _refreshState

    fun addRefreshItem() {
        viewModelScope.launch {
            _refreshState.value = _refreshState.value.copy(isLoading = true)
            delay(1000L)
            _refreshState.value = _refreshState.value.copy(
                isLoading = false,
                refreshItemList = _refreshState.value.refreshItemList.toMutableList().also {
                    it.add(
                        index = it.size,
                        element = RefreshItem(
                            index = "Index : ${it.size}",
                            randomIntData = Random.nextInt(1, 1000)
                        )
                    )
                }
            )
        }
    }

    ...
}

 

data class RefreshItem(
    val index: String,
    val randomIntData: Int
)

data class PullToRefreshState(
    val isLoading: Boolean = false,
    val refreshItemList: List<RefreshItem> = listOf(
        RefreshItem("Index : 0", 123),
        RefreshItem("Index : 1", 6),
        RefreshItem("Index : 2", 812),
        RefreshItem("Index : 3", 32),
        RefreshItem("Index : 4", 657),
        RefreshItem("Index : 5", 12),
        RefreshItem("Index : 6", 4),
    )
)

 

Data class는 위와 같이 선언해서 사용했으며, addRefreshItem 함수를 통해 List의 마지막에 새로운 아이템을 더하는 형태로 보이게 될 것이다.

 

이렇게 함수를 구현한 다음, 필요한 값을 refreshState에 저장해 주도록 한다.

val pullRefreshState = rememberPullRefreshState(
    refreshing = refreshState.isLoading,
    onRefresh = refreshViewModel::addRefreshItem
)

 

 

isLoading의 값은 addRefreshItem 호출 시 true로 변경됐다가 1초 후 false로 다시 돌아온다.

즉, Refresh 하는 도중에 나오는 UI가 1초 동안 유지된다는 것이다.

 

실무에서 사용할 때는 모든 UI를 불러오거나, api를 모두 호출해서 call back을 받은 후 false로 변경시켜 주겠지만, 

이번 예제에서는 시간이 걸리는 작업이 하나도 없기 때문에 UI를 보여주기 위해 임시로 1초로 지정해 둔 것이다.

 

그다음, 위에서 선언했던 Box의 pullRefresh에 선언한 State를 넣고, UI 부분을 작성해주기만 하면 된다.

PullRefreshIndicator(
    refreshing = refreshViewModel.refreshState.value.isLoading,
    state = pullRefreshState,
    modifier = Modifier.align(Alignment.TopCenter),
    backgroundColor = Color.LightGray,
    contentColor = if (refreshViewModel.refreshState.value.isLoading) Color.Red else Color.Blue,
)

 

해당 compose 함수에 들어가는 매개변수들이 직관적이기 때문에 설명할 것은 많지 않지만, 중요한 부분만 설명하고 넘어가겠다.

 

isLoading을 refreshing에 사용하였는데, 새로고침이 되고 있는 기간은 1초로 data가 갱신되는 도중에 true로 유지시켜 주면 된다.

즉, 실제로 사용할 때는 refresh가 시작되고부터 완료되는 시점을 감지하는 flag 값을 사용하면 된다.

 

contentColor와 같은 경우, 현재 간단한 예제를 만들기 위해서 이 또한 isLoading을 사용해서 색상을 변경해 주었는데, 이렇게 구현하면 실제로는 색상이 살짝 어색하게 된다.

맨 처음에 위에서 아래로 잡아 당길 때 indicator가 나오면서 돌아가게 되는데, 가장 하단으로 내려오기 전에는 데이터가 아직 갱신되기 전이기 때문에 색상이 로딩 전, 후 와 다른 별개의 색상을 보여야 하지만 위의 코드대로 진행하게 된다면 blue > red > blue 순으로 나오게 된다.

따라서 이와 같은 부분에 대해서만 유의해서 작업을 진행하면 될 것이라고 생각한다.

 

끝으로,

해당 기능을 구현하면서 궁금할 법한 2가지에 대해서 설명하고 끝내고자 한다.

 

첫 번째로,

refreshState를 선언할 때, viewModel에 대한 함수만 호출되도록 해두어서 혹시 다른 방식은 어떻게 해야 하나 궁금한 사람이 있을 수 있다.

val samePullRefreshState = rememberPullRefreshState(
    refreshing = refreshState.isLoading,
    onRefresh = {
        coroutineScope.launch {
            refreshViewModel.changeRefreshState(true)
            delay(1000L)
            refreshViewModel.changeRefreshState(false)
            refreshViewModel.updateItem()
        }
    }
)

 

이처럼 onRefresh 내에서 여러 가지 작업을 함께 수행할 수 있다.

위에서 사용된 각 함수는 addRefreshItem 함수의 일부분을 떼내어서 작성한 함수들이다.

 

두 번째로,

pullRefresh를 수행할 때 나오는 UI에 대해서 그 위치를 변경할 수 있는가? 생각할 수 있다.

 

물론, 가능하며 해당 부분은 PullRefreshIndicator의 위치와 modifier를 수정하면 된다.

Box(
    modifier = Modifier
        .fillMaxSize()
        .pullRefresh(pullRefreshState)
        .background(color = Color.White)
)
PullRefreshIndicator(
    ..
    modifier = Modifier.align(Alignment.TopCenter),
    ..
)

 

fillMaxSize의 Box 안에 pullRefreshIndicator가 존재하고, TopCenter로 되어있기 때문에 가장 상단 systembar 하단에서부터 UI가 나오게 된다.

하지만 실무에서 구현을 하다 보면, 해당 위치에서 나오는 것이 아니라 특정 header 하단에서부터 나오는 것을 원할 수 있다.

 

그렇게 구현하기 위해서는 pullRefreshIndicator의 선언 위치를 변경하여 해당 Modifier가 적용되는 부분이 header의 하단으로 되게 하던지, 아니면 padding 같은 값을 넣어 임의로 변경할 수 있다.

 

box의 pullRefresh(state)와 PullRefreshIndicator를 적절히 변경하면 원하는 부분에 UI를 보여줄 수 있을 것이다.


이것으로 간단하게 pull to refresh를 구현하는 방법에 대하여 작성을 해보았다.

이전에 xml을 사용해서 해당 기능을 작성할 때는 생각보다 더 많은 순서의 작업이 필요했던 것으로 기억하는데,

확실히 선언형 UI인 compose로 넘어오고부터 많은 과정들이 생략되고, 코드의 양이 줄어든 것 같다.

 

기본적으로 compose 버전이 올라갈 때마다 많이 사용하는 기능들에 대해서 직접 제공해주는 것들도 늘었고,

여러모로 구현하기가 쉬워진 것 같아서 개발을 진행할 때 마다 놀라울 따름이다.

 

pullRefresh처럼 자주 사용되는 기능들을 내재화시켜서 제공해 주는 것이 또 있을 것 같다는 생각이 드는데,

앞으로는 기능이 필요하면 우선 IDE 내에서 한번 타이핑이라도 해보는 습관을 가져야 할 것 같다.

 

해당 게시글에 사용한 예제는 Github에 올려두었다.

https://github.com/HeeGyeong/ComposeSample

 

GitHub - HeeGyeong/ComposeSample

Contribute to HeeGyeong/ComposeSample development by creating an account on GitHub.

github.com

 

 

728x90