Slide 1

Slide 1 text

This work is licensed under the Apache 2.0 License Sungyong An, Android GDE, NAVER WEBTOON Compose Camp

Slide 2

Slide 2 text

This work is licensed under the Apache 2.0 License Pathway 3. Architecture and state https://speakerdeck.com/fornewid/compose-camp-22kr-pathway3

Slide 3

Slide 3 text

Sungyong An NAVER WEBTOON, Android GDE @fornewid on Github 캠핑지기 Link: https://github.com/fornewid

Slide 4

Slide 4 text

This work is licensed under the Apache 2.0 License Pathway 3. Architecture and state Architecting your Compose UI Compose에서 UDF 패턴을 구현하는 방법, 이벤트 및 상태 홀더를 구현하는 방법, Compose에서 ViewModel을 사용하는 방법에 중점을 둡니다. A Compose state of mind Compose의 상태 모델과 컴포지션, 상태 호이스팅/상태 홀더/AAC ViewModel을 언제 사용하는지, 컴포지션 외부에서 상태를 변경하는 법을 알아봅니다. Advanced state and side effects Jetpack Compose의 상태 및 side effects API와 관련된 고급 개념을 알아봅니다. Compose Navigation Compose에서 Navigation 라이브러리를 사용하여 앱 내 탐색과 딥 링크를 지원하고, 탐색을 테스트하는 방법을 알아봅니다. Link: https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-3 Article Video Codelab Codelab

Slide 5

Slide 5 text

This work is licensed under the Apache 2.0 License https://github.com/gdgand/ComposeCamp2022 pathway3 - AdvancedStateAndSideEffectsCodelab - NavigationCodelab Codelab 안내 Link: https://github.com/gdgand/ComposeCamp2022

Slide 6

Slide 6 text

This work is licensed under the Apache 2.0 License Codelab 안내 Link: https://github.com/gdgand/ComposeCamp2022/tree/main/pathway3

Slide 7

Slide 7 text

This work is licensed under the Apache 2.0 License Keyword NavController, NavHost. Navigate to a composable. Navigate with arguments. Deep links. Testing. LaunchedEffect, DisposableEffect, SideEffect. rememberCoroutineScope. rememberUpdatedState. produceState, derivedStateOf, snapshotFlow. Unidirectional data flow. State, Event, State hoisting. State holder, AAC ViewModel. Stateful vs Stateless. Composition, Recomposition. remember, rememberSaveable. State Side-effect Navigation

Slide 8

Slide 8 text

This work is licensed under the Apache 2.0 License Jetpack Compose에서 데이터 스트림을 관찰하는 방법, state holder를 만드는 방법, side effect API를 사용하는 방법, Composable에서 suspend 함수를 호출하는 방법을 알아봅니다. Advanced State and Side Effects Codelab Link: https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects#0

Slide 9

Slide 9 text

This work is licensed under the Apache 2.0 License var name by remember { mutableStateOf("") } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) Unidirectional Data Flow Link: https://developer.android.com/jetpack/compose/architecture

Slide 10

Slide 10 text

This work is licensed under the Apache 2.0 License val state = rememberTextFieldState("") OutlinedTextField( value = state.name, onValueChange = { state.name = it }, label = { Text("Name") } ) State Holder as source of truth class TextFieldState(initialName: String) { var name by mutableStateOf(initialName) } @Composable fun rememberTextFieldState( name: String ): TextFieldState = remember(name) { TextFieldState(name) } Link: https://developer.android.com/jetpack/compose/state#state-holder-source-of-truth Composable에 여러 UI 요소의 상태가 관련되어 있는 복잡한 UI 로직이 있다면, UI 로직과 UI 요소의 상태를 State Holder로 분리하는 것을 권장한다.

Slide 11

Slide 11 text

This work is licensed under the Apache 2.0 License val name by viewModel.name OutlinedTextField( value = name, onValueChange = { viewModel.onNameChange(it) }, label = { Text("Name") } ) ViewModel as source of truth class MyViewModel : ViewModel() { var name = mutableStateOf("") private set fun onNameChange(name: String) { this.name.value = name } } Link: https://developer.android.com/jetpack/compose/state#viewmodels-source-of-truth 화면 단위의 UI 상태를 관리하고, 비즈니스 로직에 접근이 필요한 경우에 사용하는 특별한 유형의 State Holder. Composition에 연관된 상태를 참조해서는 안된다. (Memory Leak 발생 가능)

Slide 12

Slide 12 text

This work is licensed under the Apache 2.0 License Defining source of truth Link: https://developer.android.com/jetpack/compose/state#managing-state

Slide 13

Slide 13 text

This work is licensed under the Apache 2.0 License class MyViewModel : ViewModel() { private val _name = MutableStateFlow("") val name: StateFlow get() = _name } val name by viewModel.name.collectAsState() Consuming LiveData or Flow class MyViewModel : ViewModel() { private val _name = MutableLiveData("") val name: LiveData get() = _name } val name by viewModel.name.observeAsState("") LiveData Flow Link: https://developer.android.com/jetpack/compose/state#use-other-types-of-state-in-jetpack-compose

Slide 14

Slide 14 text

This work is licensed under the Apache 2.0 License value changed -> Side-effect 내부에서 발생하면 Recomposition을 예측하기 어려워서, Composable 내부에서는 side-effect가 없어야 한다. 화면 이동과 같은 일회성 이벤트를 처리할 때, side-effect가 필요한 경우에는 Effect API를 사용한다. Link: https://developer.android.com/jetpack/compose/side-effects produceState snapshotFlow LaunchedEffect rememberCoroutineScope DisposableEffect SideEffect rememberUpdatedState derivedStateOf CoroutineScope NOT CoroutineScope -> awaitDispose{} remember in Composable outside Composable -> onDispose{} value changed -> every recomposition ->

Slide 15

Slide 15 text

This work is licensed under the Apache 2.0 License LaunchedEffect if (state.hasError) { LaunchedEffect(scaffoldState.snackbarHostState) { scaffoldState.snackbarHostState.showSnackbar( message = "Error message", actionLabel = "Retry message" ) } } 코루틴을 호출할 수 있는 Composable 함수. 키가 변경되면 기존 코루틴이 취소되고 새 코루틴에서 suspend 함수가 실행된다. composition을 종료하면 코루틴이 취소된다. Link: https://developer.android.com/jetpack/compose/side-effects#launchedeffect

Slide 16

Slide 16 text

This work is licensed under the Apache 2.0 License rememberCoroutineScope val scope = rememberCoroutineScope() Button( onClick = { scope.launch { scaffoldState.snackbarHostState.showSnackbar("Something happened!") } } ) { Text("Press me") } Link: https://developer.android.com/jetpack/compose/side-effects#remembercoroutinescope 호출되는 composition에 바인딩된 CoroutineScope를 반환하는 Composable 함수. composition을 종료하면 자동으로 취소된다.

Slide 17

Slide 17 text

This work is licensed under the Apache 2.0 License rememberUpdatedState @Composable fun LandingScreen(onTimeout: () -> Unit) { val currentOnTimeout by rememberUpdatedState(onTimeout) LaunchedEffect(Unit) { delay(SplashWaitTimeMillis) currentOnTimeout() } /* content */ } Link: https://developer.android.com/jetpack/compose/side-effects#rememberupdatedstate recomposition마다 값을 업데이트해주는 Composable 함수. 재생성/재시작 비용이 많이 들거나 금지된, 오래걸리는 작업에 사용하기 적합하다.

Slide 18

Slide 18 text

This work is licensed under the Apache 2.0 License DisposableEffect DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } Link: https://developer.android.com/jetpack/compose/side-effects#disposableeffect 키가 변경되거나 composition을 종료할 때, 정리가 필요하면 사용하는 Composable 함수.

Slide 19

Slide 19 text

This work is licensed under the Apache 2.0 License SideEffect @Composable fun rememberAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { /* ... */ } SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics } Link: https://developer.android.com/jetpack/compose/side-effects#sideeffect-publish composition될 때마다 호출되는 Composable 함수. Compose에서 관리하지 않는 객체에 Compose 상태를 공유할 때 주로 사용한다.

Slide 20

Slide 20 text

This work is licensed under the Apache 2.0 License produceState val imageState by produceState>(initialValue = Result.Loading, url, imageRepository) { val image = imageRepository.load(url) value = if (image == null) Result.Error else Result.Success(image) awaitDispose { /* ... */ } } Link: https://developer.android.com/jetpack/compose/side-effects#producestate 비동기로 생성된 값을 observable State로 반환하는 Composable 함수. Flow, LiveData, RxJava와 같은 구독 기반 상태를 Compose State로 변환할 수 있다. composition을 떠날 때 코루틴이 취소되고, 값이 달라질 때만 recomposition된다. 구독을 제거할 때는 awaitDispose 함수를 사용한다.

Slide 21

Slide 21 text

This work is licensed under the Apache 2.0 License derivedStateOf val showButton = listState.firstVisibleItemIndex > 0 // X val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } Link: https://developer.android.com/jetpack/compose/side-effects#derivedstateof 계산 결과를 State로 반환하는 Composable 함수. 계산에 사용된 상태 중 하나가 변경될 때마다 다시 계산하고, 계산 결과는 캐싱된다.

Slide 22

Slide 22 text

This work is licensed under the Apache 2.0 License snapshotFlow val editableUserInputState = rememberEditableUserInputState(hint = "Choose Destination") val currentOnDestinationChanged by rememberUpdatedState(onToDestinationChanged) LaunchedEffect(editableUserInputState) { snapshotFlow { editableUserInputState.text } .filter { !editableUserInputState.isHint } .collect { currentOnDestinationChanged(editableUserInputState.text) } } Link: https://developer.android.com/jetpack/compose/side-effects#snapshotFlow Compose State를 Flow로 변환해주는 Composable 함수. snapshotFlow 블록 내에서 읽은 State 객체 하나의 값이 변경되면, Flow가 새로운 값을 방출한다.

Slide 23

Slide 23 text

This work is licensed under the Apache 2.0 License Jetpack Compose에서 Navigation 라이브러리를 사용하는 방법을 알아봅니다. Navigation Codelab Link: https://developer.android.com/codelabs/jetpack-compose-navigation#0

Slide 24

Slide 24 text

This work is licensed under the Apache 2.0 License NavBackStackEntry Structure NavHost NavGraph NavGraph NavDestination NavDestination NavController visibleEntries Composable Composable destination

Slide 25

Slide 25 text

This work is licensed under the Apache 2.0 License NavController val navController = rememberNavController() 앱 화면을 구성하는 Composable의 Back Stack과 각 화면의 상태를 추적한다. Stateful. Link: https://developer.android.com/jetpack/compose/navigation#getting-started

Slide 26

Slide 26 text

This work is licensed under the Apache 2.0 License NavController를 이용하여 탐색하는 컨테이너. Composable 간에 이동하면 NavHost의 컨텐츠가 재구성(recomposed). Kotlin DSL 구문으로 NavGraph를 정의한다. NavHost NavHost(navController = navController, startDestination = "profile") { composable("profile") { Profile(/*...*/) } composable("friendslist") { FriendsList(/*...*/) } /*...*/ navigation(...) { composable(...) { /*...*/ } } } = Nested NavGraph = NavDestination Link: https://developer.android.com/jetpack/compose/navigation#create-navhost

Slide 27

Slide 27 text

This work is licensed under the Apache 2.0 License Navigate to a composable navController.navigate("friendslist") // "android-app://androidx.navigation/$route" NavGraph에서 Composable Destination으로 이동하려면, route와 함께 navigate 함수를 호출한다. 실제로는 딥링크로 이동한 것처럼 다뤄진다. Link: https://developer.android.com/jetpack/compose/navigation#nav-to-composable

Slide 28

Slide 28 text

This work is licensed under the Apache 2.0 License Navigate with arguments NavHost(...) { composable( "profile/{userId}", arguments = listOf(navArgument("userId") { type = NavType.StringType }) ) { backStackEntry -> Profile(navController, backStackEntry.arguments?.getString("userId")) } } navController.navigate("profile/user1234") 이동할 때 argument를 전달하려면 딥링크처럼 route에 placeholder를 추가해야 한다. 실수로 $를 붙이지 않도록 주의할 것. Link: https://developer.android.com/jetpack/compose/navigation#nav-with-args

Slide 29

Slide 29 text

This work is licensed under the Apache 2.0 License Thank You! Link: https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-3