[Android/Compose] Compose에서 Chip(선택 가능한 그룹 버튼) 만들어보기

곽의진·2023년 9월 7일
0

Android

목록 보기
4/17
post-thumbnail
post-custom-banner

오늘은 사이드 프로젝트에서 Compose를 활용하여 Chip 을 직접 구현하는 과정을 설명드리고자 합니다.

✅ 도입부

✓ Chip이란?

Chip은 사용자 인터페이스(UI)에서 작은 정보 그룹을 나타내는 컴포넌트입니다. 보통 해당 컴포넌트의 역할은 다음과 같습니다.

  1. 사용자가 빠르게 정보를 스캔할 수 있도록 도와줍니다.
  2. 이를 통해 사용자는 상태나 옵션, 또는 다른 종류의 데이터를 빠르게 이해할 수 있습니다.

예를 들어, 연락처 정보, 태그, 또는 필터 옵션 등을 나타낼 수 있습니다.

✓ 동작 사항

기본적으로 Row Scope를 이용하여 가로 구성이며, Chip을 클릭할 경우 위 사진처럼 해당 Chip 색깔만 바뀌는 UI 요소입니다.

✅ Compose 로직

Chip을 관리하는 State

먼저 Chip 상태를 관리하기 위한 State로 구성된 data class를 만들어주었습니다. 내부에는 Chip을 식별하고 구성하는 text 와 선택 상태 여부를 나타내는 isSelected를 포함하여 구성했습니다.

isSelected를 MutableState로 구성한 이유는 지속적으로 상태를 UI에 업데이트 해야하기 때문입니다.

data class ChipState(
    var text: String,
    val isSelected: MutableState<Boolean>
)

Chip 디자인을 책임지는 Chip Style

Chip의 디자인의 유연함을 구현하기 위해 selected 값에 따른 색상과 Text Style을 받아오도록 처리하였습니다. 다만 아직 초기 단계이기 때문에 모든 디자인을 커버하지 못한다는 점 참고해주시면 감사하겠습니다.

data class ChipStyle(
    val selectedColor: Color,
    val unselectedColor: Color,
    val chipTextStyle: TextStyle,
    val selectedTextColor: Color,
    val unselectedTextColor: Color,
    val chipModifier: Modifier = Modifier,
)

Chip을 구성하는 Detail

다음으로는 하나의 Chip의 UI를 구성한 Chip Composable fun 입니다. 유연한 Chip 디자인을 외부에서 적용할 수 있도록 파라메터로 구성하였으며, 하나의 Chip의 경우 Chip Group을 통해서만 접근할 수 있어야 되기 때문에 실질적으로 다른 파일에서 접근하지 못하도록 private 접근 제어자로 설정하였습니다.

@Composable
private fun Chip(
    text: String,
    selected: Boolean,
    selectedColor: Color,
    unselectedColor: Color,
    chipTextStyle: TextStyle,
    selectedTextColor: Color,
    unselectedTextColor: Color,
    @SuppressLint("ModifierParameter")
    chipModifier: Modifier,
    modifier: Modifier = Modifier,
    onChipClicked: (String, Boolean) -> Unit,
) {
    Surface(
        color = when {
            selected -> selectedColor
            else -> unselectedColor
        },
        shape = RoundedCornerShape(100.dp),
        modifier = modifier
    ) {
        Text(
            text = text,
            color = when {
                selected -> selectedTextColor
                else -> unselectedTextColor
            },
            style = chipTextStyle,
            modifier = chipModifier
                .clickable { onChipClicked(text, selected) }
        )
    }
}

Surface를 활용하여 외관 디자인을 구성하였으며, Text를 통해 Chip의 text value를 표현할 수 있도록 구성하였습니다.

Chip Group 컴포넌트

실질적으로 Chip을 활용할 수 있는 Chpi Group을 Chpis라는 Composable 함수로 구현하였습니다.
구현 방식은 다음과 같습니다.

  • LazyRow를 활용하여 가로 스크롤이 가능하도록 처리하였습니다.
  • items 를 활용하여 List로 받은 ChipState를 Chip으로 변환하여 Chip Group을 구성하였습니다.
  • 여기서 실은 List를 사용하면 안됩니다 -> Immutable에 속성에 대해 다음 포스팅에서 설명하겠습니다.
@Composable
fun Chips(
    modifier: Modifier = Modifier,
    elements: List<ChipState>,
    chipStyle: ChipStyle,
    onChipClicked: (String, Boolean, Int) -> Unit,
) {
    LazyRow(modifier = modifier) {
        items(elements.size) { idx ->
            Chip(
                text = elements[idx].text,
                selected = elements[idx].isSelected.value,
                selectedColor = chipStyle.selectedColor,
                unselectedColor = chipStyle.unselectedColor,
                chipTextStyle = chipStyle.chipTextStyle,
                selectedTextColor = chipStyle.selectedTextColor,
                unselectedTextColor = chipStyle.unselectedTextColor,
                chipModifier = chipStyle.chipModifier,
                onChipClicked = { content, isSelected ->
                    onChipClicked(content, isSelected, idx)
                }
            )
            Spacer(modifier = Modifier.padding(8.dp))
        }
    }
}

✅ 실제 사용 방법

실제로 Chips를 활용하는 예시를 보여드리겠습니다.

아래 예제에서는 ViewModel 에서 받아온 SnapshotStateList 를 활용해 ChipState의 List를 받아와 Chips 의 elements에 넣어주고 있습니다.

ChipState의 isSelected의 경우 위 설명과 동일하게 즉각적인 UI 업데이트를 위해 MutableState를 활용하였습니다.

Chip이 클릭된 경우 ViewModel에서는 isSelected 값을 true로 변경해주고 이외의 모든 ChipState는 false로 만들어 주며 Chip의 선택됨을 UI에 업데이트합니다.

val chipStyle: ChipStyle = ChipStyle(
    selectedColor = RED_LIGHT,
    unselectedColor = Grey2,
    chipTextStyle = Typography.bodySmall,
    selectedTextColor = Main100,
    unselectedTextColor = Grey7,
    chipModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
)

@Composable
fun SaveScreen(
    viewModel: SaveScreenViewModel = hiltViewModel(),
) {
    val viewState by viewModel.viewState.collectAsState()

    Scaffold(
        topBar = {
            SaveTopBar()
        }
    ) { innerPadding ->
        Chips(
            modifier = Modifier
                .padding(start = 20.dp)
                .padding(innerPadding),
            elements = viewState.chipElements,
            chipStyle = chipStyle,
            onChipClicked = { _, _, chipIndex ->
                viewModel.setEvent(SaveEvent.OnChipClicked(chipIndex))
            }
        )
        EmptyView(modifier = Modifier.fillMaxSize())
    }
}

// 아래는 ViewModel 로직입니다. (Orbit MVI 사용)
val chipElements: SnapshotStateList<ChipState> = mutableStateListOf(
        ChipState("전체", mutableStateOf(true)),
        ChipState("밥집", mutableStateOf(false)),
        ChipState("카페", mutableStateOf(false)),
        ChipState("술", mutableStateOf(false))
    )
    
override fun handleEvents(event: SaveEvent) {
        when (event) {
            is SaveEvent.OnChipClicked -> {
                updateState {
                    chipElements.mapIndexed { index, chipState ->
                        chipState.isSelected.value = index == event.chipIndex
                    }
                    copy(
                        chipElements = chipElements
                    )
                }
            }
        }
    }

해당 로직은 SDK로 배포하여 누구나 쉽게 쓸 수 있도록 할 예정입니다 많은 관심 부탁드려요🌟

profile
Android Developer
post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 9월 21일

좋은 글 감사합니다 !

답글 달기