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

Compose 첫 걸음 || Compose Migration

Ju Hyung Park
September 12, 2023

Compose 첫 걸음 || Compose Migration

DroidKnights 발표자료

Ju Hyung Park

September 12, 2023
Tweet

Transcript

  1. Contents ❏ Compose 원리 (Composition, Layout, Drawing, etc) ❏ Compose

    + XML (Compose Migration) ❏ Compose + XML에서 BottomSheet 적용기
  2. Android 레이아웃 시스템 변천사 2008 ~ 2010 2013 2014 2015

    2016 2017 2018 2019 2020 Future findViewById & Casting ButterKnife Data Binding Kotlin Synthetic Generic findViewById View Binding Jetpack Compose
  3. ViewModel private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList()) val suggestedDestinations: StateFlow<List<ExploreModel>> =

    _suggestedDestinations.asStateFlow() 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 } } } https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects
  4. ViewModel private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList()) val suggestedDestinations: StateFlow<List<ExploreModel>> =

    _suggestedDestinations.asStateFlow() 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 } } } https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects
  5. ViewModel private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList()) val suggestedDestinations: StateFlow<List<ExploreModel>> =

    _suggestedDestinations.asStateFlow() 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 } } } https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects
  6. ViewModel private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList()) val suggestedDestinations: StateFlow<List<ExploreModel>> =

    _suggestedDestinations.asStateFlow() 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 } } } https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects
  7. 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 ) ... https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects
  8. 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 ) ... https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects
  9. 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 ) ... https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects
  10. 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 ) ... https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects
  11. 컴포즈를 선택해야하는 이유는..? ❏ Kotlin 코드로 UI 작성 ❏ Adapter,

    ViewHolder패턴 안녕! ❏ 애니메이션 구현을 더 쉽게! ❏ 구성요소의 재사용 용이
  12. Compose, 일단 작성해봅시다 ❏ 요구사항 ❏ 안드로이드 버전별 정보 리스트

    ❏ “선택하기" 버튼 하단 고정 ❏ 리스트 중 한 개만 선택가능 ❏ 선택 시 하이라이트 기능 ❏ 선택 시 “선택하기" 버튼 활성화 ❏ 같은 아이템 클릭 시 하이라이트 & 버튼 초기화
  13. @Composable fun AndroidVersionInfoItem( info: AndroidVersionInfo ) { Row { ...

    Image(painterResource(id = info.versionImageRes)) Column(...) { Text(text = "Version Name : ${info.versionName}") Text(text = "Version Number : ${info.versionNumber}, API ...") } } }
  14. @Composable fun AndroidVersionInfoItem( info: AndroidVersionInfo ) { Row { ...

    Image(painterResource(id = info.versionImageRes)) Column(...) { Text(text = "Version Name : ${info.versionName}") Text(text = "Version Number : ${info.versionNumber}, API ...") } } }
  15. suspend fun foo() { ... } val foo = suspend

    { ... } fun foo(param: suspend () -> Unit) { ... }
  16. suspend fun foo() { ... } val foo = suspend

    { ... } fun foo(param: suspend () -> Unit) { ... }
  17. @MustBeDocumented @Retention(AnnotationRetention.BINARY) @Target( // val foo = @Composable { ...

    } AnnotationTarget.FUNCTION, // foo: @Composable () -> Unit AnnotationTarget.TYPE, // foo: (@Composable () -> Unit) -> Unit AnnotationTarget.TYPE_PARAMETER, // composable property getters and setters // val foo: Int @Composable get() { ... } // var bar: Int // @Composable get() { ... } AnnotationTarget.PROPERTY_GETTER ) annotation class Composable
  18. suspend fun foo() { ... } val foo = suspend

    { ... } fun foo(param: suspend () -> Unit) { ... }
  19. @Composable fun foo() { ... } val foo = @Composable

    { ... } fun foo(param: @Composable () -> Unit) { ... }
  20. // Kotlin suspend function suspend fun foo(): String { ...

    } // Continuation.kt public interface Continuation<in T> { public val context: CoroutineContext public fun resumeWith(result: Result<T>) } Decompile
  21. // Kotlin suspend function suspend fun foo(): String { ...

    } // Continuation.kt public interface Continuation<in T> { public val context: CoroutineContext public fun resumeWith(result: Result<T>) } Decompile @Composable fun foo() { /* Context ??? */ }
  22. // StringResources.android.kt @Composable @ReadOnlyComposable fun stringResource(@StringRes id: Int): String {

    val resources = resources() return resources.getString(id) } @Composable @ReadOnlyComposable internal fun resources(): Resources { LocalConfiguration.current return LocalContext.current.resources }
  23. // StringResources.android.kt @Composable @ReadOnlyComposable fun stringResource(@StringRes id: Int): String {

    val resources = resources() return resources.getString(id) } @Composable @ReadOnlyComposable internal fun resources(): Resources { LocalConfiguration.current return LocalContext.current.resources }
  24. public fun ComponentActivity.setContent( parent: CompositionContext? = null, content: @Composable ()

    -> Unit ) { val existingComposeView = window.decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? ComposeView if (existingComposeView != null) with(existingComposeView) { ... setContent(content) } else ComposeView(this).apply { ... setContent(content) ... } }
  25. public fun ComponentActivity.setContent( parent: CompositionContext? = null, content: @Composable ()

    -> Unit ) { val existingComposeView = window.decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? ComposeView if (existingComposeView != null) with(existingComposeView) { ... setContent(content) } else ComposeView(this).apply { ... setContent(content) ... } }
  26. internal fun AbstractComposeView.setContent( parent: CompositionContext, content: @Composable () -> Unit

    ): Composition { GlobalSnapshotManager.ensureStarted() val composeView = if (childCount > 0) { getChildAt(0) as? AndroidComposeView } else { removeAllViews(); null } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) } return doSetContent(composeView, parent, content) }
  27. internal fun AbstractComposeView.setContent( parent: CompositionContext, content: @Composable () -> Unit

    ): Composition { GlobalSnapshotManager.ensureStarted() val composeView = if (childCount > 0) { getChildAt(0) as? AndroidComposeView } else { removeAllViews(); null } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) } return doSetContent(composeView, parent, content) }
  28. @Composable @OptIn(ExperimentalComposeUiApi::class) internal fun ProvideAndroidCompositionLocals( owner: AndroidComposeView, content: @Composable ()

    -> Unit ) { val view = owner val context = view.context ... CompositionLocalProvider( LocalConfiguration provides configuration, LocalContext provides context, LocalLifecycleOwner provides viewTreeOwners.lifecycleOwner, LocalSavedStateRegistryOwner provides viewTreeOwners.savedStateRegistryOwner, LocalSaveableStateRegistry provides saveableStateRegistry, LocalView provides owner.view, LocalImageVectorCache provides imageVectorCache ) { ... } }
  29. @Composable @OptIn(ExperimentalComposeUiApi::class) internal fun ProvideAndroidCompositionLocals( owner: AndroidComposeView, content: @Composable ()

    -> Unit ) { val view = owner val context = view.context ... CompositionLocalProvider( LocalConfiguration provides configuration, LocalContext provides context, LocalLifecycleOwner provides viewTreeOwners.lifecycleOwner, LocalSavedStateRegistryOwner provides viewTreeOwners.savedStateRegistryOwner, LocalSaveableStateRegistry provides saveableStateRegistry, LocalView provides owner.view, LocalImageVectorCache provides imageVectorCache ) { ... } }
  30. @Composable @OptIn(ExperimentalComposeUiApi::class) internal fun ProvideAndroidCompositionLocals( owner: AndroidComposeView, content: @Composable ()

    -> Unit ) { val view = owner val context = view.context ... CompositionLocalProvider( LocalConfiguration provides configuration, LocalContext provides context, LocalLifecycleOwner provides viewTreeOwners.lifecycleOwner, LocalSavedStateRegistryOwner provides viewTreeOwners.savedStateRegistryOwner, LocalSaveableStateRegistry provides saveableStateRegistry, LocalView provides owner.view, LocalImageVectorCache provides imageVectorCache ) { ... } }
  31. @Composable fun AndroidVersionInfoItem(...) { Row { Image(...) Column(...) { Text(...)

    Text(...) } } } AndroidVersionInfoItem Row Image Column Text Text
  32. @Composable fun AndroidVersionInfoItem(...) { Row { Image(...) Column(...) { Text(...)

    Text(...) } } } AndroidVersionInfoItem Row Image Column Text Text AndroidVersionInfoItem Row Layout Image Layout Column Layout Text BasicText CoreText Layout Text BasicText CoreText Layout
  33. AndroidVersionInfoItem Row Image Column Text Text 1 measure 2 measure

    4 measure 5 measure 3 size 6 size 7 measure 8 size 1 2 4 5 7
  34. AndroidVersionInfoItem Row Image Column Text Text 1 measure 2 measure

    4 measure 5 measure 7 measure 10 size 3 size 9 size 6 size 8 size 1 2 4 5 7
  35. AndroidVersionInfoItem Row Image Column Text Text 1 measure 2 measure

    4 measure 5 measure 7 measure 10 size 3 size 9 size 6 size 8 size place place place place place 1 2 4 5 7
  36. Modifier ❏ 컴포저블 크기, 레이아웃, 동작, 및 모양 변경 ❏

    정보 추가 (ex. 접근성라벨) ❏ 사용자 입력 처리 ❏ Clickable, Scrollable, Draggable, 확대/축소등 상호작용
  37. Modifier ❏ 컴포저블 크기, 레이아웃, 동작, 및 모양 변경 ❏

    정보 추가 (ex. 접근성라벨) ❏ 사용자 입력 처리 ❏ Clickable, Scrollable, Draggable, 확대/축소등 상호작용 ❏ Tree의 리프노드인 Layout Composable에 Modifier 적용
  38. @Composable fun BottomButtons(isEnabled: Boolean = false) { Row( horizontalArrangement =

    Arrangement.Center, modifier = Modifier .fillMaxWidth() .background(Color.White) .padding(5.dp) ) { Button( onClick = { /*TODO*/ }, contentPadding = PaddingValues(12.dp), modifier = Modifier.size(130.dp, 45.dp), enabled = isEnabled ) { Text("선택하기") } } }
  39. Box ( modifier = Modifier .fillMaxSize() .wrapContentSize() .size(50.dp) .background(Color.Blue) )

    https://youtu.be/zMKMwh9gZuI?t=738 measure w:0-200, h:0-300 measure w:200, h:300
  40. Box ( modifier = Modifier .fillMaxSize() .wrapContentSize() .size(50.dp) .background(Color.Blue) )

    https://youtu.be/zMKMwh9gZuI?t=738 measure w:0-200, h:0-300 measure w:200, h:300 measure w:0-200, h:0-300
  41. Box ( modifier = Modifier .fillMaxSize() .wrapContentSize() .size(50.dp) .background(Color.Blue) )

    https://youtu.be/zMKMwh9gZuI?t=738 measure w:0-200, h:0-300 measure w:200, h:300 measure w:0-200, h:0-300 w:50, h:50
  42. Box ( modifier = Modifier .fillMaxSize() .wrapContentSize() .size(50.dp) .background(Color.Blue) )

    https://youtu.be/zMKMwh9gZuI?t=738 measure w:0-200, h:0-300 measure w:200, h:300 measure w:0-200, h:0-300 w:50, h:50 50*50 place 200*300 place 50*50 place
  43. 드디어 컴포즈 첫 적용! ❏ 당장은 서비스화면 도입은 부담 ❏

    앱 내 개발자메뉴..? ❏ 기존 XML 레이아웃 시스템에서 Compose를 어떻게 올릴 수 있을까?
  44. 마이그레이션으로 얻을 수 있는것들 (희망편 😍) ❏ 컴포즈로 복잡한 View와

    로직을 단순화 시킬 수 있었다 ❏ 컴포즈 라이브러리를 추가하면서 오히려 앱 사이즈가 줄었다?! ❏ 커스텀 UI 작성하는것이 매우 편해졌다 ❏ 컴포즈가 제공해주는 프리뷰기능은 기존 XML 프리뷰보다 훌륭하다 ❏ 코틀린으로 작성이 가능, 상대적으로 낮은 진입장벽
  45. 마이그레이션으로 얻을 수 있는것들 (절망편 😱) ❏ 너무 새로워서 아직

    몇몇 UI가 XML View 기능이 구현되지 않았었다 (지금은 많은 부분이 해소) ❏ Debug모드 시 컴포즈 퍼포먼스 차이가 커서 Debug의 어려움이 있음 ❏ 사용한 UI컴포넌트의 뜻하지 않은 버그 (핫픽스 지름길) ❏ 컴포즈 버전을 올리기 위해서는 매칭되는 코틀린 버전 변경 필요 ❏ 눈으로 일일이 확인할 수 없는 ReComposition 카운트는 오히려 코드를 다 작성한 후 자꾸 신경쓰이게 됨 ❏ XML + Compose 구조이다보니 가끔 고려하지 못한 리팩토링으로 개발공수가 늘어남
  46. //Fragment, Navigation 구성요소 사용중인 경우 class NewFeatureFragment : Fragment() {

    override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy. DisposeOnViewTreeLifecycleDestroyed) setContent { NewFeatureScreen() } } } } https://developer.android.com/jetpack/compose/interop/migration-strategy?hl=ko
  47. // ViewBinding 사용, 기존 View Layout 사용 시 <?xml version="1.0"

    encoding="utf-8"?> <LinearLayout ...> <TextView.../> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
  48. // ViewBinding 사용, 기존 View Layout 사용 시 class ExampleFragment

    : Fragment() { ... override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentExampleBinding.inflate(inflater, container, false) val view = binding.root binding.composeView.apply { setViewCompositionStrategy(ViewCompositionStrategy .DisposeOnViewTreeLifecycleDestroyed) setContent { ... } } return view } ... }
  49. // Compose UI 내 Android View 계층구조 사용 시 //

    (Compose에서 아직 사용하지 못하는 View 사용 시) @Composable fun CustomView() { AndroidView( modifier = ... factory = { context -> MyView(context) }, update = { view -> ... } ) }
  50. 제약사항 XML View <LinearLayout ...> <UnityView .../> <TextView .../> <View

    .../> </LinearLayout> ❏ 당장 기능화면을 ComposeView 로 변경할 수 없음
  51. 제약사항 XML View <LinearLayout ...> <UnityView .../> <TextView .../> <View

    .../> </LinearLayout> ❏ 당장 기능화면을 ComposeView 로 변경할 수 없음 ❏ ModalBottomSheetLayout을 사용할 수 없음
  52. 제약사항 ❏ 당장 기능화면을 ComposeView 로 변경할 수 없음 ❏

    ModalBottomSheetLayout을 사용할 수 없음 ❏ 다른 화면에서도 사용할 수 있는 BottomSheet로, 한 화면에 한정X (대부분 XML View) BottomSheetDialogFragment
  53. 제약사항 ❏ 당장 기능화면을 ComposeView 로 변경할 수 없음 ❏

    ModalBottomSheetLayout을 사용할 수 없음 ❏ 다른 화면에서도 사용할 수 있는 BottomSheet로, 한 화면에 한정X (대부분 XML View) ❏ 어떻게든 컴포즈를 BottomSheet에 적용하고 싶음 BottomSheetDialogFragment
  54. 이슈 (BottomSheetDialogFragment + Compose) ❏ BottomSheet 상단 Anchor 이슈 ❏

    Compose ModalBottomSheet와 BottomSheetDialogFragment 혼용 이슈
  55. Column { ImageView(...) Row { Text(text = "Title") } LazyColumn

    { ... } } BottomSheetDialogFragment LazyColumn Title Expandable
  56. Column { ImageView(...) Row { Text(text = "Title") } LazyColumn

    { ... } } BottomSheetDialogFragment LazyColumn Title Expandable
  57. Column { ImageView(...) Row { Text(text = "Title") } LazyColumn

    { ... } } BottomSheetDialogFragment LazyColumn Title Expandable
  58. Column { LazyColumn { item { ImageView(...) } item {

    Text(text = "Title") } } LazyColumn { ... } } BottomSheetDialogFragment LazyColumn Title Expandable
  59. ❏ ModalBottomSheet는 ComposeView의 Window 즉, 디바이스 Window하위 ComposeView 내 노출하기

    때문? ❏ BottomSheetDialogFragment는 addFragment 형태로 노출되며 ComposeView보다 상위 Window에서 노출하기 때문? ModalBottomSheet/BottomSheetDialogFragment 혼용
  60. Wrap Up ❏ Compose, 지난 레이아웃의 다른 형태로 제공 ❏

    트렌드의 변화 (명령형 UI -> 선언형 UI) ❏ 러닝커브가 낮은 편 ❏ 생산성 효율 Up ❏ 한 번만 써본 사람은 없을정도 ❏ 원리는 몰라도 좋지만 도움은 된다 ❏ Compose 첫 걸음은, Migration 부터