드디어 네오-모던 인터랙티브 디자인을 구현할 준비가 되었다! 참고로, 우리가 구축하려고 하는 것은 다음과 같다.

편집모드에서의 목업 디자인

TodoScreen에 state 및 event 전달하기

TodoViewModel에서 이 화면에 필요한 모든 state와 event 정의를 끝냈다. 이제 TodoScreen을 업데이트하여 화면을 표시하는 데 필요한 state와 event를 가져온다.

TodoScreen.kt를 열고 TodoScreen의 메서드 시그니쳐를 변경하여 추가하자.

  • 현재 편집 중인 항목: currentEditing: TodoItem?
  • 3가지 새로운 이벤트들:
    onStartEdit: (TodoItem) -> Unit
    onEditItemChange: (TodoItem) -> Unit
    onEditDone: () -> Unit
// TodoScreen.kt
@Composable
fun TodoScreen(
   items: List<TodoItem>,
   currentlyEditing: TodoItem?,
   onAddItem: (TodoItem) -> Unit,
   onRemoveItem: (TodoItem) -> Unit,
   onStartEdit: (TodoItem) -> Unit,
   onEditItemChange: (TodoItem) -> Unit,
   onEditDone: () -> Unit
) {
   // ...
}

이것들은 ViewModel에서 방금 정의한 새로운 state 및 event다.

PreviewTodoScreen은 시그니쳐가가 변경되어 컴파일되지 않는다.

TodoScreen을 호출하는 부분을 다음과 같이 변경하도록 한다.

TodoScreen(items, null, {}, {}, {}, {}, {})

그런다음 TodoActivity.kt에서 TodoActivityScreen을 호출하는 쪽에 새로운 값들을 전달한다.

// TodoActivity.kt
@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
   TodoScreen(
       items = todoViewModel.todoItems,
       currentlyEditing = todoViewModel.currentEditItem,
       onAddItem = todoViewModel::addItem,
       onRemoveItem = todoViewModel::removeItem,
       onStartEdit = todoViewModel::onEditItemSelected,
       onEditItemChange = todoViewModel::onEditItemChange,
       onEditDone = todoViewModel::onEditDone
   )
}

이것은 새로운 TodoScreen에 필요한 상태와 이벤트를 전달합니다.

컴포저블에 여러가지 인자가 있는 경우, 이름이 지정된 인자를 사용하는 것이 좋다.

인라인 에디터 컴포저블 정의하기

TodoScreen.kt에서 stateless 컴포저블인 TodoItemInput을 사용하여 인라인 에디터를 정의하는 새로운 컴포저블을 만들자.

// 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
)

이 컴포저블은 stateless다. 전달된 아이템만 표시하고 event를 사용하여 상태 업데이트를 요청한다. 이전에 stateless 컴포저블인 TodoItemInput을 추출했기 때문에 이 stateless 문맥에서 쉽게 사용할 수 있다.

copy(task = it) 및 copy(icon = it) 이란 무엇일까

이러한 함수들은 data class에 있는 값들에 대해 코틀린이 자동으로 생성하는 것들이다. copy를 호출하는 것은 변경된 지정된 매개변수와 함께 데이터 클래스의 복사본을 만든다.

이 예제는 stateless 컴포저블의 재사용성을 보여준다. 비록 헤더가 동일한 화면에서 stateful한 TodoItemEntryInput을 사용하지만, 인라인 편집기를 위해 ViewModel까지 state hoisting 할 수 있다.

Stateless 컴포저블은 상태가 저장되는 방식으로부터 분리된다.

이 예제에서, ViewModel내 목록 요소로 TodoItemInlineEditor에 전달된 state를 유지하고 있다. 그러나 TodoItemInlineEditor의 코드를 변경하지 않고도 Room 데이터베이스에 저장되도록 쉽게 변경할 수 있다.

LazyColumn내의 인라인 에디터 사용하기

TodoScreen내 LazyColumn에서 현재 아이템이 편집 중이면 TodoItemInlineEditor를 표시하고, 그렇지 않으면 TodoRow를 표시한다.

또한, 아이템을 클릭할 때 편집을 시작한다. (이전처럼 제거하는 대신)

//TodoScreen.kt

// fun TodoScreen()
// ...
LazyColumn(
   modifier = Modifier.weight(1f),
   contentPadding = PaddingValues(top = 8.dp)
) { 
 items(items) { todo ->
   if (currentlyEditing?.id == todo.id) {
       TodoItemInlineEditor(
           item = currentlyEditing,
           onEditItemChange = onEditItemChange,
           onEditDone = onEditDone,
           onRemoveItem = { onRemoveItem(todo) }
       )
   } else {
       TodoRow(
           todo,
           { onStartEdit(it) },
           Modifier.fillParentMaxWidth()
       )
   }
 }
}
// ...

LazyColumn 컴포저블은 RecyclerView와 동등하다. 현재 화면을 표시하는 데 필요한 목록의 아이템만 재구성하고 사용자가 스크롤할 때 화면을 떠난 컴포저블들을 삭제하고 스크롤되는 요소에 대해 새 컴포저블을 만든다.

LazyColumn은 큰 아이템 목록을 표시하기 위한 것이다.

현재 화면에 있는 아이템만 구성하고 화면을 떠나는 즉시 폐기한다. RecyclerView와 달리 재활용을 할 필요가 없다. 컴포즈는 새로운 컴포저블 생성을 보다 효율적인 방식으로 처리한다.

새 대화형 에디터 시도해보기

앱을 다시 실행하고 todo 행을 클릭하면 대화형 편집기가 열린다.

stateful 헤더와 대화형 편집 환경을 모두 그리기 위해 동일한 stateless UI 컴포저블을 사용하고 있다. 그리고 그렇게 하는 동안 어떠한 중복된 state도 도입하지 않았다.

Add 버튼이 어울리지 않아 헤더를 변경해야 하지만, 이미, 이 기능이 결합되기 시작했다. 다음 단계들에서 디자인을 완료하도록 하자.

이후 단계에서 버튼 디자인을 완료할 것이니. 지금 당장은 “Add” 텍스트가 있는 버튼을 그대로 둘 수 있다.

편집시 헤더 교체하기

다음으로 헤더 디자인을 마치고, 디자이너가 네오-모던 인터랙티브 디자인을 원하는 이모티콘 버튼으로 버튼을 교체하는 방법을 살펴보자.

TodoScreen 컴포저블로 돌아가, 헤더가 에디터 state의 변경 사항에 응답하도록 한다. currentlyEditing이 null이면, TodoItemEntryInput을 표시하고, elevation = true를 TodoItemInputBackground에 전달한다. currentlyEditing이 null이 아닌경우, elevation = false를 TodoItemInputBackground에 전달하고 동일한 배경에 “Editing item”이라는 텍스트를 나타낸다.

// TodoScreen.kt
@Composable
fun TodoScreen(
   items: List<TodoItem>,
   currentlyEditing: TodoItem?,
   onAddItem: (TodoItem) -> Unit,
   onRemoveItem: (TodoItem) -> Unit,
   onStartEdit: (TodoItem) -> Unit,
   onEditItemChange: (TodoItem) -> Unit,
   onEditDone: () -> Unit
) {
   Column {
       val enableTopSection = currentlyEditing == null
       TodoItemInputBackground(elevate = enableTopSection) {
           if (enableTopSection) {
               TodoItemEntryInput(onAddItem)
           } else {
               Text(
                   "Editing item",
                   style = MaterialTheme.typography.h6,
                   textAlign = TextAlign.Center,
                   modifier = Modifier
                       .align(Alignment.CenterVertically)
                       .padding(16.dp)
                       .fillMaxWidth()
               )
           }
       }
      // ..

다시 한번 우리는 재구성시에 컴포즈 트리를 변경하고 있다. 상단 섹션이 활성화되면 TodoItemEntryInput을 표시하고, 그렇지 않으면 “Editing item”을 표시하는 텍스트 컴포저블을 표시한다.

시작 코드에 있던 TodoItemInputBackground는 사이즈 조정과 elevation 변경에 자동으로 애니메이션을 적용한다. 따라서 편집 모드에 들어갈 때, 이 코드는 state들 사이에서 자동으로 애니메이션을 적용한다.

앱 다시 시작하기

앱을 다시 실행하면, 편집 중이 아닌 상태 사이에서 앱이 움직이는 것을 볼 수 있다. 우리는 이 디자인을 거의 완성했다.

다음 섹션에서는 이모티콘 버튼의 코드에 대해 구조화하는 방법을 살펴보자.

카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

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