[안드로이드 Compose 6] - SideEffect(LaunchedEffect, Coroutine 등)

이영준·2023년 8월 30일
1

안드로이드_Compose

목록 보기
6/6
post-thumbnail

Compose에는 다양한 부수효과들이 있다. 천천히 살펴보자

📌 LaunchedEffect

코루틴 스코프를 composable안에 열어서 suspend fun을 돌아갈 수 있게 해준다.
대표적으로 snackbar를 컴포즈 안에서 띄우려 할 때 쓰일 수 있다.

@Composable
fun MyScreen(
    state: UiState<List Movie>>,
	scaffoldState: ScaffoldState = rememberScaffoldState ()
) {
    if (state.hasError) {
        LaunchedEffect(scaffoldState.snackbarHostState) {
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error message"
                        actionLabel = "Retry message"
            )
        }
    }
}
Scaffold (scaffoldState = scaffoldState) {
	}
}

LaunchedEffect가 인자로 받는 Key, 위의 경우에는 scaffoldState.snackbarHostState가 값이 바뀔 때, 코루틴을 다시 만들어 실행한다.

LaunchedEffect가 composition에서 빠지면 coroutine도 같이 제거된다.

📌 rememberCoroutineScope

rememberCoroutineScope는 Composable의 생명주기에 맞게 compose가 동작되도록 한다.
즉 화면이 전환되어 composable이 destroy되면서 이 안에 있는 돌아가는 coroutine도 종료되는 것이다.

📌 rememberUpdatedState

값이 변경되는 경우 다시 시작되지 않는 효과에서 값 참조를 하는 기능이다.
value가 바뀌었을 때 state도 바뀌도록 하는 기능

쉽게 말해 rememberUpdatedState() 의 매개변수 데이터의 값이 바뀌는 경우, 이에 해당하는 state를 업데이트 해주어, recomposition의 대상이 안되는 것을 recomposition이 되도록 해준다.

예를 들어 이런 코드가 있다고 하자.

@Composable
fun LandingScreen(onTimeout: () -> Unit) {
    val currentOnTimeout by rememberUpdatedState (onTimeout)
    LaunchedEffect (true) {
        delay (SplashWaitTimeMillis)
        currentOnTimeout ()
    }
}

어떠한 다른 composable에서 LandingScreen을 부르면서 인자로 onTimeOut을 준다고 하면, state의 value를 가지고 있는 currentOnTimeOut이 덥데이트 된다. 여기서 launchdEffect 안을 true로 하는 이유는 딱 한번만 변화를 감지해서 currentOnTimeOut을 부르려고 하기 때문이다.

만약 이렇게 하지 않고 true 대신 currentOnTimeout 자체를 인자로 받는다면 계속 함수를 호출해서 무한반복되는 코드가 될 수 있다.

📌 DisposableEffect

정리가 필요한 효과이다. 즉, observer등 정리가 필요한 이벤트 등을 넣는 effect이다.

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    _onStartLogging: () - Unit,
_onStopLogging: ( - Unit
)
val startLoggingOnStart by rememberUpdatedState(_onStartLogging)
val stopLogging0nStop by rememberUpdatedState(_onStopLogging)
DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver {
        _, event-
        if (event = Lifecycle.Event.ON_START) {
            startLoggingOnStart()
        } else if (event = Lifecycle.Event.ON_STOP) {
            stopLoggingOnStop()
        }
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    println("Kotlinworld » Observer Attached")
    onDispose {
// Composable0l dispose UH observer
        lifecycleOwner.lifecycle.removeObserver(observer)
        printin("Kotlinworld » Observer Removed")
    }
}

DisposableEffect를 쓰면 lifecycleOwner가 종료되면 onDispose가 불리면 observer가 제거되게 할 수 있다.

한가지 궁금한 점은 DispoableEffect에서 lifecycleowner를 가져와서 쓴다.
일반적으로 oncreate, onpause 등 라이프사이클에 대한 분기 처리는 override 된 함수에서 사용했는데, 왜 이렇게 하는지는 다소 궁금하다.

컴포즈는 뷰 단에서 모든 것을 관리하는 구조로 하는 것 같다.

📌 SideEffect

Compose에서 관리되는 객체가 아닌 다른 객체에 compose의 상태를 공유하기 위한 용도로, Composition이 성공적으로 완료되면 진행할 동작을 예약할 때 SideEffect를 사용.

@Composable
fun BackHandler(backDispatcher: OnBackPressedDispatcher,
                isEnable: Boolean = false,
                onBack: () -> Unit) {

  ..
    // Remember in Composition a back callback that calls the `onBack` lambda
    val backCallback = remember {
       ..
    }

    SideEffect {
        backCallback.isEnabled = isEnable
    }

    // If `backDispatcher` changes, dispose and reset the effect
    DisposableEffect(backDispatcher) {
     ..
    }
}

📌 ProduceState

비 composable 값들을 composable state로 만들어준다.

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {

    // 초기값으로 Result.Loading을 세팅하고, url, imageRepository가 변경되면
    // 실행중인 producer(coroutine)이 취소되고 재시작됨
    return produceState(initialValue = Result.Loading, url, imageRepository) {

        // coroutine scope 내부이므로 suspend function을 호출할 수 있습니다.
        val image = imageRepository.load(url)

        // 진행 결과에 따라 value에 값을 세팅하여 emit 합니다.
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

produceState 자체에서 코루틴 스코프를 생성하기 때문에 suspend fun을 부를 수 있다.
그리고 result로 image를 return 한다.

📌 derivedStateOf

하나 이상의 상태 객체를 다른 상태로 변환한다.

@Composable
fun TodoList(
    highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")
) {
    val todoTasks = remember { mutableStateListOf<String>() }

    // high priority keyword의 계산은 todoTasks 또는 highPriorityKeywords 변경시에만 수행
    // recomposition 되더라도 해당 param의 변경이 없다면 수행되지 않음.
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf {
            todoTasks.filter { it.containsWord(highPriorityKeywords) }
        }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}

derivedStateOf는 상태를 만들어준다.
derivedStateOf는 안의 계산된 값이 바뀔 때에만 state의 value를 바꿔준다.

📌 snapshotFlow

State를 Flow로 바꿔준다.

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

snapshotFlow 역시 값이 바뀌었을 때에만 실행이된다.

참고로 viewmodel에서 선언된 flow를 state로 바꿔주는
Flow.collectAsState() 도 있다. 통신에 활용하자.

📌 Reference

https://tourspace.tistory.com/412

profile
컴퓨터와 교육 그사이 어딘가

0개의 댓글