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

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

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

Google I/O Extended Busan 발표자료

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