$30 off During Our Annual Pro Sale. View Details »

Compose Camp 22KR Pathway3

Compose Camp 22KR Pathway3

2022년도 한국 Compose Camp에서의 Pathway3 슬라이드입니다.
https://developersonair.withgoogle.com/events/composecamp_22kr

유튜브 영상입니다.
https://www.youtube.com/watch?v=XhKRISZRyHE

Sungyong An

October 25, 2022
Tweet

More Decks by Sungyong An

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  4. 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

    View Slide

  5. 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

    View Slide

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

    View Slide

  7. 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

    View Slide

  8. 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

    View Slide

  9. 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

    View Slide

  10. 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로 분리하는 것을 권장한다.

    View Slide

  11. 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 발생 가능)

    View Slide

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

    View Slide

  13. 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

    View Slide

  14. 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 ->

    View Slide

  15. 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

    View Slide

  16. 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을 종료하면 자동으로 취소된다.

    View Slide

  17. 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 함수.
    재생성/재시작 비용이 많이 들거나 금지된, 오래걸리는 작업에 사용하기 적합하다.

    View Slide

  18. 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 함수.

    View Slide

  19. 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 상태를 공유할 때 주로 사용한다.

    View Slide

  20. 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 함수를 사용한다.

    View Slide

  21. 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 함수.
    계산에 사용된 상태 중 하나가 변경될 때마다 다시 계산하고, 계산 결과는 캐싱된다.

    View Slide

  22. 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가 새로운 값을 방출한다.

    View Slide

  23. 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

    View Slide

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

    View Slide

  25. 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

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide