개발관련일지

android compose LaunchedEffect, rememberCoroutineScope 학습정리 본문

개발기록/안드로이드

android compose LaunchedEffect, rememberCoroutineScope 학습정리

BEECHANGBOT 2023. 7. 27. 00:55

composable은 순수함수 처럼 사용하기위해서 (f(state) = view) side effect가 없는게 좋지만 필요한 경우에는 생명주기를 인식하고 관리되는 환경에서 시행되야함 -> 이걸 도와주는게 Effect API이다. 

 

LaunchedEffect

컴포저블내에서 suspend fun을 안전하게 실행해준다.

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}

internal class LaunchedEffectImpl(
    parentCoroutineContext: CoroutineContext,
    private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver {
    private val scope = CoroutineScope(parentCoroutineContext)
    private var job: Job? = null

    override fun onRemembered() {
        job?.cancel("Old job was still running!")
        job = scope.launch(block = task)
    }

    override fun onForgotten() {
        job?.cancel()
        job = null
    }

    override fun onAbandoned() {
        job?.cancel()
        job = null
    }
}

다른컴포저블함수 {
	LaunchedEffect(key1 = null , block = {
        // suspend CoroutineScope.() -> Unit
        // job = CoroutineScope.launch(block = task) 로 시작 
    })
}

LaunchedEffect는 컴포저블 함수이며 다른 컴포저블에서만 실행이 가능하다. 

LaunchedEffect가 실행되면 block이 코루틴으로 실행 된다.  <-> LaunchedEffect이 종료되면 코루틴도 종료된다. (컴포저블의 생명주기에 따라서 변하고 리컴포지션이 되면 종료됬다가 다시 실행된다.)

LaunchedEffect는 파라미터로 key를 받을 수 있고 여러개도 가능하다. key값이 변할 때마다 현재의 코루틴을 취소하고 코루틴을 다시 실행된다. (키값에 대한 재실행이 필요 하지 않을 경우 key에 Unit같은 스태틱한 값을 넣어주면된다.)

 

컴포즈 예시앱들의 사용처들

호출 컴포저블과 같이 생명주기를 같이하거나(한번실행만필요) , 특정 key(state)를 감지해서 리컴포지션시 필요한 작업을 하기 위해서 사용한다.

// JetChat
if (drawerOpen) {
// Open drawer and reset state in VM.
	LaunchedEffect(Unit) {
// wrap in try-finally to handle interruption whiles opening drawer
    try {
        drawerState.open()
    } finally {
        viewModel.resetOpenDrawerAction()
        }
    }
}
  
// Reply
LaunchedEffect(key1 = contentType) {
        if (contentType == ReplyContentType.SINGLE_PANE && !replyHomeUIState.isDetailOnlyOpen) {
            closeDetailScreen()
        }
    }

fun ReplyDockedSearchBar(
    emails: List<Email>,
    onSearchItemSelected: (Email) -> Unit,
    modifier: Modifier = Modifier
) {
    var query by remember { mutableStateOf("") }
    var active by remember { mutableStateOf(false) }
    val searchResults = remember { mutableStateListOf<Email>() }

    LaunchedEffect(query) { } 
    ... 
 }
 
 // Jetsnack
val indicatorIndex = remember { Animatable(0f) }
    val targetIndicatorIndex = selectedIndex.toFloat()
    LaunchedEffect(targetIndicatorIndex) {
        indicatorIndex.animateTo(targetIndicatorIndex, animSpec)
}
    
selectionFractions.forEachIndexed { index, selectionFraction ->
        val target = if (index == selectedIndex) 1f else 0f
        LaunchedEffect(target, animSpec) {
            selectionFraction.animateTo(target, animSpec)
        }
}

LaunchedEffect(state.query.text) {
                state.searching = true
                state.searchResults = SearchRepo.search(state.query.text)
                state.searching = false
}

 

rememberCoroutineScope

현재 컴포저블의 생명주기에 연결된 코루틴 스코프를 제공해준다. (엑티비티에서 사용하는 lifecycleScope의 컴포즈 버전으로 이해했다.) 

런치드이팩트와 마찬가지로 해당 컴포저블이 컴포지션에서 분리되면 코루틴이 취소된다. 

코루틴을 생명주기를 컨트롤 해야하는 경우 사용

코루틴 스코프의 참조를 특정 지점에서 유지하기 위해서 사용

 

사용된 곳들 

부모 컴포저블(코루틴 선언된 곳)의 코루틴스코프 사용, 유저 상호작용으로 사용 (단발성)

// Jetsnack
@Stable
class JetsnackAppState(
    val scaffoldState: ScaffoldState,
    val navController: NavHostController,
    private val snackbarManager: SnackbarManager,
    private val resources: Resources,
    coroutineScope: CoroutineScope // rememberCoroutineScope를 호출부에서 받아옴 
) {
    // Process snackbars coming from SnackbarManager
    init {
        coroutineScope.launch { 
        // 스낵바에 대한 처리를 미리 정의해놓고 사용하는거라서 스코프로 해놓은거같음 
            snackbarManager.messages.collect { currentMessages ->
                if (currentMessages.isNotEmpty()) {
                    val message = currentMessages[0]
                    val text = resources.getText(message.messageId)

                    // Display the snackbar on the screen. `showSnackbar` is a function
                    // that suspends until the snackbar disappears from the screen
                    scaffoldState.snackbarHostState.showSnackbar(text.toString())
                    // Once the snackbar is gone or dismissed, notify the SnackbarManager
                    snackbarManager.setMessageShown(message.id)
                }
            }
        }
    }
    
// Reply
 onDrawerClicked = {
	scope.launch {
		drawerState.close()
	}
}

//Jetchat
resetScroll = {
    scope.launch {
    	scrollState.scrollToItem(0)
    }
}
                
onChatClicked = {
    findNavController().popBackStack(R.id.nav_home, false)
    scope.launch {
    	drawerState.close()
    }
}

 

사용을 많이 안해보고 공부한거라서 사용처에 대해서 와닿지가 않았다. 

생각해본걸로는 사용 이유의 대해서는

  • 1. 호출되는 곳이 컴포저블인가? 호출컴포저블의 생명주기를 단순하게 따라가면되는가? 특정 값에대해서 변화를 참고해야하는가? -> LaunchedEffect
  • 2. 호출되는 곳 보다 상위의 컴포저블 생명주기를 가진 스코프가 필요하는가? 컴포저블 외의 호출부에서 코루틴이 필요한가? -> rememberCoroutineScope

로 정리했다. 

 

참조

https://stackoverflow.com/questions/72823905/launchedeffect-vs-remembercoroutinescope-this-explanation-makes-me-confused-pl

https://stackoverflow.com/questions/66474049/using-remembercoroutinescope-vs-launchedeffect

https://kotlinworld.com/251?category=974273

https://developer.android.com/jetpack/compose/side-effects?hl=ko

https://proandroiddev.com/launchedeffect-vs-remembercoroutinescope-in-jetpack-compose-24b5c91106ac