Jetpack - Compose

2021. 10. 28. 00:49Android/AAC

- Compose : 선언형 UI 툴킷으로, 처음부터 화면 전체를 개념적으로 재생성한 후 필요한 변경사항만 적용하는 방식으로 작동

 

- Compose 특징

  1. @Composable 주석을 이용하여 Compose Compiler에게 UI로 반환되는 요소임을 알림
  2. 함수의 파라미터로 필요한 data를 받을 수 있음 => 동적인 처리가 가능(not xml, written in kotlin)
  3. 함수 내부에서 다른 Compose 함수를 불러 UI 계층화를 이룸
  4. 반환값이 없음
  5. 빠르고, 부수효과가 없이 멱등적(input이 같으면 output도 동일) => 즉, random() 혹은 global 변수 변경 작업 등을 포함하지 않아야 함

 

 

 

선언적 패러다임 작동 방식

React와 유사한 UI 패러다임으로 계층 구조를 따라 data는 아래 방향을 따라 event는 위로 이동

event에 의해 새로운 state가 되면 자동적으로 새로운 data를 가진 composable이 호출됨(Recomposition)

 

- Recomposition :  변화된 데이터가 적용된 곳에서만 일어남. 데이터를 읽거나 쓰는 작업은 오래걸리므로 viewmodel에서 백그라운드로 작업하게 하고 파라미터로 해당 값을 넘겨받고 event를 등록해 Recomposition이 작동하도록 하는 로직으로 만들어야함

 

  1. composable 함수는 병렬적으로 진행(composable은 해당 함수를 호출한 스레드와 다른 스레드에서 실행)되며 순차적으로 실행을 보장하지 않는다
  2. state가 변해서 영향이 있는 composable만 Recomposition한다
  3. recomposition이 끝나기 전에 state가 변화가 있을 경우, 해당 recomposition은 취소되는데 이때 UI tree를 버림
  4. 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

참고 사이트 :

https://wooooooak.github.io/jetpack%20compose/2021/05/18/%EC%BB%B4%ED%8F%AC%EC%A6%88%EA%B0%80%ED%95%84%EC%9A%94%ED%95%9C%EC%9D%B4%EC%9C%A0/

'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