Android/Jetpack Compose

[Android] Epoxy를 사용하여 RecyclerView를 쉽게 사용해보자 -3. other layout

Heeg's 2022. 9. 12. 17:57
728x90

본 게시글은 이전 글에 이어서 작성된 부분입니다.

2022.09.01 - [Android/Utility] - [Android] Epoxy를 사용하여 RecyclerView를 쉽게 사용해보자 -1. 기본

2022.09.05 - [Android/Utility] - [Android] Epoxy를 사용하여 RecyclerView를 쉽게 사용해보자 -2. DataBinding

 

이전 게시글에 이어서, 이번엔 Epoxy를 사용하여 흔히 사용하는 layout을 만들어보고자 한다.


이전 게시글에서는 가장 기본적으로 LinearLayout을 사용하여 recyclerView를 구현했었다.

 

val linearlayoutManager = LinearLayoutManager(this)

binding.epoxyRecyclerView.apply {
    layoutManager = linearlayoutManager
    setHasFixedSize(true)
    ...
}

 

이렇게 기본 값으로 layout을 설정하면 세로 방향의 RecyclerView가 생성된다.

 

그렇다면, 가로 방향의 RecyclerView는 어떻게 만들어야 할까?

 

val linearlayoutManager2 = LinearLayoutManager(this)
linearlayoutManager2.orientation = LinearLayoutManager.HORIZONTAL

binding.epoxyRecyclerView2.apply {
    layoutManager = linearlayoutManager2
    setHasFixedSize(true)
    ...
}

 

 

간단하게 orientation 설정을 Horizontal로 명시해주면 된다.

LinearLayout의 orientation 설정의 default 값이 Vertical이기 때문에 기존에는 명시하지 않고 사용했는데, 가로 방향으로 만들고 싶다면 간단히 orientation 설정을 명시하기만 하면 된다.

 

 

이처럼, 가로방향의 RecyclerView를 만들어줄 수 있다.

 

하지만, 코드를 보면 알겠지만 기존 RecyclerView를 사용할 때 처럼 세로 방향의 RecyclerView를 사용하고 그 안에 별도의 가로 방향의 RecyclerView를 추가한 것이 아닌, 2개의 Epoxy RecyclerView를 만들어서 가로, 세로로 구현한 것이다.

 

하나의 EpoxyRecyclerView를 사용하여 구현하는 방법이 있을지도 모르겠으나, 필자가 여러모로 찾아보고 테스트해보았을 때는 하나의 RecyclerView에 두 가지 패턴의 아이템을 넣는 방법은 확인할 수 없었다.

 

두 가지 패턴은 사용하지 못하지만, 비슷한 느낌으로 구현이 가능한 Carousel을 사용하여 원하는 패턴으로 만들 수 있다.

Carousel은 RecyclerView에서 수평으로 고정이 되어 스크롤할 수 있는 아이템을 추가할 수 있는 키워드이다.

 

withModels {
    ...
    
    carousel {
        id("carousel")
        models(List<EpoxyModel>)
    }
}

 

withModels 블럭안에서 사용되는 키워드로, 필수 설정 키워드는 동일하게 id와, EpoxyModel이 들어있는 List를 models에 넣어주면 된다.

 

여기서, EpoxyModel이 들어가야 한다는 부분을 유의해야 한다.

 

dummyData.forEach {
    dataView {
        id("addId")
        title("V : $it")
    }
}

 

기존에 사용할 때는 이처럼 DataBinding된 EpoxyModel을 호출하여 직접 아이템을 넣어주었는데,

 

val dummyModel = dummyData.map {
    ChildViewBindingModel_()
        .id("dummy")
        .subItem("carousel V $it")
}

carousel {
    id("carousel")
    models(dummyModel)
}

 

이와 같이 EpoxyModel을 통해 EpoxyModel List를 만들어 주고, 해당 List를 넣어주어야 한다.

여기서, DataBinding을 사용할 때 처럼 childView { ... } 형태로 List를 만들고자 생각할 수 있겠지만, 그렇게 만들게 된다면 EpoxyModel List가 아니게 된다.

 

 

이처럼 Unit 타입의 List가 만들어지기 때문에, DataBinding을 사용하지 않고 Epoxy에서 만들어주는 DataModel을 사용하여 List를 만들어주어야 한다.

 

이렇게 되는 이유는 간단하다.

기존 EpoxyModel을 호출해서 사용하는 경우, addTo(this)를 마지막에 붙임으로써 아이템을 recyclerView에 추가하게 된다.

하지만, DataBinding에서는 addTo를 명시하지 않아도 자동으로 아이템이 추가되고 해당 Model을 사용하기 때문에 반환되는 값이 없게 된다.

마찬가지로,

 

 

EpoxyModel에 addTo를 선언하게 되면 DataBinding을 사용했을 때와 동일하게 나오는 것을 확인할 수 있다.

 

따라서, EpoxyModel에 addTo 선언을 제외하고 EpoxyModel List를 만들어서 사용하도록 한다.

 

 

 

정상적으로 추가해서 빌드해보면 위와 같이 나오게 된다.

여기서, Carousel은 Horizontal type의 LinearLayout이라기 보다는 ViewPager와 비슷한 느낌을 보인다. 어느 정도 포커스가 들어오게 되면, 자동으로 포커스를 갖는 아이템으로 이동하게 된다.

 

 

Carousel은 위에도 언급했지만,  RecyclerView에서 수평으로 고정이 되어 스크롤할 수 있는 아이템을 추가할 수 있는 키워드이다.

따라서, Horizontal로 설정해둔 LinearLayout에서 추가했을 때는 그대로 가로 스크롤을 할 수 있는 아이템으로 추가되긴 하지만, 정상적으로 동작하지 않는 것을 확인할 수 있다.

 

 

동일하게 5개의 아이템을 Carousel로 추가하였지만 2개의 아이템까지밖에 확인하지 못하였고 위의 이미지처럼 layout에 설정된 아이템들의 위치가 원하는 것처럼 나오지 않는 것을 볼 수 있다.

 

보통, 세로 방향의 RecyclerView에서 가로 RecyclerView를 추가하는 경우는 많지만,

가로 RecyclerView를 사용할 때는 중간에 세로 방향의 RecyclerView를 사용하는 경우는 거의 없기 때문에 이러한 경우에 대해서는 실 사용시 고려하지 않아도 괜찮을 것으로 보이지만, 혹시 몰라 한번 사용해본 부분이다.


다음으로는,

GridLayout을 사용해보자.

 

보통 GridLayout을 사용하는 경우는 원하는 열을 갖는 RecylcerView를 사용하는데, 특정 조건이나 환경에 따라서는 보다 더 많거나, 적은 개수의 아이템을 보여주도록 설정하기 위해 사용한다.

 

val gridLayoutManager = GridLayoutManager(this, 2)
binding.epoxyRecyclerView3.apply {
    layoutManager = gridLayoutManager
    setHasFixedSize(true)

    withModels {
        dummyData.forEach {
            childView {
                id("addId")
                subItem("G2 : $it")
            }
        }
    }
}

 

기본적으로 LinearLayout과 동일한 방식으로 사용할 수 있다. GirdLayoutManager(this, x)에서 x에 한 행에 보여줄 아이템의 개수를 넣어주기만 하면 된다.

위와 같이 선언하면, 한 행에 2개의 아이템이 들어가는 GridLayout을 RecylcerView로 사용하게 된다.

 

 

5개의 아이템을 넣었을 때, 위와 같이 나오게 된다.

한 행에 2개의 아이템이 들어가야하기 때문에, 들어가는 아이템의 width가 match_parent로 되어있어도 5번째 아이템처럼 절반의 크기만 차지하게 된다.

 

그렇다면, 특정 조건에 따라서 한 행에 하나의 아이템만 들어가도록 변경해보도록 하자.

 

binding.epoxyRecyclerView3.apply {
    gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
        override fun getSpanSize(position: Int): Int {
            return if (position == 0) {
                2
            } else {
                1
            }
        }
    }
    ...
}

 

보통 GridLayout을 사용할 때 spanSizeLookup을 사용하여 한 행에 표현할 아이템의 개수를 조절할 수 있다.

이처럼 position이 0일 때, 즉, 첫 번째 아이템일 경우 아이템이 차지하는 사이즈를 2로 변경하여 한 행에 하나의 아이템만 보이도록 변경이 가능한 것이다.

 

하지만, 이렇게 값을 추가하고 빌드를 해보아도 원하는 결과가 정상적으로 나오지 않는다.

override 한 getSpanSize에 로그를 찍어보았을 때, 확인할 수 있는 로그가 나오지 않는것으로 보아 해당 동작이 생각하는 것처럼 되고 있지 않다는 것을 알 수 있었다.

 

spanSize를 변경하는 부분을 적용하기 위한 방법을 찾아보다, spanSizeOverride라는 함수를 epoxy에서 제공하고 있다는 것을 알게 되었다.

Airbnb에서 제공하는 Epoxy Github에서는 찾지 못하였고, stackOverFlow에 관련된 글을 찾아보다가 확인할 수 있던 함수이다.

 

spanSizeOverride { spanSize, index, itemSize ->
    2
}

 

spanSizeOverride에서 사용할 수 있는 3개의 변수는 각각 spanSize, index, itemSize 이다. 정확한 설명을 찾지 못하여 로그를 찍어보면서 어떤 값인지 확인한 것이다.

그리고 반환되는 값은 해당 item의 spanSize를 반환하는 것으로, 위 처럼 2라고 지정하는 경우 하나의 아이템을 2개의 아이템 영역을 차지하도록 한다는 것이다.

 

따라서, spanSizeOverride 함수를 다음과 같이 사용하여 원하는 layout으로 만들어서 사용할 수 있다.

 

withModels {
    dummyData.forEach {
        childView {
            spanSizeOverride { _, index, _ ->
                if (index == 0) {
                    2
                } else {
                    1
                }
            }

            id("addId")
            subItem("G2 : $it")
        }
    }
}

 

 

index에 따라서 다른 크기로 사용하고 싶을 때 이처럼 index 값에 대한 조건문을 추가하여 사용하도록 한다.

 

 

 

위와 같이 사용하는 경우 이처럼 첫 번째 아이템을 2칸을 차지하도록 보여주고, 그 외 값들은 한 행에 2개의 아이템씩 보이도록 구현할 수 있는 것이다.

 

index 별로 같은 layout을 사용하고 아이템의 크기만 변경할 때는 이와 같이 사용하면 되지만,

 

index 별로 다른 layout을 사용하는 경우는 이처럼 사용할 수 있다.

 

withModels {
    dummyData.forEachIndexed { index, it ->
        if (index == 0) {
            dataView {
                spanSizeOverride { _, _, _ -> 2}

                id("addId")
                title("G2 : $it")
            }
        } else {
            childView {
                spanSizeOverride { _, _, _ -> 1}

                id("addId")
                subItem("G2 : $it")
            }
        }
    }
}

 

forEachIndexed를 사용하여 index 값에 따라 다른 layout을 binding하여 사용할 수 있도록 선언하였다.

현재 샘플에서는 dataView와 childView가 동일하게 구현되어있기 때문에 차이를 못느끼겠지만, 각각 별도의 ui를 갖게 구현하게 된다면 좀 더 유용하게 사용할 수 있다는 것을 체감할 수 있을 것이다.

 

 

위에서 선언한 layout들을 모두 사용하게 되면 이처럼 구현이 가능하다.


간단하게 자주 사용하는 layout을 Epoxy에 적용해 보았다.

 

기존 RecyclerView에서 사용하는 것 처럼, innerRecyclerView를 선언한 xml파일을 사용하고, 다른 layout을 사용하는 객체를 여러 개 만들어서 적용해보려고 해 보았지만 어떠한 방법을 사용해도 정상적으로 적용할 수 없었다.

많은 시행착오를 겪었지만, 관련된 내용이 Github에서도 찾을 수 없어서 적용하는 것을 성공하지 못했다는 점이 아쉽다.

 

하지만, GridLayout, Carousel을 사용하면 주로 사용하는 다양한 UI를 구현할 수 있을 것으로 보인다.

간단하게 UI만 그렸기 때문에 별다르게 확인할 부분은 없지만, 디자인을 넣고 조금씩 확장한다면 정말 쉽게 원하는 ui를 그릴 수 있지 않을까 생각한다.

 

Airbnb의 Github에서 확인할 수 있는 내용이 생각보다 적다는 것이 아쉽긴 하지만, 계속해서 업데이트 하면서 다양한 것들을 확장해서 사용할 수 있기 때문에 실 사용할 때는 필요한 부분에 대해 코드를 확인하면서 작업을 진행하면 어찌어찌 쓰는 것에는 문제가 없지 않을까 싶다.

728x90