Compose SideEffects에 대해 알아야할 모든 것

강현석·2023년 2월 27일
0

article

목록 보기
5/9

본 내용은 학습을 위해 Everything you need to know about Side Effects in Jetpack Compose with examples을 보고 입맛대로 정리한 글입니다.


Composable 함수에 non-composable 함수를 실행하지 마라

@Composable
fun LavaComposable() {

  val context = LocalContext.current

  // 🚫 don't call makeToast() here
  // 토스트가 여러번 호출됨
  Toast.makeToast(context, "💀", Toast.LENGTH_SHORT).show()

  LaunchedEffect(Unit) {
    // ✅ safe to run any non-composable code here
    // LavaComposable이 Composition에 들어가면, 토스트는 한번만 호출되고 끝남
    Toast.makeToast(context, "🏝️", Toast.LENGTH_SHORT).show()
  }
}

SideEffect 사용 예

SideEffect()

@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // composition이 성공할 때마다 호출
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}
  • recomposition 할 때마다 실행됨

LaunchedEffect()

@Composable
fun SurpriseComposable() {
    Text("Wait for a surprise ⏰")

    LaunchedEffect(key1 = Unit) {
        delay(2000)
        Toast.makeToast(context, "🎊 Surprise 🎊", Toast.LENGTH_SHORT).show()
    }
}
  • composition 진입 및 key가 업데이트될 때 실행됨
  • key가 변경될 때마다 다시 람다 실행
  • 코루틴 scope 지원
  • 위의 예제에서는 key가 Unit이므로, Toast가 한 번만 표시됨(싱글턴)
@Composable
fun PoppingList(onListEmptied : () -> Unit) {
    val list = remember { mutableStateListOf(1, 2, 3, 4, 5) }
    LaunchedEffect(key1 = list.size) {
        println("${list.size} items left")
        if (list.isEmpty()) {
            println("No more left! Bail")
            onListEmptied()
        }
    }
    Button(onClick = { list.removeLast() }) {
        Text("Pop")
    }
}
  • 여러 개의 key 설정 가능
@Composable
fun NavigateOnStateChange(onNavigateAway : () -> Unit) {
    val viewModel = viewModel { MyViewModel() }

    val state = viewModel.state
    when(state) {
        UserJourneyCompleted -> LaunchedEffect(key1 = Unit) {
            // make sure the navigation happens in a launched effect
            // otherwise you risk having it called multiple times
            onNavigateAway()
        }
        // .. your other states here
    }
}
  • 1회성 이벤트 호출
  • 상태 변경 시 non-composable 코드 호출(예, 새 화면 이동)

DisposableEffect

@Composable
fun BroadcastReceiver(intentFilter: IntentFilter, onReceive: (Intent) -> Unit) {
    val context = LocalContext.current
    DisposableEffect(key1 = context) {
        val broadcastReceiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                onReceive(intent)
            }
        }
        context.registerReceiver(broadcastReceiver, intentFilter)
        onDispose {
            context.unregisterReceiver(broadcastReceiver)
        }
    }
}
  • LaunchedEffect와 동일하지만, clean-up 기능이 있음
  • 코루틴 scope 지원하지 않음
  • onDispose
    • key가 변경되거나 Composable이 composition을 벗어나는 즉시 호출
    • 리소스 해제 관련 로직 수행

SideEffect 정말 필요할까?

@Composable
fun ButtonClick() {
    val list = remember { mutableStateListOf(1, 2, 3, 4, 5) }
    val context = LocalContext.current

    Button(onClick = {
        if (list.isEmpty()) {
            Toast.makeText(context, "List is empty", Toast.LENGTH_SHORT).show()
        } else {
            list.removeLast()
        }
    }) {
        Text("Pop: ${list.size} left")
    }
}
  • 대부분의 경우, 사용자가 버튼을 클릭할 때와 같이 non-composable 함수를 실행할 수 있음

Composable 함수에서 코루틴을 시작하는 방법

@Composable
fun ShowSnackbar() {
    Box(Modifier.fillMaxSize()) {
        val snackbarHostState = remember { SnackbarHostState() }
        val scope = rememberCoroutineScope()
        Button(onClick = {
            scope.launch {
                val result = snackbarHostState.showSnackbar(
                    "Message deleted", actionLabel = "Undo"
                )
                when (result) {
                    SnackbarResult.Dismissed -> {
                        // nothing to do
                    }
                    SnackbarResult.ActionPerformed -> {
                        // TODO perform undo
                    }
                }
            }
        }
        ) {
            Text("Delete")
        }
        SnackbarHost(
            hostState = snackbarHostState,
            modifier = Modifier.align(Alignment.BottomCenter),
        )
    }
}
  • rememberCoroutineScope를 활용하여, 코루틴을 시작하는 데 사용할 수 있음
  • SideEffect Composable 또는 콜백 외부에서 코루틴을 실행하고 싶지 않을 경우 사용
  • non-composable에서 코루틴을 시작하면, 정의되지 않은 순간에 새 코루틴을 시작할 위험 존재
profile
볼링을 좋아하는 안드로이드 개발자

0개의 댓글