[안드로이드스튜디오_문화][Compose Sideeffects]

기말 지하기포·2024년 1월 7일
0

부수효과

-"Compose Ui가 렌더링 되는 것 이외의 효과를 주는 Composable Function" 또는 "Composable Context 내부에서 사용되는 특별한 Funtioin"을 의미한다.

-Composable Function과 관련이 있기 때문에 "Compose 부수효과"라고 불린다.

LaunchedEffect(suspend Function)

-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) }
}
  • Key 값이 2개인 LaunchedEffect 원본 코드(1)
fun LaunchedEffect(
    key1: Any?,
    key2: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1, key2) { LaunchedEffectImpl(applyContext, block) }
}
  • Key 값이 3개인 LaunchedEffect 원본 코드(1)
@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) }
}
  • LaunchedEffect를 사용한 예시코드(1)
>아래코드를 보면 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())
    }
}

rememberCoroutineScope

-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
}
  • rememberCoroutineScope를 사용한 잘못된 예시 코드 (1)
>아래코드는 Recomposition의 직접적인 실행부분에서 launch가 들어갔는데 이는,
>Composable Function이 다양한 상황에서 다시 호출 될 수 있기 때문에, 해당 코루틴이
>예상치 못한 방식으로 여러 번 실행 될 수 있기 때문이다.

>즉, 직접적으로 Composable Function내부에서 코루틴을 사용하고 싶으면 LaunchedEffect를 
>사용하는 것이고, rememberCoroutineScope를 사용하려면 사용자와 상호작용시 발생하는 이벤트처리
>코드 블락 내부에서 사용해야 한다.
@Composable
fun RememberCoroutineScopeExCode1() {
    val scope = rememberCoroutineScope()
    scope.launch {

    }
}
  • rememberCoroutineScope를 사용한 올바른 예시 코드 (1)
>onClick이라는 이벤트 처리부분에서 Coroutine을 생성하였다.
@Composable
fun RememberCoroutineScopeExCode() {
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch {
            delay(100L)
            println("Hello World!")
        }
    }) {

    }
}

rememberUpdatedState

-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 }
  • rememberUpdatedState 예시 코드(1)
@Composable
fun RememberUpdatedStateExCode(
    onTimeout : () -> Unit
) {
    val updatedOnTimeout by rememberUpdatedState(onTimeout)
    LaunchedEffect(true) {
        delay(3000L)
        onTimeout()
    }
}

DisposableEffect

-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 값이 변경된 것으로 인식한다.

  • DisposableEffect 원본 코드 (1)
fun DisposableEffect(
    key1: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
) {
    remember(key1) { DisposableEffectImpl(effect) }
}
  • DisposableEffect 원본 코드 (2)
@Composable
@NonRestartableComposable
fun DisposableEffect(
    key1: Any?,
    key2: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
) {
    remember(key1, key2) { DisposableEffectImpl(effect) }
}
  • DisposableEffect 원본 코드 (3)
@Composable
@NonRestartableComposable
fun DisposableEffect(
    key1: Any?,
    key2: Any?,
    key3: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
) {
    remember(key1, key2, key3) { DisposableEffectImpl(effect) }
}
  • DisposableEffect 예시 코드 (1)
@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)
        }
    }
}

SideEffect

-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에 의해 관리되지 않는 객체를 관리한다 ...}

  • SideEffect 원본 코드 (1)
@OptIn(InternalComposeApi::class)
fun SideEffect(
    effect: () -> Unit
) {
    currentComposer.recordSideEffect(effect)
}

ProduceState

-비동기 작업의 결과를 Compose 상태로 변환하고자 할 때 주로 사용한다.

-비compose 상태를 compose 상태로 변환하는 역할을 한다. 외부 데이터 소스를 받아서 데이터를 로드하고 받은 데이터를 compose 상태로 관리한다.

-compose 상태로 값을 관리하기 위해서 state를 반환하고 value의 값에 넣어주면 되는데 이때 value는 반환된 state의 value를 의미한다.

-코루틴 스코프를 제공하며 state[T] 객체를 return한다.

  • ProduceState 원본 코드 (1)
>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
}
  • ProduceState 예시 코드 (1)
@Composable
fun produceStateExCode(countUpTo : Int) : State<Int> {
    return produceState(initialValue = 0) {
        while (value < countUpTo) {
            delay(1000L)
            value++
        }
    }
}

derivedStateOf

-state를 사용해서 새로운 파생된 state(하나 이상의 다른 상태 객체에 의존하는 상태)를 생성한다. 새롭게 파생된 state는 의존하는 state의 값이 변경될 때만 값이 업데이트 된다. 따라서 불필요한 Recomposition을 막을 수 있고 불필요한 UI 업데이트도 최소화 할 수 있다.

  • derivedStateOf 원본 코드 (1)
fun <T> derivedStateOf(
    calculation: () -> T,
): State<T> = DerivedSnapshotState(calculation, null)
  • derivedStateOf 예시 코드 (1)
@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)
    }
}

SnapshotFlow

-compose 상태를 flow로 변환하며 , 이전에 방출한 값과 다를 경우에만 값을 방출한다.

-snapShotFlow{...여기의 compose 상태의 변화를 관찰하다가 상태값이 변경되면 변경된 값을 flow로 emit한다.}.collect{flow로 방출된 값들을 가져와서 여기서 flow 연산을 실행한다.}

-snapShotFlow에서 방출한 값을 가져와서 바로 종단연산자에 넣어야 되는 것은 아니야 중간에 가공해도 된다.

  • snapShotFlow 원본 코드 (1)
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()
    }
}
profile
포기하지 말기

0개의 댓글