지금까지 Android 뷰 계층 구조는 UI 위젯의 트리로 표시할 수 있었다. 사용자 상호작용 등의 이유로 인해 앱의 상태가 변경되면, 현재 데이터를 표시하기 위해 UI 계층 구조를 업데이트 해야 한다. UI를 업데이트하는 가장 일반적인 방법은 findViewById()
와 같은 함수를 사용하여 트리를 탐색하고 button.setText(String)
, container.addChild(View)
또는 img.setImageBitmap(Bitmap)과 같은 메소드를 호출하여 노드를 변경하는 것이며 이러한 메소드는 위젯의 내부 상태를 변경한다.
이처럼 뷰를 수동으로 조작하면 오류 발생 가능성이 커진다. 데이터를 여러 위치에서 렌더링한다면 데이터를 표시하는 뷰 중 업데이트를 누락할 수 있다. 또한, 두 업데이트가 예기치 않은 방식으로 충돌할 경우, 잘못된 상태를 야기하기도 쉽다. 일반적으로 업데이트가 필요한 뷰의 수가 많을수록 소프트웨어 유지관리 복잡성은 증가한다.
Compose
는 선언형 UI 모델로 처음부터 화면 전체를 개념적으로 재생성한 후 필요한 변경사항만 적용하는 방식으로 작동한다. 이러한 접근 방식은 Stateful한 뷰 계층 구조를 수동으로 업데이트할 때의 복잡성을 방지할 수 있다.
아래는 name을 받아서 Text를 보여주는 함수이다.
@Composable
fun Greeting(name: String){
Text("Hello $name")
}
많은 명령형 객체 지향 UI 도구 키트를 사용하여 위젯의 트리를 인스턴스화함으로써 UI를 초기화한다. 흔히 XML 레이아웃 파일을 확장하여 이러한 작업을 한다. 각 위젯은 내부 상태를 유지하고 앱 로직이 위젯과 상호작용할 수 있도록 하는 getter / setter 메소드를 노출한다.
Compose의 선언형 접근 방식에서 위젯은 Stateless
상태이며, getter / setter를 노출하지 않는다. 사실상 위젯은 객체로 노출되지 않는다. 동일한 Composable 함수를 다른 인수로 호출하여 UI를 업데이트한다. 이렇게 하면 앱 아키텍처 가이드에 설명된대로 ViewModel과 같은 아키텍처 패턴에 상태를 쉽게 제공할 수 있다. 그런 다음, 컴포저블은 식별 가능한 데이터가 업데이트될 때마다 현재 애플리케이션 상태를 UI로 변환한다.
앱 로직은 최상위의 Composable 함수에 데이터를 제공한다. 그러면 함수는 데이터를 사용하여 다른 컴포저블을 호출함으로써 UI를 형성하고 적절한 데이터를 해당 컴포저블 및 계층 구조 아래로 전달한다.
사용자가 UI와 상호작용할 때 UI는 onClick과 같은 이벤트를 발생시킨다. 이러한 이벤트를 앱 로직에 전달하여 앱의 상태를 변경해야 한다. 상태가 변경되면 Composable 함수는 새 데이터와 함께 다시 호출된다. 이렇게 하면 UI 요소가 다시 그려지게 되고 이 프로세스를 Recomposition
이라고 한다.
사용자가 UI 요소와 상호작용하며 이에 따라 이벤트가 트리거 된다. 앱 로직이 이벤트에 응답한다. 그러면 Composable 함수가 필요한 경우 새 매개변수를 사용하여 자동으로 다시 호출된다.
Composable 함수는 XML이 아닌 Kotlin으로 작성되기 때문에 Kotlin 코드와 마찬가지로 동적일 수 있다. 상당히 정교할 수 있는데, if문을 통해 특정 UI 요소를 표시할지 여부를 결정할 수 있고 for문과 같은 루프를 사용할 수 있으며 기본 언어의 유연성을 완전히 활용할 수 있다. 이러한 성능과 유연성은 Compose의 이점 중 하나이다.
명령형 UI 모델에서 위젯을 변경하려면 setter를 호출하여 내부 상태를 변경한다. Compose에서는 새로운 데이터를 사용하여 Composable 함수를 다시 호출한다. 이렇게 하면 함수가 재구성
되며, 필요한 경우 함수에서 내보낸 위젯이 새 데이터로 다시 그려진다. Compose 프레임워크는 변경된 구성요소만 지능적으로 재구성할 수 있다.
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
버튼이 클릭될 때마다 호출자는 clicks
값을 업데이트 한다. Compose는 Text
함수를 사용해 람다를 다시 호출하여 새 값을 표시한다. 이 프로세스를 재구성
이라고 하며, 값에 종속되지 않은 다른 함수는 재구성되지 않는다.
재구성은 입력이 변경될 때 Composable 함수를 다시 호출하는 프로세스이다. 이는 함수의 입력이 변경될 때 발생한다. Compose는 새 입력을 기반으로 재구성할 때 변경되었을 수 있는 함수 또는 람다만 호출하고 나머지는 건너뛴다. 매개 변수가 변경되지 않은 함수 또는 람다는 모두 건너뜀으로써 Compose의 재구성이 효율적으로 이뤄질 수 있다.
함수의 재구성을 건너뛸 수 있으므로 Composable 함수 실행의 부작용에 의존해서는 안된다. 그렇게 하면 사용자가 앱에서 이상하고 예측할 수 없는 동작을 경험할 수 있다. 부작용은 앱의 나머지 부분에 표시되는 변경사항이다.
Composable 함수는 애니메이션이 렌더링될 때와 같이 모든 프레임에서와 같은 빈도로 재실행될 수 있다. 애니메이션 도중 버벅거림을 방지하려면 Composable 함수가 빨라야 한다. 그래서 비용이 많이 들거나 시간이 오래 걸리는 작업은 백그라운드 코루틴에서 작업을 실행하고 값 결과를 Composable 함수에 매개변수로 전달한다.
Composable은 SharedPreferences에 읽거나 쓰지 않아야 한다. 대신 아래 코드는 백그라운드 코루틴의 ViewModel로 읽기 및 쓰기를 이동하고 앱 로직은 콜백과 함께 값을 전달하여 업데이트를 트리거한다.
@Composable
fun SharedPrefsToggle(
text: String,
value: Boolean,
onValueChanged: (Boolean) -> Unit
) {
Row {
Text(text)
Checkbox(checked = value, onCheckedChange = onValueChanged)
}
}
Composable 함수는 코드가 반드시 표시된 순서대로 실행되는 것은 아니다. Composable 함수에 다른 Composable 함수 호출이 포함되어 있다면 그 함수는 순서와 관계 없이 실행될 수 있다. Compose에는 일부 UI 요소가 다른 UI 요소보다 우선순위가 높다는 것을 인식하고 그 요소를 먼저 그리는 옵션이 있다.
Compose는 Composable 함수를 동시에 실행하여 재구성을 최적화할 수 있다. 이를 통해 Compose는 다중 코어를 활용하고 화면에 없는 Composable 함수를 낮은 우선순위로 실행할 수 있다.
이 최적화는 Composable 함수가 백그라운드 스레드 풀 내에서 실행될 수 있음을 의미한다. Composable 함수가 ViewModel에서 함수를 호출하면 Compose는 동시에 여러 스레드에서 이 함수를 호출할 수 있다.
애플리케이션이 올바르게 작동하도록 하려면 모든 Composable 함수에 부작용이 없어야 한다. 대신 UI 스레드에서 항상 실행되는 onClick과 같은 콜백에서 부작용을 트리거한다.
Composable 함수가 호출될 때 호출자와 다른 스레드에서 호출이 발생할 수 있다. 즉, Composable 람다의 변수를 수정하는 코드를 피해야 한다. 이러한 코드는 스레드로부터 안전하지 않을 뿐만 아니라 Composable 람다의 허용되지 않는 부작용이기 때문이다.
[Good 👍]
@Composable
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count: ${myList.size}")
}
}
[Bad 👎]
@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
var items = 0
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
items++ // Avoid! Side-effect of the column recomposing.
}
}
Text("Count: $items")
}
}
이 코드에서 items는 모든 재구성을 통해 수정된다. 수정은 굉장히 빈번하게 호출될 수 있으며, UI에 잘못된 갯수가 표시될 수 있다. 이러한 쓰기는 Compose에서 지원되지 않는다.
UI의 일부가 잘못된 경우, Compose는 업데이트해야 하는 부분만 재구성하기 위해 최선을 다한다. 즉, UI 트리에서 위 또는 아래에 있는 컴포저블을 실행하지 않고 단일 버튼의 컴포저블을 다시 실행하는 것을 건너뛸 수 있다.
모든 Composable 함수 및 람다는 자체적으로 재구성할 수 있다.
/**
* Display a list of names the user can click with a header
*/
@Composable
fun NamePicker(
header: String,
names: List<String>,
onNameClicked: (String) -> Unit
) {
Column {
// this will recompose when [header] changes, but not when [names] changes
Text(header, style = MaterialTheme.typography.h5)
Divider()
// LazyColumn is the Compose version of a RecyclerView.
// The lambda passed to items() is similar to a RecyclerView.ViewHolder.
LazyColumn {
items(names) { name ->
// When an item's [name] updates, the adapter for that item
// will recompose. This will not recompose when [header] changes
NamePickerItem(name, onNameClicked)
}
}
}
}
/**
* Display a single name the user can click.
*/
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}
이러한 각 범위는 재구성 도중에 실행할 유일한 사항일 수 있다. Compose는 header
가 변경될 때 상위 요소 중 어느것도 실행하지 않고 Column
람다로 건너뛸 수 있다. 그리고 Column을 실행할 때 Compose는 names가 변경되지 않았다면 LazyColumn의 항목을 건너뛰도록 선택할 수 있다.
모든 Composable 함수 또는 람다를 실행하는 작업에는 부작용이 없어야 한다. 부작용을 실행할 때는 콜백에서 부작용을 트리거 해야 한다.
Compose가 컴포저블의 매개변수가 변경되었을 수 있다고 생각할 때마다 재구성이 시작된다. 재구성은 낙관적이다.
즉, Compose는 매개변수가 다시 변경되기 전에 재구성을 완료할 것으로 예상한다. 재구성이 완료되기 전에 매개변수가 변경되면 Compose는 재구성을 취소하고 새 매개변수를 사용하여 재구성을 다시 시작할 수 있다.
재구성이 취소되면 Compose는 재구성에서 UI 트리를 삭제한다. 표시되는 UI에 종속되는 부작용이 있다면 구성이 취소된 경우에도 부작용이 적용된다. 이로 인해 일관되지 않은 앱 상태가 발생할 수 있다.
낙관적 재구성을 처리할 수 있도록 모든 Composable 함수 및 람다가 멱등원이고 부작용이 없는지 확인해야 한다.
경우에 따라 Composable 함수는 UI 애니메이션의 모든 프레임에서 실행될 수 있다. 함수가 IO 작업과 같은 비용이 많이 드는 작업을 실행하면 이로 인해 UI 버벅거림이 발생할 수 있다. (ANR)
Composable 함수에 데이터가 필요하다면 데이터의 매개변수를 정의해야 한다. 그런 다음, 비용이 많이 드는 작업을 Compose 외부의 다른 스레드로 이동하고 mutableStateOf 또는 LiveData를 사용하여 Compose에 데이터를 전달할 수 있다.