[Android/Kotlin] 리사이클러뷰 FlexboxLayoutManager 사용하기

코코아의 개발일지·2024년 8월 13일
2

Android-Kotlin

목록 보기
31/36
post-thumbnail

✍🏻 요구사항 분석

위 사진처럼 화면의 남은 공간에 따라 아이템을 배치해주어야 하는 필터 화면을 요구받았다.

위젯 하나하나를 만들어 넣자니 옵션의 변동 가능성을 너무 고려하지 못하는 느낌이라
어찌되었건 리사이클러뷰로 만들어야겠다는 생각은 하게 되었다.

어떻게 구현해야할지 고민하던 중, 같은 팀원분께서 flexbox-layout를 추천해 주셨다.

그래서 오늘은 리사이클러뷰에 FlexboxLayoutManager를 적용한 방법에 대해 작성해 보겠다!

💻 코드 작성

1️⃣ flexbox 의존성 추가

dependencies {
    implementation 'com.google.android.flexbox:flexbox:3.0.0'
}

모듈 단위의 Gradle에 flexbox 의존성을 추가해 준다.

2️⃣ 필터 옵션 정의하기

/** 필터 유형 */
enum class FilterType(val order: Int) {
    WITH_WHOM(0), // 누구와
    HOW_MANY(1), // 몇 명과
    HOW_LONG(2), // 며칠 동안
    ROUTE_STYLE(3), // 원하는 스타일
    MEANS_OF_TRANSPORTATION(4), // 이동 수단
}

/** 필터 옵션 */
enum class FilterOption(val filterType: FilterType, val optionName: String) {
    // 누구와
    WITH_ALONE(FilterType.WITH_WHOM, "혼자"),
    WITH_FRIEND(FilterType.WITH_WHOM, "친구와"),
    WITH_LOVER(FilterType.WITH_WHOM, "연인과"),
    WITH_PARTNER(FilterType.WITH_WHOM, "배우자와"),
    WITH_CHILD(FilterType.WITH_WHOM, "아이와"),
    WITH_PARENT(FilterType.WITH_WHOM, "부모님과"),
    WITH_ETC(FilterType.WITH_WHOM, "기타"),
    // 몇 명과
    MANY_ALONE(FilterType.HOW_MANY, "혼자"),
    MANY_TWO(FilterType.HOW_MANY, "2명"),
    MANY_THREE(FilterType.HOW_MANY, "3명"),
    MANY_FOUR(FilterType.HOW_MANY, "4명"),
    MANY_OVER_FIVE(FilterType.HOW_MANY, "5명 이상"),
    // 며칠 동안
    LONG_THE_DAY(FilterType.HOW_LONG, "당일"),
    ...

    companion object {
        // 필터 유형에 해당하는 선택지 리스트 반환
        fun findOptionsByFilterType(type: FilterType): List<FilterOption> {
            return entries.filter { it.filterType == type }
        }

        // 필터 유형 순서에 따라 정렬된 필터 옵션 리스트 반환
        fun getOptionsSortedByFilterType(): List<List<FilterOption>> {
            return FilterType.entries.sortedBy { it.order }.map { type ->
                findOptionsByFilterType(type)
            }
        }
    }
}

변동 가능성을 최대한 고려해 enum class를 만들어 필터 유형과, 해당 필터 유형에 속하는 옵션을 정의해준다.

3️⃣ 옵션 아이템 만들기

아이템 디자인이 굉장히 간단해서 TextView의 배경으로 테두리가 회색인, 원형 형태의 drawable을 넣어주고 옵션 이름을 텍스트에 넣어준다. (데이터바인딩 활용)

4️⃣ xml에 리사이클러뷰 추가하기

생각보다 간단하다. 그냥 레이아웃 매니저로 FlexboxLayoutManager를 설정할 뿐이다.

<androidx.recyclerview.widget.RecyclerView
	android:id="@+id/filter_question1_with_whom_rv"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:layout_marginTop="16dp"
	android:layout_marginEnd="30dp"
	android:overScrollMode="never"
	app:layoutManager="com.google.android.flexbox.FlexboxLayoutManager"
	tools:listitem="@layout/item_filter_option"
	tools:itemCount="3"/>

이런 식으로 리사이클러뷰를 정의해주면

위와 같은 화면이 나온다.
tools:listitem을 통해 대충 적용한 모습을 미리 확인해볼 수 있다. (실제 화면에는 적용X, xml의 Design 탭에서 확인해 보는 용도)

5️⃣ 어댑터 코드 작성

class FilterOptionsRVAdapter: RecyclerView.Adapter<FilterOptionsRVAdapter.ViewHolder>(){

    private var optionList = listOf<FilterOption>()

    @SuppressLint("NotifyDataSetChanged")
    fun addOption(optionList: List<FilterOption>) {
        this.optionList = optionList
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        val binding: ItemFilterOptionBinding = ItemFilterOptionBinding.inflate(
            LayoutInflater.from(viewGroup.context), viewGroup, false
        )
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(optionList[position])
        holder.itemView.setOnClickListener {
            holder.updateSelection(selectedOption)
        }
    }

    override fun getItemCount(): Int = optionList.size

    inner class ViewHolder(val binding: ItemFilterOptionBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(option: FilterOption) {
            binding.option = option
            updateSelection(option)
        }

        fun updateSelection(option: FilterOption) {
            // 옵션 아이템 클릭 여부에 따른 UI 업데이트
        }
    }
}

필터 옵션들을 optionList로 받아 bind에서 추가해주는 코드이다.
일반적인 LinearLayoutManager 등을 사용할 때와 크게 다른 점은 없다.

6️⃣ 리사이클러뷰와 어댑터 연결

class FilterActivity : AppCompatActivity() {
    private lateinit var binding: ActivityFilterBinding

    private val viewModel: FilterViewModel by viewModels()
    private lateinit var adapterList: List<FilterOptionsRVAdapter>
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_filter)

        binding.apply {
            viewModel = this@FilterActivity.viewModel
            lifecycleOwner = this@FilterActivity
        }

        setFilterOptions()
    }
    
    private fun setFilterOptions() {
        val filterOptionList = FilterOption.getOptionsSortedByFilterType() // 필터 유형마다의 옵션 목록을 리스트에 저장
        adapterList = List(filterOptionList.size) { FilterOptionsRVAdapter() } // 리사이클러뷰와 연결할 어댑터 리스트 정의
        binding.apply {
            val recyclerViewList = listOf<RecyclerView>(
                filterQuestion1WithWhomRv,
                filterQuestion2HowManyRv,
                filterQuestion3HowLongRv,
                filterQuestion4RouteStyleRv,
                filterQuestion5MeansOfTransportationRv
            )
            // 리사이클러뷰에 어댑터 연결
            recyclerViewList.forEachIndexed { index, rv ->
                rv.apply {
                    adapter = adapterList[index]
                    layoutManager = FlexboxLayoutManager(context).apply {
                        flexWrap = FlexWrap.WRAP
                        flexDirection = FlexDirection.ROW
                    }
                }
            }
        }
        // FilterType 순서대로 필터 옵션을 어댑터에 추가
        adapterList.forEachIndexed { index, adapter ->
            adapter.addOption(filterOptionList[index])
        }
    }
}

전체 코드는 위와 같다.
여기서 주목해야할 부분은

rv.apply {
	adapter = adapterList[index]
	layoutManager = FlexboxLayoutManager(context).apply {
	flexWrap = FlexWrap.WRAP
	flexDirection = FlexDirection.ROW
	}
}

이 부분이다.
리사이클러뷰의 레이아웃 매니저를 FlexboxLayoutManager로 설정하고, [flexWrap], [flexDirection] 속성을 정의해 준다.

[FlexWrap]

  • WRAP : 현재 라인에 충분한 공간이 없는 경우 다음 라인에 뷰를 배치함
  • NOWRAP : 한 라인 안에서 공간을 나눠가지는 듯함

[FlexDirection]

  • ROW : 아이템이 가로(행) 방향으로 '좌->우' 순으로 배치됨. 행을 다 채웠다면 그 아래 행에 배치함.
  • ROW_REVERSE : 아이템이 오른쪽부터 행 방향으로 배치됨
  • COLUMN : 아이템이 세로(열) 방향으로 '위->아래' 순으로 배치됨
  • COLUMN_REVERSE : 아이템이 아래부터 열 방향으로 배치됨

번외) FlexLayout 속성 테스트

flexWrap = wrap인 경우, flexDirection에 따른 차이

wrap_row rowwrap_rerow row_reversewrap_column columnwrap_recolumn column_reverse

flexWrap = nowrap인 경우, flexDirection에 따른 차이

nowrap_row rownowrap_rerow row_reversenowrap_column column

nowrap, column_reverse인 경우에는 앱이 종료되었다.
FlexboxLayout이 필요한 상황을 생각해보면, 아무래도 flexWrap = wrap, flexDirection = row인 경우가 가장 많을 것 같았다. (내가 구현하려던 것도 이 경우에 속함)

같은 데이터를 넣었음에도 속성을 뭘로 설정하냐에 따라 매우 다른 결과가 나와서 신기했다.

📱 완성 화면


설정한 옵션들이 깔끔하게 잘 표시되는 걸 확인할 수 있다!


📚 참고 자료

profile
안드로이드 개발자를 꿈꾸는 학생입니다

0개의 댓글