복잡한 UI를 표시하는 stateless 컴포저블에는 많은 매개변수가 포함될 수 있다. 매개변수가 많지 않고, 매개변수가 직접적으로 컴포저블을 설정하는 경우, 이건 괜찮다. 그러나 때로는 하위 컴포저블을 설정하기 위해 매개변수를 전달해야 한다.
우리의 네오-모던 인터렉티브 디자인에서는 디자이너가 Add 버튼을 상단에 유지하고, 인라인 에디터에는 두개의 이모지 버튼으로 교체하기를 원한다. 이 경우를 처리하기 위해 TodoItemInput에 더 많은 매개변수를 추가할 수 있지만, 이것이 실제로 TodoItemInput의 책임인지는 확실하지 않다.
우리에게 필요한 것은 컴포저블이 미리 구성된 버튼 섹션을 가져오는 방법이다. 이렇게 하면 호출자가 버튼을 구성할 수 있지만 TodoItemInput으로 구성하는 데 필요한 모든 state를 공유하지 않고도 버튼을 구성할 수 있다.
이렇게 하면 상태 stateless 컴포저블에 전달되는 매개변수의 수를 줄이고 재사용 가능성을 높일 수 있다.
미리 구성된 섹션을 전달하는 패턴으로 slot이 있다. slot은 컴포저블에 대한 매개변수로, 호출자가 화면의 한 섹션을 형성할 수 있도록 한다. 내장된 컴포저블 API를 통해 slot 예제를 찾을 수 있다. 가장 일반적으로 사용되는 예제중 하나는 Scaffold다.
Scaffold는 화면의 topBar, bottomBar, body와 같은 Material 디자인의 전체 화면을 설명하기 위한 컴포저블이다.
화면의 각 섹션을 구성하기 위해 수백 개의 매개변수를 제공하는 대신 Scaffold는 원하는 컴포저블로 채울 수 있는 slot을 노출시킨다. 이는 Scaffold에 대한 매개변수의 수를 줄이고, 재사용성을 높인다. 커스텀 topBar를 만들고 싶다면 Scaffold가 기꺼이 표현해준다.
@Composable
fun Scaffold(
// ..
topBar: @Composable (() -> Unit)? = null,
bottomBar: @Composable (() -> Unit)? = null,
// ..
bodyContent: @Composable (PaddingValues) -> Unit
) {
Slot은 호출자가 화면의 한 섹션을 표현하도록 하는 컴포저블 함수 매개변수들이다.
slot은 매개변수 타입이 @Composable() -> Unit 과 함께 선언한다.
TodoItemInput 에 slot 정의하기
// TodoScreen.kt
@Composable
fun TodoItemInput(
text: String,
onTextChange: (String) -> Unit,
icon: TodoIcon,
onIconChange: (TodoIcon) -> Unit,
submit: () -> Unit,
iconsVisible: Boolean,
buttonSlot: @Composable () -> Unit
) {
// ...
이것은 호출자가 원하는 버튼으로 채울 수 있는 일반적인 slot이다. 헤더 및 인라인 에디터에서 다른 버튼을 지정하는데 사용할 것이다.
buttonSlot의 내용 보여주기
TodoEditButton에 대한 호출을 slot의 내용으로 교체한다.
// TodoScreen.kt
@Composable
fun TodoItemInput(
text: String,
onTextChange: (String) -> Unit,
icon: TodoIcon,
onIconChange: (TodoIcon) -> Unit,
submit: () -> Unit,
iconsVisible: Boolean,
buttonSlot: @Composable() () -> Unit,
) {
Column {
Row(
Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
) {
TodoInputText(
text,
onTextChange,
Modifier
.weight(1f)
.padding(end = 8.dp),
submit
)
// 새로운 코드: TodoEditButton에 대한 호출을 slot의 내용으로 교체한다
Spacer(modifier = Modifier.width(8.dp))
Box(Modifier.align(Alignment.CenterVertically)) { buttonSlot() }
// 새로운 코드의 끝
}
if (iconsVisible) {
AnimatedIconRow(icon, onIconChange, Modifier.padding(top = 8.dp))
} else {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
buttonSlot()을 직접적으로 호출할 수도 있지만, align을 중앙에 정렬을 유지해야 하기 때문에 Box내에 slot을 배치한다.
Slot을 사용하기 위해 stateful한 TodoItemEntryInput 수정하기
이제 buttonSlot을 사용하도록 호출하는 쪽을 수정해야 한다. 먼저 TodoItemEntryInput을 수정하도록 하자
// TodoScreen.kt
@Composable
fun TodoItemEntryInput(onItemComplete: (TodoItem) -> Unit) {
val (text, onTextChange) = remember { mutableStateOf("") }
val (icon, onIconChange) = remember { mutableStateOf(TodoIcon.Default)}
val submit = {
if (text.isNotBlank()) {
onItemComplete(TodoItem(text, icon))
onTextChange("")
onIconChange(TodoIcon.Default)
}
}
TodoItemInput(
text = text,
onTextChange = onTextChange,
icon = icon,
onIconChange = onIconChange,
submit = submit,
iconsVisible = text.isNotBlank()
) {
TodoEditButton(onClick = submit, text = "Add", enabled = text.isNotBlank())
}
}
buttonSlot은 TodoItemInput의 마지막 매개변수이므로 후행 람다 구문을 사용할 수 있다. 그런 다음 람다에서 이전처럼 TodoEditButton을 호출한다.
Slot을 사용하기 위해 TodoItemInlineEditor 수정하기
리팩터링을 완료하려면 TodoItemInlineEditor도 slot을 사용하도록 변경해야한다.
// TodoScreen.kt
@Composable
fun TodoItemInlineEditor(
item: TodoItem,
onEditItemChange: (TodoItem) -> Unit,
onEditDone: () -> Unit,
onRemoveItem: () -> Unit
) = TodoItemInput(
text = item.task,
onTextChange = { onEditItemChange(item.copy(task = it)) },
icon = item.icon,
onIconChange = { onEditItemChange(item.copy(icon = it)) },
submit = onEditDone,
iconsVisible = true,
buttonSlot = {
Row {
val shrinkButtons = Modifier.widthIn(20.dp)
TextButton(onClick = onEditDone, modifier = shrinkButtons) {
Text(
text = "\uD83D\uDCBE", // floppy disk
textAlign = TextAlign.End,
modifier = Modifier.width(30.dp)
)
}
TextButton(onClick = onRemoveItem, modifier = shrinkButtons) {
Text(
text = "❌",
textAlign = TextAlign.End,
modifier = Modifier.width(30.dp)
)
}
}
}
)
여기서는 buttonSlot을 이름이 있는 매개변수로 전달했다. 그런 다음 buttonSlot에서 인라인 편집기 디자인을 위한 두 개의 Button을 포함하는 Row를 만든다.
앱 다시 시작하기
앱을 다시 실행하고 인라인 편집기를 사용해보자.
자세히 보면 편집기에 들어가고 나갈 때 아이콘 투명도 색상이 변경되는 것을 알 수 있다. 이는 투명도 색상이 TodoRow에 기억되어 편집기를 열 때 제거되었다가 컴포지션에 다시 추가되기 때문이다.
이 색상이 변경되지 않도록 하려면 ViewModel로 state를 hoisting하면된다.
이 섹션에서는 호출자가 화면의 섹션을 제어할 수 있는 slot을 사용하여 stateless 컴포저블을 커스터마이징 했다. slot을 사용하여, 추후에 추가될 수 있는 모든 다른 디자인과 TodoItemInput가 함께 결합(coupling)되는 것을 방지했다.
stateless 컴포저블에 매개변수를 추가하여 하위요소를 커스터마이징하는 경우 slot이 더 나은 디자인인지 평가해보자. slot은 매개변수의 수를 관리 가능한 상태로 유지하면서 컴포저블을 더 재사용할 수 있게 만드는 경향이 있다.
2개의 댓글
이상효 · 2021년 12월 6일 6:33 오후
덕분에 Compose 공부 좀 더 수월하게 하고 있습니다 감사합니다
Charlezz · 2021년 12월 7일 9:07 오전
도움이 되서 기쁩니다 🙂 컴포즈 공부 화이팅!