[Compose] Thinking in Compose
https://developer.android.com/jetpack/compose/mental-model
Jetpack Compose 는 Android 를 위한 선언형 Ui 툴킷이다.
Compose 의 특징에 대해 알아보자.
선언형 프로그래밍 패러다임
선언형 UI 모델로 전환되며 UI 빌드 및 업데이트 와 관련된 엔지니어링이 크게 간소화 되었다.
이 기법은 처음부터 화면 전체를 개념적으로 재생성 한 후 필요한 변경사항만 적용하는 방식이다.
이러한 접근 방식은 stateful 뷰 계층 구조를 업데이트 할 때 복잡성을 방지할 수 있다.
화면 전체를 재생성 하는데 시간, 컴퓨팅 성능, 배터리 등 비용이 많이 든다는 문제가 있다. 이 비용을 줄이기 위해 Compose 는 특정 시점에 UI 의 어떤 부분을 다시 그려야 하는지를 지능적으로 선택한다.
@Composable annotation 으로 일반 함수를 Composable fucntion 으로 만든다.
이 주석으로 함수가 데이터를 UI 로 변환하기 위한 함수라는 것을 Compose 컴파일러에게 알린다.
Composable function 은 UI 위젯을 구성하는 대신 원하는 화면의 상태만 설명하므로 return 값이 필요없다.
또한 동일한 인수로 여러번 호출될때 동일하게 작동하며, 전역 변수같은 값을 사용하지 않는다.
- 선언형 패러다임의 전환
Compose 에서 위젯(View)은 비교적 Stateless 상태이며 getter, setter 함수를 노출하지 않는다.
사실상 위젯은 객체로 노출되지 않고, 다른 Composable function 을 호출하여 UI 를 업데이트 한다.
이렇게 하면 ViewModel 과 같은 아키텍처 패턴에 상태를 쉽게 제공할 수 있다.
- 동적 콘텐츠
Composable function 은 Kotlin 으로 구현되기 때문에 조건문, 반복문, helper 함수 등이 사용가능하여 언어의 유연성을 완전히 활용할 수 있다.
- Recomposition
기존 명령형 UI 모델에서 위젯을 변경하려면 위젯의 setter 를 호출하여 내부 상태를 변경했다.
Compose 에서는 새 데이터를 사용하여 Composable function 을 다시 호출한다.
이때 함수가 재구성(Recomposition) 되며, 필요한 경우 함수에서 내보낸 위젯이 새 데이터로 다시 그려진다.
Compose 프레임워크는 변경된 구성요소만 지능적으로 재구성할 수 있다.
Recomposition 은 입력이 변경될 때 Composable function 을 다시 호출하는 프로세스이다.
새 입력을 기반으로 변경되었을 수 있는 람다만 호출하고 나머지는 호출하지 않는다.
효율적인 Recomposition 을 위해 하지 말아야 할 것들이 있다. 이를 주의하여 구현해야한다.
1) shared object 의 속성 쓰기
2) ViewModel 에서 observable 업데이트
3) sharedPreference 업데이트
Composable fucntion 은 모든 프레임에서 같은 빈도로 재실행 될 수 있다. 애니메이션이 버벅거리는 것을 방지하려면, Compose function 이 빨라야한다. SharedPrefenrence 와 같이 읽고, 쓸 때 비용이 많이 드는 작업은 Background Corountine 에서 작업을 실행하고, 값의 결과를 Composable function 의 매개변수로 전달한다. 다시 한번 말한다. Composable 은 SharedPrefrence 자체에서 읽거나 쓰지 않아야 한다. 대신 Background Coroutine 의 ViewModel 로 읽기 및 쓰기를 이동한다.
@Composable
fun SharedPrefsToggle(
text: String,
value: Boolean,
onValueChanged: (Boolean) -> Unit
) {
Row {
Text(text)
Checkbox(checked = value, onCheckedChange = onValueChanged)
}
}
Compose 에서 프로그래밍 할 때 알아야 할 사항
1. Composable function 은 순서와 관계없이 실행할 수 있다.
2. Composable function 은 동시에 실행할 수 있다.
3. Recomposition 은 최대한 많은 수의 Composable function 및 Lambda 를 건너뛴다.
4. Recomposition 은 최적화 되어있다.
5. Composable function 은 매우 자주 실행될 수 있다.
1. Composable function 은 순서와 관계없이 실행할 수 있다.
Composable function 은 코드가 표시된 순서대로 실행된다고 가정할 수 있으나, 반드시 표기된 순서대로 실행되는 것은 아니다.
때문에 각 함수는 독립적이어야 한다.
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
2. Composable function 은 동시에 실행할 수 있다.
Compose 는 Composable function 를 동시에 실행하여 Recomposition 을 최적화 할 수 있다. 여기서 최적화는 Composable function 가 Background Thread Pool 에서 동시에 실행될 수 있음을 말한다.
Composable function 가 ViewModel 에서 함수를 호출하면 Compose 는 동시에 여러 스레드에서 이 함수를 호출 할 수 있고, Composable function 가 호출될 때 호출하는 곳과 다른 스레드에서 호출될 수 있다.
즉, Composable lambda 변수를 수정하는 코드는 피해야한다. 이런 코드는 thread safety 하지 않으며 Composable lambda 에서 허용되지 않는 부작용이다.
// A코드
@Composable
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count: ${myList.size}")
}
}
// B코드
@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")
}
}
A 코드는 부작용이 없으며 입력 목록을 UI 로 변환하는 코드이다.
B 코드는 함수내에 로컬 변수가 사용되는 경우 이 코드는 thread safety 하지 않다.
이 코드의 items 는 스레드가 다른 Recomposition 을 통해 수정되어서 UI 개수가 잘못 표시된다.
이러한 방식은 금지된다.
3. Recomposition 은 최대한 많은 수의 Composable function 및 Lambda 를 건너뛴다.
매개변수로 인해 Recomposition 될 때 반드시 재구성되어야 하는 Composable function 만 Recomposition 된다.
// 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)
}
}
* 잠깐 : LazyColumn 은 RecyclerView 이고, Items() 에게 전달된 lambda 는 RecyclerView.ViewHolder 와 유사하다.
4. Recomposition 은 최적화 되어있다.
Compose 는 매개변수가 변경되었다고 인지할 때마다 Recomposition 을 실행한다.
Compose 는 Recomposition 이 완료되기 전에 매개변수가 변경되면, 진행하던 Recomposition 을 취소하고, 새 매개변수를 사용하여 Recomposition 을 다시 시작한다.
Recomposition 이 취소되면, Compose 는 UI 트리를 삭제한다. UI 에 문제가 있다면, Recomposition 이 취소된 경우에도 부작용이 나타난다. 이로 인해 앱 상태가 일관되지 않은 문제가 발생할 수 있다. 최적화 된 Recomposition 을 처리할 수 있도록 모든 Composable function, lambda 가 멱등원이고, 부작용이 없는지 확인해야 한다.
5. Composable function 은 매우 자주 실행될 수 있다.
Composable function 은 모든 프레임에서 매우 자주 실행될 수 있다. 때문에 여기서 I/O 작업과 같이 비용이 많이 드는 작업을 한다면 앱 성능에 치명적인 영향을 줄 수 있다.
Composable function 에 데이터가 필요하다면, 데이터 매개변수를 정의해야하고, 그런 다음 비용이 많이 드는 작업을 다른 스레드로 이동한다. mutableStateOf, LiveData 를 사용하여 Compose 에 데이터를 전달 할 수 있다.
- State 문서 읽기
: https://developer.android.com/jetpack/compose/state