2021. 10. 28. 00:49ㆍAndroid/AAC
- Compose : 선언형 UI 툴킷으로, 처음부터 화면 전체를 개념적으로 재생성한 후 필요한 변경사항만 적용하는 방식으로 작동
- Compose 특징
- @Composable 주석을 이용하여 Compose Compiler에게 UI로 반환되는 요소임을 알림
- 함수의 파라미터로 필요한 data를 받을 수 있음 => 동적인 처리가 가능(not xml, written in kotlin)
- 함수 내부에서 다른 Compose 함수를 불러 UI 계층화를 이룸
- 반환값이 없음
- 빠르고, 부수효과가 없이 멱등적(input이 같으면 output도 동일) => 즉, random() 혹은 global 변수 변경 작업 등을 포함하지 않아야 함
React와 유사한 UI 패러다임으로 계층 구조를 따라 data는 아래 방향을 따라 event는 위로 이동
event에 의해 새로운 state가 되면 자동적으로 새로운 data를 가진 composable이 호출됨(Recomposition)
- Recomposition : 변화된 데이터가 적용된 곳에서만 일어남. 데이터를 읽거나 쓰는 작업은 오래걸리므로 viewmodel에서 백그라운드로 작업하게 하고 파라미터로 해당 값을 넘겨받고 event를 등록해 Recomposition이 작동하도록 하는 로직으로 만들어야함
- composable 함수는 병렬적으로 진행(composable은 해당 함수를 호출한 스레드와 다른 스레드에서 실행)되며 순차적으로 실행을 보장하지 않는다
- state가 변해서 영향이 있는 composable만 Recomposition한다
- recomposition이 끝나기 전에 state가 변화가 있을 경우, 해당 recomposition은 취소되는데 이때 UI tree를 버림
- composable 함수는 호출이 잦아서(특히 animation 동작시 매 프레임마다 호출됨) 데이터를 읽거나 쓰는 작업과 같이 비싼 작업은 composition 밖에서 다른 스레드에서 작동하도록 하고 parameter로 data를 넘겨받는다
- Composable state 관리
1. remember : Recomposition 시 상태는 유지되지 않기 때문에 초기 composition 때 값을 저장하고 싶으면 remember를 이용(configuration change에는 rememberSaveable 이용)
- val mutableState = remember { mutableStateOf(default) }
- var value by remember { mutableStateOf(default) }
- val (value, setValue) = remember { mutableStateOf(default) }
2. livedata, flow, rxjava2 형태의 데이터 저장도 지원하지만 recomposition시에는 State<T> 형태를 관찰하므로 다른 형태의 데이터를 읽을 때 State<T>로의 형변환이 필요
3. state hoisting으로 stateless composable을 만들어 재사용성을 높인다
4. bundle에 상태 저장하기 위한 방법
- @parcelize : 주석을 붙이고, 반환형은 Parcelable
@Parcelize
data class City(val name: String, val country: String) : Parcelable
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
- MapSaver
- ListSaver
5. state가 복잡해지고 로직이 필요한 경우, state holder를 이용(plain object와 viewmodel 두개를 동시에 state holder로 이용하기도 함, 이 때 필요하다면 plain state holder에서 viewmodel을 참조해도 됨(수명주기가 더 짧기 때문에 memory leak이 없기 때문에))
- 간단한 ui element state, ui logic : Composable 함수를 state holder로 이용
- 복잡한 ui element state, ui logic : plain object를 state holder로 이용
- screen or ui state, business logic : viewmodel을 state holder로 이용(viewmodel은 수명주기가 길기 때문에 screen-level composable의 상태를 담당하는 것만 하는 것이 권장됨)
private class ExampleState(
val lazyListState: LazyListState,
private val resources: Resources,
private val expandedItems: List<Item> = emptyList()
) { ... }
@Composable
private fun rememberExampleState(...) { ... }
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val uiState = viewModel.uiState
val exampleState = rememberExampleState()
LazyColumn(state = exampleState.lazyListState) {
items(uiState.dataToDisplayOnScreen) { item ->
if (exampleState.isExpandedItem(item) {
...
}
...
}
}
}
6. 리스트가 제공되는 composable의 smart recomposition을 돕는 key composable(LazyColumn과 같은 일부 composable은 자체적으로 key composable을 내장해 지원)
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
key(movie.id) { // Unique ID for this movie
MovieOverview(movie)
}
}
}
}
7. stable한 파라미터만이 smart recomposition의 트리거가 될 수 있음(primitive types, String, lamda, MutableState) => 만약, stable로 간주되지 않는 객체형을 사용하고 싶다면 @Stable 처리(ex>인스턴스)
// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
val value: T?
val exception: Throwable?
val hasError: Boolean
get() = exception != null
}
- side effect 다루기
반응형 UI는 본질적으로 비동기이며 Jetpack Compose는 콜백을 사용하는 대신 API 수준에서 코루틴을 이용하여 이를 해결
1. LaunchEffect : composable 안에서 suspend 함수를 안전하게 호출(키 값(넘겨진 파라미터)가 변경되면 해당 LaunchEffect Composable이 취소되고(이에 따라 suspend 함수도 중단) 새로운 suspend 함수가 호출됨)
2. rememberCoroutineScope : composable 밖에서 suspend 함수를 호출(ex> 이벤트 핸들러 내에서 호출)
@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
Column {
/* ... */
Button(
onClick = { // composable 내에서 호출되는게 아니라 onClick 람다에서 호출
// Create a new coroutine in the event handler
// to show a snackbar
scope.launch {
scaffoldState.snackbarHostState
.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
3. rememberUpdateState : 업데이트 상태에 따라 coroutine 실행 결정(경우에 따라 효과에서 값이 변경되면 효과를 다시 시작하지 않을 값을 캡처할 수 있습니다. 이렇게 하려면 rememberUpdatedState를 사용하여 캡처하고 업데이트할 수 있는 이 값의 참조를 만들어야 합니다. 이 접근 방식은 비용이 많이 들거나 다시 만들고 다시 시작할 수 없도록 금지된 오래 지속되는 작업이 포함된 효과에 유용합니다.)
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
// This will always refer to the latest onTimeout function that
// LandingScreen was recomposed with
val currentOnTimeout by rememberUpdatedState(onTimeout)
// Create an effect that matches the lifecycle of LandingScreen.
// If LandingScreen recomposes, the delay shouldn't start again.
LaunchedEffect(true) { // 호출한 composable과 같은 생명주기를 가지기 위해 상수를 매개변수로 전달
delay(SplashWaitTimeMillis)
currentOnTimeout()
}
/* Landing screen content */
}
4. DisposableEffect : 키 값이 변경(리컴포지션 필요한 상황)되거나 컴포지션이 종료된 이후에도 작업이 이루어져야하는 경우에 사용
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// Safely update the current lambdas when a new one is provided
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
// If `lifecycleOwner` changes, dispose and reset the effect
DisposableEffect(lifecycleOwner) {
// Create an observer that triggers our remembered callbacks
// for sending analytics events
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnStop()
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
/* Home screen content */
}
5. sideEffect : Compose가 아닌 객체에 Compose 객체의 상태를 전달할 때 사용
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember { // Compose에 속한 객체가 아님
/* ... */
}
// On every successful composition, update the analytics library with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect { // 따라서, SideEffect composable을 사용해 값을 전달
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
6. produceState : suspend 함수 실행이 가능하며 State를 반환하는 함수(이 변형으로 Composable의 상태값으로 사용할 수 있음)
7. derivedStateOf : 하나 이상의 상태로부터 파생되는 상태값 계산
8. snapshotFlow : State<T>를 Flow로 반환해, 이전값과 새로운 값이 다를 때만 데이터 방출
- Compose 양상
1. Composition : what UI to show
2. Layout : where to place UI
3. Drawing : how it renders
- CompositionalLocal : 매개변수로 선언하지 않아도, 구성 가능한 하위 요소에 사용할 수 있습니다
@Composable
fun CompositionLocalExample() {
MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
Column {
Text("Uses MaterialTheme's provided alpha")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("Medium value provided for LocalContentAlpha")
Text("This Text also uses the medium value")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
DescendantExample()
}
}
}
}
}
@Composable
fun DescendantExample() {
// CompositionLocalProviders also work across composable functions
Text("This Text uses the disabled alpha now")
}
1. compositionLocalOf: 재구성 중에 제공된 값을 변경하면 current 값을 읽는 콘텐츠만 무효화됩니다.
2. staticCompositionLocalOf: compositionLocalOf와 달리 staticCompositionLocalOf 읽기는 Compose에서 추적하지 않습니다. 값을 변경하면 컴포지션에서 current 값을 읽는 위치만이 아니라 CompositionLocal이 제공된 content 람다 전체가 재구성됩니다.
공식 사이트 :
https://developer.android.com/jetpack/compose/mental-model
참고 사이트 :
'Android > AAC' 카테고리의 다른 글
Compose의 State (0) | 2022.03.03 |
---|---|
Compose 감잡기 (0) | 2022.02.24 |
Navigation (1) | 2021.08.13 |
DataBinding (1) | 2021.08.13 |
ViewModel (1) | 2021.08.12 |