지난 섹션에서 ViewModelLiveData를 사용하는 단방향 데이터 흐름에 대해서 살펴보았다. 이제는 컴포즈로 옮겨가서 어떻게 단방향 데이터 흐름을 컴포즈에서 ViewModel과 함께 사용하는지 알아보도록 하자.

이전 섹션을 읽지 않은 사람을 위해, 다시 한번 아래의 3가지 용어에 대해서 짚고 넘어가자.

  • State – 시간에 따라 변경 될 수 있는 어떤 값
  • Event – 프로그램 일부에 무슨 일이 생긴 것을 알린다.
  • 단방향 데이터 흐름(Unidirectional data flow) – 이벤트는 위로 state는 아래로 흐르는 설계

이 섹션 마지막에 다음과 같은 화면을 만들게 될 것이다.

TodoScreen 컴포저블 살펴보기

다운로드한 코드는 이 코드랩을 통해 사용하고, 수정할 몇가지 컴포저블을 포함한다.

다운로드한 프로젝트의 컴포저블들은 두개의 파일로 나눠져 있다.

– TodoScreen.Kt : 이 컴포저블들은 직접적으로 state와 상호작용하고, 우리는 컴포즈에서 state를 살펴보므로 이 파일을 수정 할 것이다.

-TodoComponents.kt : 이 컴포저블들은 재사용가능한 몇가지 UI들을 정의한다. 이 UI들은 우리가 TodoScreen을 만드는데 사용한다. 이 코드랩에서 이 컴포저블들을 수정할 필요는 없다.

state상에서 TodoScreen.kt의 코드에 집중하기 위해 임의로 이러한 코드 분리를 했다. 실전에서 이러한 컴포저블들은 같은 파일에서 관리되거나 여러 파일들에 의존하여 사용되기도 한다.

TodoScreen.kt을 열고 내부에 있는 컴포저블들을 살펴보자.

TodoScreen.kt

@Composable
fun TodoScreen(
   items: List<TodoItem>,
   onAddItem: (TodoItem) -> Unit,
   onRemoveItem: (TodoItem) -> Unit
) {
   /* ... */
}

지금 당장은, 앱을 실행해도 아무것도 나타나지 않는다.

이 섹션 이후에, 우리가 TodoActivity로부터 TodoScreen을 호출하는 코드를 작성하도록 하자.

이 컴포저블이 무엇을 나타내려는지 보려면, 안드로이드 스튜디오에 있는 preview pane을 사용하자.

이 컴포저블은 수정할 수 있는 TODO 목록을 보여준다. 하지만 이 목록은 어떤 state도 갖고 있지 않는다. 명심해야 할 것은 state는 변경할 수 있는 어떤 값이다. 하지만 TodoScreen에 있는 어떠한 인자도 수정될 수가 없다.

  • items – 화면에 나타내는 변경할 수 없는 아이템 목록
  • onAddItem – 사용자의 요청에 의해 아이템이 추가 될 때 발생하는 event
  • onRemoveItem – 사용자의 요청에 의해 아이템이 제거 될 때 발생하는 event

사실은 이 컴포저블은 stateless(상태를 유지하지 않는)다. 전달된 아이템 목록만 표시하고, 목록을 직접 수정할 수가 없다. 대신에, 변경을 요청할 수 있는 두 가지 event인 onRemoveItemonAddItem이 전달된다.

stateless 컴포저블은 어떠한 state도 직접적으로 변경할 수 없는 컴포저블을 말한다.

아마 이쯤되면 한가지 궁금한 점이 생길 것이다. 만약 stateless라면 어떻게 수정가능한 목록을 화면에 나타낼까? 그건 바로 state hoisting이라는 기술을 사용하는 것이다. State hoisting(상태 끌어올리기)는 어떤 컴포넌트를 stateless하게 만들기 위해 state를 위로 올리는 패턴을 의미한다. stateless한 컴포넌트들은 테스트 하기 쉽고, 버그가 적게 발생하는 경향이 있으며, 재사용성을 높여준다.

stateless는 호출자가 컴포저블 밖으로 state를 끌어올릴 수 있도록 하기 위해 몇가지 매개변수의 조합을 사용한다. 이게 어떻게 동작하는지 살펴보려면, 이전 섹션의 composable의 UI 업데이트 순환을 살펴보자.

  • Event – 사용자가 아이템을 추가 또는 삭제 요청할 때, TodoScreen에서는 onAddItem 또는 onRemoveItem을 호출 한다.
  • Update state – TodoScreen의 호출자는 state를 업데이트 하는 것으로 이러한 이벤트에 반응한다.
  • Display state – state가 업데이트 될 때, TodoScreen이 새로운 항목과 함께 다시 호출되고, 그것들을 화면에 나타낸다.

호출자는 state를 어떻게 관리하고, 그리고 어디에 관리해야 하는지에 대한 책임이 있다. 예를들어 호출자는 메모리 또는 데이터베이스에 저장하고 불러올 수 있다. TodoScreen은 state가 어떻게 관리되느냐라는 부분에서는 완벽히 분리되어있다.

State hoisting은 컴포넌트를 stateless하게 만들기 위해 state를 위로 옮기는 패턴이다.

컴포저블에 이를 적용할 때, 두개의 매개변수를 컴포저블에 도입한다.

· value:T – 보여주기 위한 현재

· onValueChange:(T) -> Unit – 값을 변경하기 위해 요청하는 이벤트, 여기서 T는 제안된 새로운 값

TodoActivityScreen 컴포저블 정의하기

TodoViewModel.kt를 열고 state 변수 하나랑 이벤트 두개를 선언하고 있는 기존 ViewModel을 찾아보자.

class TodoViewModel : ViewModel() {

   // state: todoItems
   private var _todoItems = MutableLiveData(listOf<TodoItem>())
   val todoItems: LiveData<List<TodoItem>> = _todoItems

   // event: addItem
   fun addItem(item: TodoItem) {
        /* ... */
   }

   // event: removeItem
   fun removeItem(item: TodoItem) {
        /* ... */
   }
}

TodoScreen에서 state를 끌어올리기 위해 이 ViewModel을 사용한다. 끝날 때, 단방향 데이터 흐름 설계가 다음과 같이 만들어져 있을 것이다.

TodoActivity.kt를 열고 TodoScreenTodoActivity 내에 통합하는 것으로 시작한다. 그리고 새로운 @Composable 함수TodoActivityScreen(todoViewModel:TodoViewModel)을 정의하고 onCreatesetContent에서 이를 호출하도록 하자.

이 섹션의 나머지에서는 한번에 하나씩 TodoActivityScreen을 만들어 보도록 하자. 아래에 나올 가짜 state 및 event들과 함께 TodoScreen을 호출해보자.

class TodoActivity : AppCompatActivity() {

   private val todoViewModel by viewModels<TodoViewModel>()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           StateCodelabTheme {
               Surface {
                   TodoActivityScreen(todoViewModel)
               }
           }
       }
   }
}

@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
   val items = listOf<TodoItem>() // 다음 단계에서 이를 완성시키자!
   TodoScreen(
       items = items,
       onAddItem = { }, // 다음 단계에서 이를 완성시키자!
       onRemoveItem = { } // 다음 단계에서 이를 완성시키자!
   )
}

StateCodelabTheme 과 Surface란 무엇인가?

컴포즈 프로젝트로 시작할 때 안드로이드 스튜디오에서 기본적으로 생성해주는 테마다. 컴포즈에서 테마를 다루는 법에 대해서 더 많은 정보를 알고 싶다면, 컴포즈의 테마 설정을 확인하자.

Surface는 앱에 배경을 추가하고 텍스트 색상을 설정한다.

TodoActivityScreen 컴포저블은 ViewModel에서 저장된 state 및 프로젝트에 이미 정의된 TodoScreen 컴포저블 사이를 이어주는 다리 역할을 한다. ViewModel을 직접적인 인자로 갖는 TodoScreen 함수로 변경할 수도 있다. 하지만 그렇게 되면, TodoScreen의 재사용성이 떨어지게 된다. List<TodoItem>과 같은 단순한 매개변수를 사용하면, state가 끌어올려진 특정한 곳에 TodoScreen이 결합되지 않는다.

만약 app을 즉시 실행하면, 버튼 하나를 볼수 있게 되지만 클릭은 아무 동작을 하지 않는다. 왜냐면 아직 ViewModel을 TodoScreen에 연결하지 않았기 때문이다.

Event 올리기

이제 ViewModel, 다리 역할을 하는 TodoActivityScreen 컴포저블 및 TodoScreen, 우리가 필요로 하는 모든 컴포넌트를 준비했다. 모든 것들을 엮고 단방향 데이터 흐름을 사용해서 동적인 목록을 보여주도록 하자.

TodoActivityScreen에서 addItem 및 removeItem을 ViewModel로 부터 전달한다.

// TodoActivity.kt
@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
   val items = listOf<TodoItem>()
   TodoScreen(
       items = items,
       onAddItem = { todoViewModel.addItem(it) },
       onRemoveItem = { todoViewModel.removeItem(it) }
   )
}

TodoScreen에 전달된 event는 코틀린 람다 문법을 사용한다. 코틀린의 람다에 대해서 더 많이 알고 싶다면, 코틀린 문서를 확인하자.

TodoScreenonAddItem 또는 onRemoveItem을 호출 할 때, 우리는 이 호출을 ViewModel 상에 올바른 event로 전달할 수 있다.

코틀린 깨알 정보

메서드 참조 문법을 사용하는 단일 메서드를 호출하여 람다를 생성할수도 있다. 이는 메서드 호출 바깥에서 람다를 생성한다. 위에서 메서드 참조 문법을 사용하는 onAddItemonAddItem = todoViewModel::addItem으로도 표현할수 있다.

State를 아래로 전달하기

단방향 데이터 흐름의 이벤트를 연결했다. 이제 우리는 state를 아래로 전달할 필요가 있다.

TodoActivityScreen을 수정하여 observeAsState를 사용하는 todoItems LiveData를 수정하자.

// TodoActivity.kt
@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
   val items: List<TodoItem> by todoViewModel.todoItems.observeAsState(listOf())
   TodoScreen(
       items = items,
       onAddItem = { todoViewModel.addItem(it) },
       onRemoveItem = { todoViewModel.removeItem(it) }
   )
}

observeAsState 이 한줄은 LiveData를 관찰하고 현재 값을 즉시 List<TodoItem>으로 사용하게 한다.

이 한줄에 많은 내용이 포함되어있는데, 하나씩 분석해보자.

  • val items: List<TodoItem>List<TodoItem> 타입의 items 변수를 선언한다.
  • todoViewModel.todoItemsViewModel에서의 LiveData<List<TodoItems>>
  • .observeAsStateLiveData<T>를 관찰하고 이를 State<T> 객체로 변환한다. 그래서 컴포즈가 변경된 값에 반응할 수 있다.
  • listOf()LiveData가 초기화 되기 전, 가능한 null 결과를 회피하기 위해 사용하는 초기값이다. items를 전달하지 않으면, items는 nullable한 List<TodoItem>? 타입이 된다.
  • by는 코틀린의 property delegate 문법이다. 이는 State<List<TodoItem>>observeAsState를 통해 자동으로 언래핑하여 일반적인 List<TodoItem>로 만들어 준다.

observeAsState는 LiveData를 관찰하고, LiveData가 변경될 때마다 업데이터되는 State객체를 반환한다.

컴포저블이 컴포지션으로부터 제거될 때, 자동으로 관찰을 중단한다.

앱 다시 실행하기

앱을 다시 시작하고 동적으로 업데이트 되는 목록을 볼 수 있다. 하단의 버튼을 클릭하면 새로운 아이템이 추가되고, 아이템을 클릭하면 제거된다.

이 섹션에서는 컴포즈와 ViewModel을 사용하여 단방향 데이터 흐름 설계를 어떻게 만드는지 살펴보았다. 또한 stateless 컴포저블 사용법에 대해 알아보았다. 이를 통해 state hoisting(상태 끌어올리기)이라 불리는 기술을 사용하고 stateful한 UI를 화면에 나타냈다.

그리고 state 및 event 용어측면에서 동적인 UI에 대해 생각하는 방법에 대해 알아보았다.

다음 섹션에서는 내부적으로 state를 컴포저블 함수에 추가하는 방법에 대해서 살펴본다.

카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

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