-"Compose Ui가 렌더링 되는 것 이외의 효과를 주는 Composable Function" 또는 "Composable Context 내부에서 사용되는 특별한 Funtioin"을 의미한다.
-Composable Function과 관련이 있기 때문에 "Compose 부수효과"라고 불린다.
-Composable Function 내부에 Coroutine을 생성하기 위해서 사용한다.
compose의 생명주기와 연결된 Coroutine Scop를 생성 할 수 있다. 즉, Composable Function이 화면에 렌더링 될 때 자동으로 Coroutine Scope를 생성해서 내부의 Coroutine을 시작하고 , 화면에서 사라질 때 Coroutine Scope 내부의 Coroutine을 취소한다.
Key 값이 변경되면 Recomposition이 실행됨과 동시에 Coroutine Scope 내부의 Coroutine이 종료되고 다시 실행된다. 여기서 Key 값이 의미하는 것은 LaunchedEffect의 parameter에 들어가 있는 값을 의미한다.
Key 값이 1개인 LaunchedEffect 원본 코드(1)
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
fun LaunchedEffect(
key1: Any?,
key2: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1, key2) { LaunchedEffectImpl(applyContext, block) }
}
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
key2: Any?,
key3: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1, key2, key3) { LaunchedEffectImpl(applyContext, block) }
}
>아래코드를 보면 LauncedEffect의 parameter로 counter가 들어갔으므로
>counter 값이 변경되면 LaunchedEffect의 CoroutineScope 내부의 Coroutine이
>재실행되므로 animatable.animateTo(counter.toFloat())이 다시 실행된다.
>
@Composable
fun LaunchedEffectExCode(
counter : Int
) {
val animatable = remember {
Animatable(0f)
}
LaunchedEffect(key1 = counter) {
animatable.animateTo(counter.toFloat())
}
}
-UI 이벤트 처리를 하기 위해서 또는 비동기 작업의 결과를 UI와 동기화 하기 위해서 사용한다.
-Composable Function 내부에 재사용이 가능하며 해당 Composable Function의 생명주기에 바인딩된 CoroutineScope를 생성한다. 따라서 해당 Composable Function의 생명주기를 따르기 때문에 rememberCoroutineScope()를 통해서 만든 CoroutineScope가 취소 될 수 있다. 또한, remember가 되었기 때문에 해당 Composable Function이 Recomposition이 되어도 같은 CoroutineScope가 사용 될 수 있다.
-Composable Function 내부에서 비동기 작업을 관리하기 위해서 사용된다.
-Composable Funtion이 실행되는 직접적인 부분에서는 사용 할 수 없다. 이는 Recomposition되는 동안 Coroutine을 생성하면 Coroutine이 중복 실행 될 위험이 있어서 비효율적이고 예상치 못한 동작을 초래 할 수 있기 때문이다.
Composable Function이 Recomposition 되어도 CoroutineScope 내부의 Coroutine이 취소되지 않고 계속 유지되는 지속성을 띄기 때문에 안정성이 높다. 따라서 UI와 상호작용하는 작업을 구성할 때 사용하면 좋다.
rememberCoroutineScope 원본 코드(1)
@Composable
inline fun rememberCoroutineScope(
crossinline getContext: @DisallowComposableCalls () -> CoroutineContext =
{ EmptyCoroutineContext }
): CoroutineScope {
val composer = currentComposer
val wrapper = remember {
CompositionScopedCoroutineScopeCanceller(
createCompositionCoroutineScope(getContext(), composer)
)
}
return wrapper.coroutineScope
}
>아래코드는 Recomposition의 직접적인 실행부분에서 launch가 들어갔는데 이는,
>Composable Function이 다양한 상황에서 다시 호출 될 수 있기 때문에, 해당 코루틴이
>예상치 못한 방식으로 여러 번 실행 될 수 있기 때문이다.
>즉, 직접적으로 Composable Function내부에서 코루틴을 사용하고 싶으면 LaunchedEffect를
>사용하는 것이고, rememberCoroutineScope를 사용하려면 사용자와 상호작용시 발생하는 이벤트처리
>코드 블락 내부에서 사용해야 한다.
@Composable
fun RememberCoroutineScopeExCode1() {
val scope = rememberCoroutineScope()
scope.launch {
}
}
>onClick이라는 이벤트 처리부분에서 Coroutine을 생성하였다.
@Composable
fun RememberCoroutineScopeExCode() {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
delay(100L)
println("Hello World!")
}
}) {
}
}
-Recomposition 되어도 State의 값을 최신 상태로 유지하고자 할 때 사용된다.
-항상 State의 값을 최신으로 유지한다.
remember는 초기화 코드 때 한번만 실행되고 더 이상 실행되지 않는다. 즉, Recomposition시 State가 Update 되어도 State의 값은 그대로이다. 따라서 rememberUpdatedState를 사용해서 State의 값 또한 Update 될 수 있게 도와 줄 수 있다.
rememberUpdatedState 원본 코드(1)
>.apply의 내부 코드 블락으로 인해서 항상 State의 최신 값이 유지 될 수 있다.
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }
@Composable
fun RememberUpdatedStateExCode(
onTimeout : () -> Unit
) {
val updatedOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(true) {
delay(3000L)
onTimeout()
}
}
-Composable Function이 Dispose될 때 리소스 할당 및 해제 , 이벤트 리스너 등록 및 해제 등등의 등록 및 해제 작업을 할 때 주로 사용된다.
-Composabel Function에서 Dispose 되었다는 의미는 "Composable Function이 화면에서 사라짐" 또는 "해당 Composable Function이 더 이상 화면에 표시되지 않음"을 의미한다.
-이렇게 Composable Function이 화면에서 제거될 때 이와 관련된 리소스(리스너 등등)을 제거해야 할 때 DisposableEffect를 사용한다. 왜냐하면 Dispose 되었다는 것은 어차피 사용할 일 이 없다는 것이고 그럼 의도적으로 리소스를 해제해줘야 메모리 누수가 발생하지 않기 때문이다.
-DisposableEffect의 후행람다에서 onDispose{} 코드 블락에서 리소스 해제 작업을 실행 시켜주면 된다. 여기서 onDispose{} 코드 블락이 실행 될 수 있는 조건은 두가지가 존재하는데 이는, "DisposableEffect를 포함하고 있는 Composable Function이 Dispose 되었을 때"와 "DisposableEffect의 parameter에 들어간 key 값이 변경되었을 때" 이렇게 두가지의 경우이다.
-주의할점은 DisposableEffect의 Key 값으로 사용되는 것이 객체로 사용된다면(ex:lifecycleowner) 객체가 참조하는 대상이 변경되어야 Key 값의 변경으로 인식하고 , 단순한 값(ex:Int)이 사용된다면 그냥 값이 바뀌면 Key 값이 변경된 것으로 인식한다.
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}
@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
key2: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1, key2) { DisposableEffectImpl(effect) }
}
@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
key2: Any?,
key3: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1, key2, key3) { DisposableEffectImpl(effect) }
}
@Composable
fun DisposableEffectExCode(
) {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if(event == Lifecycle.Event.ON_PAUSE) {
println("On pause called")
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
-Recomposition 될 때 비 compose 상태를 처리하기 위해서 사용한다.
-Compose 상태를 비 Compose 코드에 전달한다. 즉, JetpackCopmose 내부에서 관리되는 상태 또는 데이터가 Compose Function 범위를 벗어난 코드에 영향을 줄 수 있다는 것이다.
-Recomposition 될 때마다 SideEffect {}의 람다 코드 블락이 실행된다. 이렇게 하면 SideEffect{}의 코드 블락 내부에서 Compose에 의해 관리되지 않는 값들을 Compose의 라이프사이클에 알맞게 관리 할 수 있다.
-"@Composable 내부에있는 Compose Function의 Parameter를 값으로 갖는 Compose에 의해 관리되지 않는 객체"는 composable의 parameter의 값이 변경되어서 Recomposition이 되어서, composable의 바뀐 parameter의 값을 다시 받으려고 할 때 못 받는다.
-왜냐하면 compose에 의해 관리되지 않는 객체이기 때문이다. 이를 해결하기 위해서 SideEffect{... 여기서 compose에 의해 관리되지 않는 객체를 관리한다 ...}
@OptIn(InternalComposeApi::class)
fun SideEffect(
effect: () -> Unit
) {
currentComposer.recordSideEffect(effect)
}
-비동기 작업의 결과를 Compose 상태로 변환하고자 할 때 주로 사용한다.
-비compose 상태를 compose 상태로 변환하는 역할을 한다. 외부 데이터 소스를 받아서 데이터를 로드하고 받은 데이터를 compose 상태로 관리한다.
-compose 상태로 값을 관리하기 위해서 state를 반환하고 value의 값에 넣어주면 되는데 이때 value는 반환된 state의 value를 의미한다.
-코루틴 스코프를 제공하며 state[T] 객체를 return한다.
>LaunchedEffect로 인해서 코루틴을 시작 할 수 있는것이다.
@Composable
fun <T> produceState(
initialValue: T,
producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(Unit) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
@Composable
fun produceStateExCode(countUpTo : Int) : State<Int> {
return produceState(initialValue = 0) {
while (value < countUpTo) {
delay(1000L)
value++
}
}
}
-state를 사용해서 새로운 파생된 state(하나 이상의 다른 상태 객체에 의존하는 상태)를 생성한다. 새롭게 파생된 state는 의존하는 state의 값이 변경될 때만 값이 업데이트 된다. 따라서 불필요한 Recomposition을 막을 수 있고 불필요한 UI 업데이트도 최소화 할 수 있다.
fun <T> derivedStateOf(
calculation: () -> T,
): State<T> = DerivedSnapshotState(calculation, null)
@SuppressLint("UnrememberedMutableState")
@Composable
fun DerivedStateOfExCode() {
var counter by remember { mutableStateOf(0) }
val counterText by derivedStateOf { "The counter is $counter" }
Button(onClick = { counter++ }) {
Text(text = counterText)
}
}
-compose 상태를 flow로 변환하며 , 이전에 방출한 값과 다를 경우에만 값을 방출한다.
-snapShotFlow{...여기의 compose 상태의 변화를 관찰하다가 상태값이 변경되면 변경된 값을 flow로 emit한다.}.collect{flow로 방출된 값들을 가져와서 여기서 flow 연산을 실행한다.}
-snapShotFlow에서 방출한 값을 가져와서 바로 종단연산자에 넣어야 되는 것은 아니야 중간에 가공해도 된다.
fun <T> snapshotFlow(
block: () -> T
): Flow<T> = flow {
// Objects read the last time block was run
val readSet = mutableSetOf<Any>()
val readObserver: (Any) -> Unit = { 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, _ ->
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
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()
}
}