| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- genarics
- ktor-auth
- android
- 2022 KAKAO BLIND RECRUITMENT
- Java
- gradle jdk
- 선언형ui
- 무선 페어링
- 선언형 ui
- rememberupdatedstate
- 2022 kakao blind
- 명령형ui
- mutableStateOf
- compose
- producestate
- 양과 늑대
- 명령형 ui
- clean coder
- 2989번
- 안드로이드
- NavHost
- mutablestate
- 자바
- bottomscaffold
- JCF
- snapshotflow
- remembercoroutinescope
- derivedstateof
- apollo3
- State
- Today
- Total
버미
[Jetpack Compose] Side Effect ( 2 / 2 ) 본문
Side Effect ( 1 / 2 ) 에서 설명하지 못했던 rememberCoroutineScope, produceState, derivedStateOf, snapshotFlow에 대해 설명하겠다.
공식 문서에서는 rememberCoroutineScope를 LaunchedEffect와 비교하여 설명하고 있는데, 코드와 특징을 정리하면 다음과 같다.
rememberCoroutineScope
LaunchedEffect는 Key 값의 변경에 따라 비동기 작업이 자동으로 트리거된다.
반면, rememberCoroutineScope는 컴포지션 생명 주기에 연동된 CoroutineScope를 제공하며, Key 값은 제공하지 않으며, 외부 이벤트(예: 버튼 클릭, 사용자 입력 등)에 의해 명시적으로 트리거되어 비동기 작업이 수행된다고 이해하면 된다.
코드
@Composable
inline fun rememberCoroutineScope(
crossinline getContext: @DisallowComposableCalls () -> CoroutineContext =
{ EmptyCoroutineContext }
): CoroutineScope {
val composer = currentComposer
val wrapper = remember {
CompositionScopedCoroutineScopeCanceller(
createCompositionCoroutineScope(getContext(), composer)
)
}
return wrapper.coroutineScope
}
rememberCoroutineScope는 컴포지션의 생명 주기와 연동된 CoroutineScope를 반환하며, 외부 이벤트에 따라 수동으로 코루틴을 실행하는 데 사용된다. 컴포지션이 종료되면 반환된 스코프는 자동으로 취소되므로, 안전하고 간편하게 비동기 작업을 관리할 수 있다.
rememberCoroutineScope의 특징
- 컴포지션 생명 주기와 연동된 CoroutineScope 제공
- 반환된 CoroutineScope는 컴포지션이 유지되는 동안만 유효하며, 컴포저블이 트리에서 제거되면 스코프가 자동으로 취소.
- 컴포지션 종료 시 비동기 작업이 자동으로 정리되므로 메모리 누수 방지에 유리하다.
- 비동기 작업을 수동으로 실행
- rememberCoroutineScope는 비동기 작업을 자동으로 실행하지 않으며, 수동으로 launch를 호출해야 한다.
- 외부 이벤트(예: 버튼 클릭, 사용자 입력 등), 지연 작업이나 장기 실행 작업을 트리거하는 데 적합하다.
- 디스패처 커스텀:
- 컴포지션의 Recomposer 디스패처가 기본 디스패처로 사용된다.
- 필요시 Dispatchers.IO, Dispatchers.Default 등 커스텀 디스패처를 설정할 수 있다.
ex) rememberCoroutineScope { Dispatchers.IO } 등
- 동일한 스코프 재사용
- 컴포저블이 리컴포지션 돼도 동일한 CoroutineScope 인스턴스를 반환되어 재생성을 방지.
- 오류 처리
- CoroutineContext(예: Job 포함), 컴포지션 종료 후 비동기 작업, 컴포저블 외부에서 호출하는 등의 컴포지션 규칙을 어겨도 함수 자체는 예외를 던지지 않는다.
- 대신 반환된 CoroutineScope의 coroutineContext에 실패한 Job을 포함시켜, 이후 작업 실행이 불가능하도록 처리한다.
- 상태 기반 작업에는 부적합
- rememberCoroutineScope는 상태 변경에 반응하지 않기 때문에, LaunchedEffect나 rememberUpdatedState를 사용하는 것이 더 적합하다.
rememberUpdatedState
컴포저블 내에서 비동기 작업 중에 최신 상태 값을 안전하게 참조할 수 있도록 해준다.
이 함수는 상태 변경에 따라 컴포지션이 재실행되지 않도록 보장하면서도, 비동기 작업 또는 효과(예: LaunchedEffect, DisposableEffect)에서 항상 최신 상태를 사용할 수 있게 한다. 간략하게 말하자면, 리컴포지션이 발생하지 않으면서 최신 상태로 관리하고 싶을 때 사용한다.
코드
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }
특징
- 최신 값을 참조:
- rememberUpdatedState는 최신 값을 참조할 수 있는 State Holder(State<T>)를 반환한다.
- 이를 통해, 리컴포지션을 방지하면서도 최신 상태를 비동기 작업에서 안전하게 사용할 수 있다.
- 컴포지션과 독립적:
- 상태가 변경되더라도 컴포지션 자체는 재구성되지 않는다.
- LaunchedEffect, DisposableEffect 등 장기 실행 작업에서 상태를 추적해야 할 때 유용.
- 상태 홀더 생성:
- 함수 호출 시 상태 홀더를 한 번 생성하며, 값이 변경될 때마다 홀더 내부의 값이 업데이트된다.
produceState
Compose에서 비동기 데이터나 외부 데이터를 Compose의 상태(State<T>)로 변환하기 위해 사용되는 함수다. 이를 통해 외부 데이터 소스(예: 네트워크 호출, 데이터베이스 등)를 State 객체로, UI를 동기화할 수 있다.
State 중 유일하게 초기값(initialValue)이 존재한다. 데이터가 로드되기 전에도 UI를 즉시 렌더링한다던가, 초기값에서 최종 데이터로의 전환을 부드럽게 처리할 수 있다.
코드
@Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
produceState의 특징
- 외부 데이터를 State로 변환
- 외부 데이터 소스를 관찰하거나 비동기 작업을 수행한 결과를 Compose의 상태(State<T>)로 제공한다.
- 비동기 작업의 결과를 UI와 직접 연결할 때 적합하다.
- 상태 관리와 UI의 동기화
- 반환된 State 객체를 통해 데이터의 최신 상태를 UI와 동기화한다.
- 데이터가 업데이트되면 자동으로 컴포저블이 리컴포지션.
- 초기값 설정
- initialValue를 통해 초기 상태 값을 설정할 수 있으며, 비동기 작업이 완료되기 전까지 초기값이 UI에 표시.
- key 기반으로 재실행
- produceState는 key 값을 기반으로 동작하며, key 값이 변경되면 기존 작업은 취소되고 새 작업이 시작된다.
derivedStateOf
Compose에서 하나 이상의 상태(State<T>)를 기반으로 새로운 상태를 생성할 때 사용하는 함수다.
생성된 상태는 의존하는 상태가 변경될 때만 재계산되며, 이를 통해 불필요한 리컴포지션을 방지하고 성능을 최적화할 수 있다.
derivedStateOf의 이점을 가질 수 있는 상황은 변경 가능성이 낮은 계산을 가지고 있으며, 계산 비용이 큰 상태를 관리하기에 적합하다.
코드
@StateFactoryMarker
fun <T> derivedStateOf(
policy: SnapshotMutationPolicy<T>,
calculation: () -> T,
): State<T> = DerivedSnapshotState(calculation, policy)
특징
- 의존하는 상태 기반 상태 생성:
- 기존 State 객체의 값을 조합하거나 변환하여 새로운 State 객체를 만든다.
ex. val derivedCount = remember { derivedStateOf { "Derived Count: ${count.value * 2}" } } - 새로 생성된 상태는 의존하는 상태가 변경될 때만 재계산.
- 기존 State 객체의 값을 조합하거나 변환하여 새로운 State 객체를 만든다.
- 불필요한 리컴포지션 방지:
- 의존하는 상태에 변경이 없으면, 기존 값을 재사용하여 리컴포지션을 방지.
- 복잡한 상태 계산이 빈번히 발생할 때 사용한다.
- 최적화된 상태 변환:
- derivedStateOf는 변경 가능성이 낮은 상태를 효율적으로 관리하며, 상태 변경 시 계산 비용을 최소화한다.
snapshotFlow
Jetpack Compose에서 Compose 상태(State<T>)를 Flow로 변환하는 데 사용되는 함수다. Compose 상태의 변경을 관찰하고, 비동기 작업과 결합할 수 있다.
Compose의 State 관리와 Flow라는 코틀린 비동기 스트림 간의 연결을 제공하기 때문에 코드가 약간 길다.
코드
fun <T> snapshotFlow(
block: () -> T
): Flow<T> = flow {
// Objects read the last time block was run
val readSet = MutableScatterSet<Any>()
val readObserver: (Any) -> Unit = {
if (it is StateObjectImpl) {
it.recordReadIn(ReaderKind.SnapshotFlow)
}
readSet.add(it)
}
// This channel may not block or lose data on a trySend call.
val appliedChanges = Channel<Set<Any>>(Channel.UNLIMITED)
// Register the apply observer before running for the first time
// so that we don't miss updates.
val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
val maybeObserved = changed.fastAny {
it !is StateObjectImpl || it.isReadIn(ReaderKind.SnapshotFlow)
}
if (maybeObserved) {
appliedChanges.trySend(changed)
}
}
try {
var lastValue = Snapshot.takeSnapshot(readObserver).run {
try {
enter(block)
} finally {
dispose()
}
}
emit(lastValue)
while (true) {
var found = false
var changedObjects = appliedChanges.receive()
// Poll for any other changes before running block to minimize the number of
// additional times it runs for the same data
while (true) {
// Assumption: readSet will typically be smaller than changed set
found = found || readSet.intersects(changedObjects)
changedObjects = appliedChanges.tryReceive().getOrNull() ?: break
}
if (found) {
readSet.clear()
val newValue = Snapshot.takeSnapshot(readObserver).run {
try {
enter(block)
} finally {
dispose()
}
}
if (newValue != lastValue) {
lastValue = newValue
emit(newValue)
}
}
}
} finally {
unregisterApplyObserver.dispose()
}
}
코드의 동작 흐름은 다음과 같다.
- 스냅샷 생성 및 상태 읽기 추적:
- Snapshot.takeSnapshot으로 상태 스냅샷을 생성하고, block을 실행하여 관찰된 상태를 readSet에 저장.
- 첫 번째 값 방출:
- block의 결과를 계산하고 emit으로 방출.
- 상태 변경 감지:
- registerApplyObserver로 상태 변경을 감지하고, 변경된 상태가 readSet과 겹칠 경우 block을 재실행.
- 변경된 값 방출:
- block 실행 결과가 이전 값과 다르면 새로운 값을 방출.
- 컴포지션 종료 시 정리:
- 스냅샷 관찰자를 제거하여 리소스 누수를 방지.
snapshotFlow의 특징
- Compose 상태를 Flow로 변환:
- Compose의 상태 값을 Flow로 변환하여, 상태 변경을 비동기적으로 처리.
- Compose 상태가 변경될 때마다 Flow에서 새로운 값을 방출한다.
- Compose 상태의 관찰:
- Compose 상태를 읽는 람다를 입력받아 상태 변경을 감지한다.
ex. val snapshotFlow = snapshotFlow { count.value } - Compose의 State나 MutableState를 람다에서 참조.
- Compose 상태를 읽는 람다를 입력받아 상태 변경을 감지한다.
- 비동기 작업과 연동:
- snapshotFlow를 통해 변환된 Flow는 Compose 외부의 비동기 작업(예: 네트워크 호출, 데이터 처리 등)과 결합할 수 있다.
- 동기 상태 변화 감지:
- 내부적으로 Compose의 스냅샷 시스템을 활용하여 상태 변화를 즉시 감지한다.
- Compose 상태의 일관성을 보장.
- Reference
Jetpack Compose Side Effect - Mash-up
Jetpack Compose Doc 읽기 — Part1[기초]
[Compose] Side Effect 관련 API 재 정리
'안드로이드' 카테고리의 다른 글
| [안드로이드] API 35 버전 업데이트 정리 (0) | 2025.06.28 |
|---|---|
| [안드로이드] GraphQL 사용하기 (Apollo3) (0) | 2024.12.30 |
| [JetPack Compose] State, remember 그리고 MutableStateOf (0) | 2024.12.03 |
| [Jetpack Compose] Side Effect ( 1 / 2 ) (0) | 2024.12.01 |
| Kapt와 KSP (0) | 2024.10.16 |