Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Jetpack Compose의 상태 및 사이드효과 API

Jetpack Compose의 상태 및 사이드효과 API

Google I/O Extended Busan 발표자료

Avatar for Ju Hyung Park

Ju Hyung Park

September 13, 2023
Tweet

More Decks by Ju Hyung Park

Other Decks in Programming

Transcript

  1. Activity, Fragment 클래스는 UI 및 OS 상호작용을 처리하는 로직만 포함

    데이터 모델은 앱의 데이터를 나타냄 UI요소 및 기타 구성요소로부터 독립 앱의 테스트 가능성 & 견고성이 높아짐 관심사 분리 데이터 모델에서 UI 도출 단일소스 저장소 데이터 유형을 정의하는 것을 의미 저장소만 데이터를 수정하거나 변경가능 불변 데이터 노출, 이벤트 수신 및 함수노출로 데이터를 수정 SSOT는 UDF패턴과 함께 사용됨 UDF에서 상태는 한 방향으로만 흐름 데이터를 수정하는 이벤트는 반대방향으로 흐름 https://developer.android.com/jetpack/compose/arc hitecture?hl=ko#udf Single Source Of Truth 단방향 데이터 흐름 (UDF) 기본적인 아키텍처 https://developer.android.com/topic/architecture
  2. fun updatePeople(people: Int) { viewModelScope.launch { if (people > MAX_PEOPLE)

    { _suggestedDestinations.value = emptyList() } else { val newDestinations = withContext(defaultDispatcher) { destinationsRepository.destinations .shuffled(Random(people * (1..100).shuffled().first())) } _suggestedDestinations.value = newDestinations } } } ViewModel
  3. fun updatePeople(people: Int) { viewModelScope.launch { if (people > MAX_PEOPLE)

    { _suggestedDestinations.value = emptyList() } else { val newDestinations = withContext(defaultDispatcher) { destinationsRepository.destinations .shuffled(Random(people * (1..100).shuffled().first())) } _suggestedDestinations.value = newDestinations } } } ViewModel
  4. Activity / Fragment @OptIn(ExperimentalMaterialApi::class) @Composable fun CraneHomeContent(viewModel: MainViewModel = viewModel())

    { val suggestedDestinations by viewModel.suggestedDestinations.collectAsStateWithLifecycle() BackdropScaffold( modifier = modifier, ... frontLayerContent = { when (tabSelected) { CraneScreen.Fly -> { ExploreSection( title = "Explore Flights by Destination", exploreList = suggestedDestinations, onItemClicked = onExploreItemClicked ) } } } ) }
  5. remember 컴포저블 함수 내 init composition 시 할당된 데이터를 보존

    & 리컴포지션 시 별도 계산없이 사용 collectAsState collectAsStateWithLifecycle Flow 값을 수집하고 최신 값을 컴포즈 상태로 나타냄 LaunchedEffect 컴포저블 함수 내 코루틴 suspend 함수 실행 rememberUpdateState 할당된 데이터를 remember로 보존 & 값이 들어올때마다 새로운 value Emit StateHolder 많은 양의 State들을 한 곳에 모아 관리할때 사용 DisposableEffect Composition에서 Composable 함수가 끝날 때 호출되는 Callback 함수 DerivedStateOf 다른 상태로부터 파생된 State를 구할때 사용 APIs
  6. remember 컴포저블 함수 내 init composition 시 할당된 데이터를 보존

    & 리컴포지션 시 별도 계산없이 사용 collectAsState collectAsStateWithLifecycle Flow 값을 수집하고 최신 값을 컴포즈 상태로 나타냄 LaunchedEffect 컴포저블 함수 내 코루틴 suspend 함수 실행 rememberUpdateState 할당된 데이터를 remember로 보존 & 값이 들어올때마다 새로운 value Emit StateHolder 많은 양의 State들을 한 곳에 모아 관리할때 사용 DisposableEffect Composition에서 Composable 함수가 끝날 때 호출되는 Callback 함수 DerivedStateOf 다른 상태로부터 파생된 State를 구할때 사용 APIs
  7. var tabSelected by remember { mutableStateOf(CraneScreen.Fly) } @Composable inline fun

    <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T = currentComposer.cache(false, calculation) @Composable inline fun <T> remember( key1: Any?, crossinline calculation: @DisallowComposableCalls () -> T ): T { return currentComposer.cache(currentComposer.changed(key1), calculation) }
  8. var tabSelected by remember { mutableStateOf(CraneScreen.Fly) } @Composable inline fun

    <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T = currentComposer.cache(false, calculation) @ComposeCompilerApi inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T { @Suppress("UNCHECKED_CAST") return rememberedValue().let { if (invalid || it === Composer.Empty) { val value = block() updateRememberedValue(value) value } else it } as T } @Composable inline fun <T> remember( key1: Any?, crossinline calculation: @DisallowComposableCalls () -> T ): T { return currentComposer.cache(currentComposer.changed(key1), calculation) }
  9. @Composable fun <T> StateFlow<T>.collectAsStateWithLifecycle( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, minActiveState: Lifecycle.State

    = Lifecycle.State.STARTED, context: CoroutineContext = EmptyCoroutineContext ): State<T> = collectAsStateWithLifecycle( initialValue = this.value, lifecycle = lifecycleOwner.lifecycle, minActiveState = minActiveState, context = context )
  10. @Composable fun <T> Flow<T>.collectAsStateWithLifecycle( initialValue: T, lifecycle: Lifecycle, minActiveState: Lifecycle.State

    = Lifecycle.State.STARTED, context: CoroutineContext = EmptyCoroutineContext ): State<T> { return produceState(initialValue, this, lifecycle, minActiveState, context) { lifecycle.repeatOnLifecycle(minActiveState) { if (context == EmptyCoroutineContext) { [email protected] { [email protected] = it } } else withContext(context) { [email protected] { [email protected] = it } } } } }
  11. @Composable fun <T> produceState( initialValue: T, vararg keys: Any?, producer:

    suspend ProduceStateScope<T>.() -> Unit ): State<T> { val result = remember { mutableStateOf(initialValue) } @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") LaunchedEffect(keys = keys) { ProduceStateScopeImpl(result, coroutineContext).producer() } return result }
  12. @Composable @NonRestartableComposable @OptIn(InternalComposeApi::class) fun LaunchedEffect( key1: Any?, block: suspend CoroutineScope.()

    -> Unit ) { val applyContext = currentComposer.applyCoroutineContext remember(key1) { LaunchedEffectImpl(applyContext, block) } } 컴포지션 종료시 코루틴도 종료 @Composable내 코루틴을 사용할때 주로 사용 다양한 수의 키를 매개변수로 사용, 키값 변경 시 재실행
  13. 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 } }
  14. @Composable fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier)

    { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { LaunchedEffect(onTimeout) { delay(SplashWaitTime) onTimeout() } Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null) } }
  15. @Composable fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier)

    { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { LaunchedEffect(onTimeout) { delay(SplashWaitTime) onTimeout() } Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null) } } @Composable fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) { val currentOnTimeout by rememberUpdatedState(onTimeout) Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { LaunchedEffect(Unit) { delay(SplashWaitTime) currentOnTimeout() } Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null) } }
  16. @Composable fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier)

    { val currentOnTimeout by rememberUpdatedState(onTimeout) Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { LaunchedEffect(Unit) { delay(SplashWaitTime) currentOnTimeout() } Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null) } } @Composable fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { LaunchedEffect(onTimeout) { delay(SplashWaitTime) onTimeout() } Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null) } } @Composable fun <T> rememberUpdatedState(newValue: T): State<T> = remember { mutableStateOf(newValue) }.apply { value = newValue }
  17. @Composable val scope = rememberCoroutineScope() scope.launch { scaffoldState.drawerState.open() } @Composable

    inline fun rememberCoroutineScope( crossinline getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext } ): CoroutineScope { val composer = currentComposer val wrapper = remember { CompositionScopedCoroutineScopeCanceller( createCompositionCoroutineScope(getContext(), composer) ) } return wrapper.coroutineScope } //suspend 함수
  18. @Composable fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) { CraneEditableUserInput( hint =

    "Choose Destination", caption = "To", vectorImageId = R.drawable.ic_plane, onInputChanged = onToDestinationChanged ) } @Composable fun CraneEditableUserInput( hint: String, caption: String? = null, @DrawableRes vectorImageId: Int? = null, onInputChanged: (String) -> Unit ) { var textState by remember { mutableStateOf(hint) } val isHint = { textState == hint } ... }
  19. class EditableUserInputState(private val hint: String, initialText: String) { var text

    by mutableStateOf(initialText) private set fun updateText(newText: String) { text = newText } val isHint: Boolean get() = text == hint } // StateHolder 만들기 1. text는 변경 가능한 상태이므로 상태값을 저장하고 리컴포지션 시 최신 상태값으로 가져오기 위해 mutableStateOf 사용 2. updateText로 상태값 update 3. initialText 매개변수로 text 초기화 4. text의 Hint여부 로직 포함
  20. class EditableUserInputState(private val hint: String, initialText: String) { var text

    by mutableStateOf(initialText) private set fun updateText(newText: String) { text = newText } val isHint: Boolean get() = text == hint } // StateHolder 만들기 @Composable fun rememberEditableUserInputState(hint: String): EditableUserInputState = remember(hint) { EditableUserInputState(hint, hint) }
  21. class EditableUserInputState(private val hint: String, initialText: String) { var text

    by mutableStateOf(initialText) private set fun updateText(newText: String) { text = newText } val isHint: Boolean get() = text == hint } // StateHolder 만들기 @Composable fun rememberEditableUserInputState(hint: String): EditableUserInputState = remember(hint) { EditableUserInputState(hint, hint) } var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) }
  22. class EditableUserInputState(private val hint: String, initialText: String) { var text

    by mutableStateOf(initialText) private set fun updateText(newText: String) { text = newText } val isHint: Boolean get() = text == hint companion object { val Saver: Saver<EditableUserInputState, *> = listSaver( save = { listOf(it.hint, it.text) }, restore = { EditableUserInputState( hint = it[0], initialText = it[1], ) } ) } } // StateHolder 만들기
  23. // StateHolder 만들기 @Composable fun rememberEditableUserInputState(hint: String): EditableUserInputState = rememberSaveable(hint,

    saver = EditableUserInputState.Saver) { EditableUserInputState(hint, hint) } @Composable fun CraneEditableUserInput( state: EditableUserInputState = rememberEditableUserInputState(""), caption: String? = null, @DrawableRes vectorImageId: Int? = null ) { /* ... */ } @Composable fun CraneEditableUserInput( hint: String, caption: String? = null, @DrawableRes vectorImageId: Int? = null, onInputChanged: (String) -> Unit )
  24. // CranEditableUserInput 사용처 @Composable fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) {

    val editableUserInputState = rememberEditableUserInputState(hint = "Choose Destination") CraneEditableUserInput( state = editableUserInputState, caption = "To", vectorImageId = R.drawable.ic_plane ) }
  25. @Composable fun rememberMapViewWithLifecycle(): MapView { val context = LocalContext.current //

    TODO Codelab: DisposableEffect step. Make MapView follow the lifecycle return remember { MapView(context).apply { id = R.id.map onCreate(Bundle()) } } } MapView의 경우 Lifecycle에 따른 관리가 필요한 객체임에도 불구, 컴포저블 내에서 주기를 알 수 없음
  26. @Composable fun rememberMapViewWithLifecycle(): MapView { val context = LocalContext.current //

    TODO Codelab: DisposableEffect step. Make MapView follow the lifecycle return remember { MapView(context).apply { id = R.id.map onCreate(Bundle()) } } } private fun getMapLifecycleObserver(mapView: MapView): LifecycleEventObserver = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) Lifecycle.Event.ON_START -> mapView.onStart() Lifecycle.Event.ON_RESUME -> mapView.onResume() Lifecycle.Event.ON_PAUSE -> mapView.onPause() Lifecycle.Event.ON_STOP -> mapView.onStop() Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() else -> throw IllegalStateException() } }
  27. @Composable fun rememberMapViewWithLifecycle(): MapView { val context = LocalContext.current val

    mapView = remember { MapView(context).apply { id = R.id.map } } val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(key1 = lifecycle, key2 = mapView) { // Make MapView follow the current lifecycle val lifecycleObserver = getMapLifecycleObserver(mapView) lifecycle.addObserver(lifecycleObserver) onDispose { lifecycle.removeObserver(lifecycleObserver) } } return mapView }
  28. // LazyColumn의 LazyListState를 사용. val showButton by remember { derivedStateOf

    { listState.firstVisibleItemIndex > 0 } } if (showButton) { val coroutineScope = rememberCoroutineScope() FloatingActionButton( backgroundColor = MaterialTheme.colors.primary, modifier = Modifier .align(Alignment.BottomEnd) .navigationBarsPadding() .padding(bottom = 8.dp), onClick = { coroutineScope.launch { listState.scrollToItem(0) } } ) { Text("Up!") } } https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b
  29. State In Jetpack Compose Advanced state in Jetpack Compose 참고

    https://io.google/2022/program/c9768969-9e81 -4865-9dff-29a2ab1201ea/intl/ko/ https://developer.android.com/codelabs/jetpac k-compose-state?hl=ko#0 https://io.google/2023/program/9aae6fa0-5fa2 -459d-bb46-f5d13db817a0/intl/ko/ https://developer.android.com/codelabs/jetpac k-compose-advanced-state-side-effects?hl=k o#0