지난 섹션에서 컴포저블에 state를 추가하는 방법과 컴포저블이 사용하는 state를 stateless로 만들기 위해 state hoisting을 하는 방법에 대해서 배웠다.

이제 state기반의 동적인 UI를 만들는 방법을 알아보자. 디자이너의 목업 디자인으로 다시 보면, 텍스트가 공백이 아닐 때마다 icon행을 보여줘야 한다.

Todo input(state: expadned – 텍스트 있음)
Todo input(state: collapsed – 텍스트 없음)

상태로부터 iconVisible 얻기

TodoScreen.kt 을 열고 새로운 state 변수를 생성하여 현재 선택된 icon을 저장하도록 하자. 그리고 새로운 val iconsVisible은 텍스트가 비어있지 않을 때마다 true가 된다.

// TodoScreen.kt
@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, setText) = remember { mutableStateOf("") }
   val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
   val iconsVisible = text.isNotBlank()
    // ...

icon이라는 두번째 state조각을 추가하여 현재 선택된 아이콘을 저장하도록 했다.

iconVisible 값은 TodoItemInput에 새로운 상태로 추가하지 않는다. TodoItemInput이 직접적으로 이를 변경할 방법은 없다. 대신에, 전적으로 text 값을 기반으로 한다. 재구성 시 text 값이 무엇이든지간에, iconVisible은 적절히 설정되고 우리는 그걸 사용하여 올바른 UI를 보여주면 된다.

아이콘이 표시되는 시점을 제어하기 위해 TodoItemInput에 또 다른 상태를 추가할 수 있지만 스펙을 자세히 살펴보면, 가시성은 전적으로 입력된 텍스트를 기반으로 하고 있다. 두 가지 상태를 만들면 이들을 동기화하기 쉽지 않다.

대신, 우리는 Single source of truth를 갖는 것을 선호한다. 이 컴포저블에서는 state가 될 text만 필요하고, iconsVisibletext 기반으로 결정된다.

함수적인 변환에 익숙하다면 iconsVisible은 text 값에서 매핑된다.

LiveData로 동일한 변환을 수행하기 위해 map 함수를 사용한다.

val iconVisible: LiveData<Boolean> = textLiveData.map { it.isNotBlank() }

iconsVisible의 값에 따라 AnimatedIconRow를 표시하도록 TodoItemInput을 계속 편집하자. iconsVisible이 true이면 AnimatedIconRow를 표시하고, false이면 16.dp가 있는 Spacer를 표시한다.

// TodoScreen.kt
@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, setText) = remember { mutableStateOf("") }
   val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
   val iconsVisible = text.isNotBlank()
   Column {
       Row( /* ... */ ) {
           /* ... */
       }
       if (iconsVisible) {
           AnimatedIconRow(icon, setIcon, Modifier.padding(top = 8.dp))
       } else {
           Spacer(modifier = Modifier.height(16.dp))
       }
   }
}

재구성은 새로운 데이터를 기반으로 컴포지션트리의 구조를 변경한다.

여기에서 우리는 Spacer에 대한 AnimatedIconRow를 교체한다. Navigation 컴포저블에서와 같이 전체 트리를 변경할 수도 있다.

앱을 다시 시작해보면, 텍스트 입력을 시작할 때 아이콘 애니메이션이 시작되는 것을 볼 수 있다.

여기서 iconVisible 값을 기반으로 동적으로 컴포지션 트리를 변경하고 있다. 여기 있는 컴포지션 트리의 다이어그램은 iconVisibles의 state에 따른 내용을 보여준다.

조건부로 보여주는 이런 형식의 로직은 안드로이드 View 시스템에서 visibility를 gone으로 설정하는 것과 동등하다.

iconVisible이 변경될 때 TodoItemInput 컴포지션 트리

컴포즈에서는 “visibility” 속성이 존재하지 않는다.

컴포즈는 동적으로 구성을 변경할 수 있기 때문에, visibility를 gone으로 설정할 필요가 없다. 대신에, 컴포지션으로부터 컴포저블을 제거하면 된다.

앱을 다시 시작 하면 icon 행이 올바르게 표시되는 것을 볼수 있지만 “Add”를 클릭해도 선택한 아이콘이 todo 행으로 추가되지 않는다. 이유는 새 아이콘 상태를 전달하도록 이벤트를 업데이트 하지 않았기 때문이다.

아이콘을 사용하도록 이벤트 업데이트하기

TodoItemInput에 있는 TodoEditButton을 수정하여 새로운 icon 상태를 onClick 리스너에서 사용하도록 하자.

// TodoScreen.kt
TodoEditButton(
   onClick = {
       onItemComplete(TodoItem(text, icon))
       setIcon(TodoIcon.Default)
       setText("")
   },
   text = "Add",
   modifier = Modifier.align(Alignment.CenterVertically),
   enabled = text.isNotBlank()
)

onClick 리스너에서 직접 새 icon 상태를 사용할 수 있다. 또한 사용자가 TodoItem 입력을 완료하면 기본값으로 재설정한다.

지금 앱을 실행하면 애니메이션 버튼과 함께 상호작용 가능한 todo 입력이 표시된다. Great job!

ImeAction과 함께 디자인 끝내기

디자이너에게 앱을 보여주면, todo를 추가하는 동작이 키보드에 있는 ime action으로부터 되었으면 한다고 말한다. 그건 바로 오른쪽 하단에 있는 파란 버튼이다.

안드로이드 키보드에 있는 ImeAction.Done

TodoInputText를 사용하면 onImeAction 이벤트로 imeAction에 응답할 수 있다.

우리는 onImeActionTodoEditButton과 똑같은 행동을 하기를 정말로 원한다. 코드를 복제할 수는 있지만 시간이 지남에 따라 유지 관리하기 어렵고, event 중 하나만 업데이트하는 것이 쉽다.

event를 변수로 추출하여 TodoInputTextonImeActionTodoEditButtononClick 둘다 사용할 수 있도록 하자.

TodoItemInput을 다시 편집하여 todo를 추가하는 작업을 수행하는 사용자를 처리하는 새 람다 함수 submit을 선언한다. 그런 다음 새로 정의된 람다 함수를 TodoInputTextTodoEditButton 모두에 전달한다.

@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, setText) = remember { mutableStateOf("") }
   val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
   val iconsVisible = text.isNotBlank()
   val submit = {
       onItemComplete(TodoItem(text, icon))
       setIcon(TodoIcon.Default)
       setText("")
   }
   Column {
       Row(Modifier
           .padding(horizontal = 16.dp)
           .padding(top = 16.dp)
       ) {
           TodoInputText(
               text = text,
               onTextChange = setText,
               modifier = Modifier
                   .weight(1f)
                   .padding(end = 8.dp),
               onImeAction = submit // TodoInputText에게 submit 콜백 전달
           )
           TodoEditButton(
               onClick = submit, // TodoEditButton에게 submit 콜백 전달
               text = "Add",
               modifier = Modifier.align(Alignment.CenterVertically),
               enabled = text.isNotBlank()
           )
       }
       if (iconsVisible) {
           AnimatedIconRow(icon, setIcon, Modifier.padding(top = 8.dp))
       } else {
           Spacer(modifier = Modifier.height(16.dp))
       }
   }
}

원하는 경우, 더 나아가 함수로부터 해당 로직을 추출할 수 있다. 그러나 이 컴포저블은 꽤 괜찮아 보이므로 그냥 두도록 하자.

이것은 Compose의 큰 장점 중 하나다. Kotlin에서 UI를 선언하기 때문에 코드를 분리하고 재사용하는 데 필요한 추상화를 만들 수 있다.

키보드와 함께 이를 다루기 위해서는 TextField 가 제공하는 두가지 매개변수가 있다.

  • keyboardOptions – Done IME action을 보여주는것을 활성화하기 위해 사용된다.
  • keyboardActions – 트리거된 특정 IME 작업에 대한 응답으로 트리거할 작업을 지정하는 데 사용된다. 위의 경우 Done(완료)을 누르면 submit이 호출되고 키보드가 숨겨진다.

소프트웨어 키보드를 제어하기 위해서, 우리는 LocalSoftwareKeyboardController.current를 사용할 것이다.이거는 아직 실험적인 API이기 때문에, @OptIn(ExperimentalComposeUiApi::class) 애노테이션을 함수에 붙여야 한다.

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun TodoInputText(
    text: String,
    onTextChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    onImeAction: () -> Unit = {}
) {
    val keyboardController = LocalSoftwareKeyboardController.current
    TextField(
        value = text,
        onValueChange = onTextChange,
        colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),
        maxLines = 1,
        keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
        keyboardActions = KeyboardActions(onDone = {
            onImeAction()
            keyboardController?.hide()
        }),
        modifier = modifier
    )
}

앱을 다시 시작하여 새로운 아이콘을 시도해보기

앱을 다시 실행하면 text 상태가 변경될 때 아이콘이 자동으로 표시되고 숨겨지는 것을 볼 수 있다. 아이콘 선택을 변경할 수도 있다. “Add” 버튼을 누르면 입력된 값을 기반으로 새로운 TodoItem이 생성되는 것을 볼 수 있다.

지금까지 컴포즈에서의 상태, 상태 끌어올리기 및 상태를 기반으로 동적 UI를 빌드하는 방법에 대해 배웠다.

다음 몇가지 섹션에서는 state와 상호 작용하는 재사용 가능한 컴포넌트를 만드는 방법을 살펴보도록 한다.

카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.