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

Droidknights 2023 - 이상훈 - 기존 앱을 Jetpack Compose...

Dora Lee
September 12, 2023

Droidknights 2023 - 이상훈 - 기존 앱을 Jetpack Compose로 마이그레이션 하기

기존 안드로이드 뷰 시스템 (XML, RecyclerView 등) 으로 이루어진 앱을 Compose로 Step by Step으로 천천히 마이그레이션 하는 방법을 다룹니다. 또한, 기존 뷰 시스템에서 제공중인 UI 컴포넌트를 Compose에서 똑같이 사용할 수 있는지, 어떤 컴포넌트는 없어서 만들어서 사용해야 하는지 등도 함께 다룹니다.

Dora Lee

September 12, 2023
Tweet

More Decks by Dora Lee

Other Decks in Technology

Transcript

  1. Index ❏ 1. 기존 앱 → Compose 마이그레이션 전략 ❏

    2. 마이그레이션 하기 전 Compose 환경 만들기 ❏ 3. Compose용 디자인 시스템 구축하기 (a.k.a 공통 컴포넌트) ❏ 4. 실제 마이그레이션 하기 ❏ Case 1. 아예 새로운 화면을 만들 때 ❏ Case 2. 기존 XML 뷰에 Compose 뷰 추가하기 혹은 기존 컴포넌트 교체하기 ❏ Case 3. RecyclerView를 컴포즈 리스트로 교체하기 (ViewHolder 아이템부터 전체 뷰까지) ❏ Case 4. Compose 에서 지원하지 않는 뷰 (ex. TextureView / VideoPlayer) 호스팅 하기 ❏ Case 5. 기타 안드로이드 구성요소 (ex. Context, Dialog, BottomSheet)를 Compose에서 사용하기
  2. Compose를 도입하기로 했는데... 방향성을 못잡겠다면? ❏ 기존 뷰 시스템 (ex.

    XML Based View Layout, Fragment etc…) 을 사용하는 우리 앱에서 Compose를 도입할까 하는데 어떡하죠?
  3. Compose를 도입하기로 했는데... 방향성을 못잡겠다면? ❏ 기존 뷰 시스템 (ex.

    XML Based View Layout, Fragment etc…) 을 사용하는 우리 앱에서 Compose를 도입할까 하는데 어떡하죠? ❏ 점진적으로 우리 앱은 Compose 기반의 앱으로 마이그레이션 할 계획이 있어요
  4. Compose로 한번에 마이그레이션 할 생각 하지말기 ❏ 이러한 고민은 이미

    Compose 개발 팀에서 충분히 고려됨 ❏ Compose는 기존 안드로이드의 뷰 시스템과 호환 가능하도록 설계됨 ❏ Compose에 대한 이해가 개인 / 팀 차원에서 얼라인 되어야 원활하게 마이그레이션 가능 ❏ 사람마다 개개인이 느끼는 난이도와 학습 속도가 다름 ❏ 그렇기 때문에 한번에 우다다 바꾸고 팀과 싱크가 맞지 않으면 유지보수에 어려울 수도 있음 ❏ 이건 Compose 도입 차원 뿐만 아니라 새로운 기술을 사용하여 기존 코드를 마이그레이션 하는 모든 경우에 해당
  5. 부담없이 Compose로 천천히 마이그레이션 하는 전략 ❏ Step 1. 새로운

    화면에서 Compose 사용하기 ❏ Step 2. 기존 뷰 시스템 (not RecyclerView) 으로 이루어진 화면에서 새로운 컴포넌트만 Compose 사용해서 코드베이스 공존시키기
  6. 부담없이 Compose로 천천히 마이그레이션 하는 전략 ❏ Step 1. 새로운

    화면에서 Compose 사용하기 ❏ Step 2. 기존 뷰 시스템 (not RecyclerView) 으로 이루어진 화면에서 새로운 컴포넌트만 Compose 사용해서 코드베이스 공존시키기 ❏ Step 3. 기존 뷰 시스템으로 이루어진 컴포넌트를 Compose 컴포넌트로 교체하기
  7. 부담없이 Compose로 천천히 마이그레이션 하는 전략 ❏ Step 1. 새로운

    화면에서 Compose 사용하기 ❏ Step 2. 기존 뷰 시스템 (not RecyclerView) 으로 이루어진 화면에서 새로운 컴포넌트만 Compose 사용해서 코드베이스 공존시키기 ❏ Step 3. 기존 뷰 시스템으로 이루어진 컴포넌트를 Compose 컴포넌트로 교체하기 ❏ Step 4. RecyclerView의 각 ViewHolder Item을 Compose 컴포넌트로 교체하기
  8. 부담없이 Compose로 천천히 마이그레이션 하는 전략 ❏ Step 1. 새로운

    화면에서 Compose 사용하기 ❏ Step 2. 기존 뷰 시스템 (not RecyclerView) 으로 이루어진 화면에서 새로운 컴포넌트만 Compose 사용해서 코드베이스 공존시키기 ❏ Step 3. 기존 뷰 시스템으로 이루어진 컴포넌트를 Compose 컴포넌트로 교체하기 ❏ Step 4. RecyclerView의 각 ViewHolder Item을 Compose 컴포넌트로 교체하기 ❏ Step 5. Step 3~4가 모두 완료됐다면 해당 화면 모두 Compose 기반으로 바꾸기
  9. build.gradle.kts android { buildFeatures { compose = true } composeOptions

    { kotlinCompilerExtensionVersion = "1.5.2" // 2023-08-24 기준 } } Compose를 위한 Gradle 설정하기
  10. build.gradle.kts dependencies { val composePlatform = platform("androidx.compose:compose-bom:2023.08.00") implementation(composePlatform) // (선택)

    Compose 테스트 할 경우 androidTestImplementation(composePlatform) // 이하 생략 } Compose를 위한 Gradle 설정하기 (의존성)
  11. build.gradle.kts dependencies { // (optional) Material3 사용 할 경우 implementation("androidx.compose.material3:material3")

    // (optional) Material2 사용 할 경우 implementation("androidx.compose.material:material") // Material Design 사용하지 않고 직접 만들 경우 implementation("androidx.compose.foundation:foundation") // Compose UI implementation("androidx.compose.ui:ui") } Compose를 위한 Gradle 설정하기 (의존성)
  12. build.gradle.kts dependencies { // (optional) Activity <-> Compose 상호작용 implementation("androidx.activity:activity-compose:1.7.3")

    // (optional) Compose에서 ViewModel 사용하기 implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") // (optional) Compose에서 LiveData 사용하기 implementation("androidx.compose.runtime:runtime-livedata") // (optional) Compose에서 RxJava 사용하기 implementation("androidx.compose.runtime:runtime-rxjava2") } Compose를 위한 Gradle 설정하기 (의존성)
  13. Compose 테마 시스템 만들기 ❏ 컬러 팔레트 ❏ 폰트 (Typography)

    ❏ 컴포즈 용 앱 자체 테마 (Material 2 or Material 3)
  14. Compose 사용 환경 만들기 ❏ 앱에서 여러 개의 Activity /

    Fragment를 사용한다고 가정했을 때 ❏ 컬러 팔레트(Light Mode / Dark Mode) / Typography 등을 일괄적으로 적용할 수 있는 Base Activity, Base Fragment를 만들어서 Compose를 사용하는 모든 화면에 대해서 적용하기
  15. Compose 사용 환경 만들기 ❏ ex. 기존 레이아웃 시스템을 사용하는

    화면 (Activity / Fragment...) ❏ BaseActivity, BaseFragment ❏ 데이터 바인딩을 사용하는 화면인 경우 ❏ (제 경험상) BaseBindingActivity, BaseBindingFragment
  16. Compose 사용 환경 만들기 ❏ ex. Compose를 사용하는 화면 (Activity

    / Fragment…) ❏ BaseComposeActivity, BaseComposeFragment ❏ (제 경험상) 만약 ʻ우리 팀이 Compose가 이제 기본이다’ 라는 의사결정이 내린 상황이라면 ❏ Compose를 사용하는 화면의 베이스 클래스의 이름을 BaseActivity / BaseFragment 같은걸로 하고 ❏ 데이터바인딩을 사용하는 화면을 BaseBindingActivity / BaseBindingFragment 같은걸로 설정했었음
  17. BaseComposeActivity.kt abstract class BaseComposeActivity: ComponentActivity() { @Composable abstract fun Content()

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) /// 생략 setContent { Content() } } } Compose 사용 환경 만들기
  18. BaseComposeFragment.kt abstract class BaseComposeFragment : Fragment() { @Composable abstract fun

    Content() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle? ): View? { return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { Content() } } } } Compose 사용 환경 만들기
  19. Compose 테마 시스템 만들기 - 색상 팔레트 ❏ 기존에 정의되어

    있는 XML Resources → Compose Color System으로 재정의 하기
  20. colors.xml <resource> <color name="primaryRed">#00ffff</color> <color name="primaryOrange">#ffa500</color> <color name="primaryGreen">#008000</color> </resource> Compose

    테마 시스템 만들기 - 색상 팔레트 Theme.kt val PrimaryRed = Color(0xff00ffff) val PrimaryOrange = Color(0xffffa500) val PrimaryGreen = Color(0xff008000)
  21. Compose 테마 시스템 만들기 - 색상 팔레트 (m2) Theme.kt val

    PrimaryRed = Color(0xff00ffff) val PrimaryOrange = Color(0xffffa500) val PrimaryGreen = Color(0xff008000) private val DarkColors = darkColors( primary = PrimaryRed, secondary = PrimaryOrange, // ... ) private val LightColors = lightColors( primary = PrimaryRed100, primaryVariant = Yellow400, // ... ) 다크 모드 팔레트 라이트 모드 팔레트 정의
  22. Compose 테마 시스템 만들기 - 색상 팔레트 (m3) Theme.kt val

    PrimaryRed = Color(0xff00ffff) val PrimaryOrange = Color(0xffffa500) val PrimaryGreen = Color(0xff008000) private val DarkColors = darkColorScheme( primary = PrimaryRed, onPrimary = PrimaryOrange, // ... ) private val LightColors = lightColorScheme( primary = PrimaryRed100, onPrimary = Yellow400, // ... ) 다크 모드 팔레트 라이트 모드 팔레트 정의
  23. Compose 테마 시스템 만들기 - 색상 팔레트 Theme.kt private val

    DarkColors = darkColors(...) private val LightColors = lightColors(...) @Composable fun MyAppTheme( content: @Composable () -> Unit, ) { MaterialTheme( colors = if (isSystemInDarkTheme()) DarkColors else LightColors, ) { content() } } 테마를 일관적으로 적용하기 위해 공통 Compose Component 만들기
  24. Compose 테마 시스템 만들기 - 색상 팔레트 Theme.kt private val

    DarkColors = darkColors(...) private val LightColors = lightColors(...) @Composable fun MyAppTheme( content: @Composable () -> Unit, ) { MaterialTheme( colors = if (isSystemInDarkTheme()) DarkColors else LightColors, ) { content() } } isSystemInDarkTheme() 시스템이 현재 다크모드로 설정되어있는지 가져오는 Compose 함수
  25. Compose 테마 시스템 만들기 - 폰트 (Typography) ❏ 기존에 정의되어

    있는 XML Resources → Compose Typography System으로 재정의 하기 ❏ res/fonts/* 에 들어있는 파일을 FontWeight에 따라서 Compose Typography System으로 재정의 하기
  26. Compose 테마 시스템 만들기 - 폰트 (Typography) Typography.kt internal val

    pretendard = FontFamily( Font(R.font.pretendard_normal), Font(R.font.pretendard_medium, FontWeight.W500), Font(R.font.pretendard_semibold, FontWeight.SemiBold) )
  27. Compose 테마 시스템 만들기 - 폰트 (Typography) Typography.kt internal val

    pretendard = FontFamily( Font(R.font.pretendard_normal), Font(R.font.pretendard_medium, FontWeight.W500), Font(R.font.pretendard_semibold, FontWeight.SemiBold) ) val myAppTypography = Typography( h1 = TextStyle( fontFamily = pretendard, fontWeight = FontWeight.W300, fontSize = 96.sp ), /*...*/ )
  28. Compose 테마 시스템 만들기 - 폰트 (Typography) Theme.kt @Composable fun

    MyAppTheme( content: @Composable () -> Unit, ) { MaterialTheme( colors = if (isSystemInDarkTheme()) DarkColors else LightColors, typography = myAppTypography, ) { content() } } Compose 전체 테마에 Typography 적용
  29. BaseComposeActivity.kt abstract class BaseComposeActivity: ComponentActivity() { @Composable abstract fun Content()

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) /// 생략 setContent { MyAppTheme { Content() } } } } Compose 사용 환경 만들기 (테마 시스템 적용)
  30. BaseComposeFragment.kt abstract class BaseComposeFragment : Fragment() { /// 생략 override

    fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { MyAppTheme { Content() } } } } Compose 사용 환경 만들기
  31. Compose 테마 시스템 만들기 - 자체 커스텀 테마 ❏ Material

    Theme 2 (or 3)을 사용하지 않고 직접 만드는 경우 ❏ Compose Material에 있는 MaterialTheme에서 제공하는 Typography / ColorPalette를 직접 앱의 디자인에 맞게 아예 커스텀 할 경우 → 이 경우에는 자체적인 Compose용 테마를 만들어야 함
  32. ThemeSystem.kt @Immutable data class MyAppColorPalette( val mono100: Color, val mono200:

    Color, /* ... */ val gradient: List<Color> /* ... */ ) @Immutable data class MyAppTypography( val fontFamily: FontFamily, val textStyle: TextStyle, ) Compose 자체 커스텀 테마 시스템 만들기 앱에서 사용하는 컬러 팔레트 예시 (Figma)
  33. ThemeSystem.kt val LocalColorPalette = staticCompositionLocalOf { MyAppColorPalette( mono100 = Color.Unspecified,

    mono200 = Color.Unspecified, // ... gradient = emptyList() ) } val LocalTypography = staticCompositionLocalOf { MyAppTypography( fontFamily = FontFamily.Default, textStyle = TextStyle.Default ) } Compose 자체 커스텀 테마 시스템 만들기
  34. Theme.kt (Page 1/2) @Composable fun MyAppTheme( content: @Composable () ->

    Unit ) { val colorPalette = MyAppColorPalette( mono100 = Color(0xff000000), // ... ) val typography = MyAppTypography( fontFamily = pretendard, textStyle = TextStyle(fontSize = 18.sp) ) //… Compose 자체 커스텀 테마 시스템 만들기
  35. Theme.kt (Page 2/2) @Composable fun MyAppTheme( content: @Composable () ->

    Unit ) { /* ... */ CompositionLocalProvider( LocalColorPalette provides colorSystem, LocalTypography provides typographySystem, /* ... */ content = content ) } Compose 자체 커스텀 테마 시스템 만들기
  36. MyAppTheme.kt object MyAppTheme { val colors: MyAppColorPalette @Composable get() =

    LocalColorPalette.current val typography: CustomTypography @Composable get() = LocalTypography.current } // 사용법 MyAppTheme.colors.mono100 Compose 자체 커스텀 테마 시스템 만들기
  37. Compose 용 디자인 시스템 구축하기 ❏ 기존 XML에 작성된 컴포넌트

    혹은 커스텀 뷰를 Compose에서 사용할 수 있도록 공통화 하기
  38. ArtistFollowCard.kt @Composable fun ArtistFollowCard( modifier: Modifier = Modifier, artistName: String,

    artistImageUrl: String, isFollowing: Boolean = false, onFollowButtonClick: () -> Unit, onFollowingButtonClick: () -> Unit, usingCheckIcon: Boolean = false, ) { // ... } Compose 용 디자인 시스템 구축하기 (예시)
  39. ArtistFollowCard.kt @Composable fun ArtistFollowCard( modifier: Modifier = Modifier, artistName: String,

    artistImageUrl: String, isFollowing: Boolean = false, onFollowButtonClick: () -> Unit, onFollowingButtonClick: () -> Unit, usingCheckIcon: Boolean = false, ) { // ... } Compose 용 디자인 시스템 구축하기 (예시)
  40. Compose 용 디자인 시스템 구축하기 ❏ 기존 XML에 작성된 컴포넌트

    혹은 커스텀 뷰를 Compose에서 사용할 수 있도록 공통화 하기 ❏ 처음부터 모든 컴포넌트를 Compose로 만들려고 시도하지 않기 ❏ 마이그레이션 하는 화면에서 쓰이는 컴포넌트 먼저 공통화 ❏ 새로 만드는 컴포넌트 공통화
  41. Compose 용 디자인 시스템 구축하기 - NowInAndroid ❏ android/nowinandroid 프로젝트를

    예시로 들면 :core:designsystem 이라는 모듈을 만들고, 거기서 NowInAndroid 앱에서 사용되는 공통 Compose 컴포넌트를 모아놨음
  42. Compose 용 디자인 시스템 구축하기 - NowInAndroid ❏ android/nowinandroid 프로젝트를

    예시로 들면 :core:designsystem 이라는 모듈을 만들고, 거기서 NowInAndroid 앱에서 사용되는 공통 Compose 컴포넌트를 모아놨음
  43. 마이그레이션 - I. 아예 새로운 화면 만들 때 ❏ 아까

    언급했던 Compose 기반의 신규 화면 같은 경우에는 BaseComposeActivity / BaseComposeFragment 사용하여 처음부터 Compose 로 구현하기
  44. 마이그레이션 - I. 아예 새로운 화면 만들 때 ❏ 아까

    언급했던 Compose 기반의 신규 화면 같은 경우에는 BaseComposeActivity / BaseComposeFragment 사용하여 처음부터 Compose 로 구현하기 ❏ 만들어놨던 Compose 용 디자인 컴포넌트를 활용해서 화면 만들기
  45. NewScreenWithComposeActivity.kt class NewScreenWithComposeActivity: BaseComposeActivity() { @Composable override fun Content() {

    MyAppTheme { // 배치 } } } 마이그레이션 - I. 아예 새로운 화면 만들 때
  46. NewScreenWithComposeFragment.kt class NewScreenWithComposeFragment: BaseComposeFragment() { @Composable override fun Content() {

    MyAppTheme { // 배치 } } } 마이그레이션 - I. 아예 새로운 화면 만들 때
  47. 마이그레이션 - II. 기존 XML 뷰에 Compose 뷰 추가하기 ❏

    Compose Interop API 중 XML-Based Layout 에서 사용할 수 있는 ComposeView API 지원 ㄴ androidx.compose.ui.platform.ComposeView ❏ 화면 중 일부의 컴포넌트에 대해서만 Compose로 작성하고, ComposeView로 Wrapping 하여 XML에 배치
  48. 마이그레이션 - II. 기존 XML 뷰에 Compose 뷰 추가하기 activity_mixed_compose.xml

    <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" app:action="@{viewModel.action}"> <com.google.android.material.appbar.MaterialToolbar /> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintTop_toBottomOf="@id/appbar" app:layout_constraintBottom_toBottomOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
  49. activity_mixed_compose.xml <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" app:action="@{viewModel.action}"> <com.google.android.material.appbar.MaterialToolbar /> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent"

    android:layout_height="0dp" app:layout_constraintTop_toBottomOf="@id/appbar" app:layout_constraintBottom_toBottomOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> 마이그레이션 - II. 기존 XML 뷰에 Compose 뷰 추가하기
  50. XML ComposeView 에 프리뷰 추가하기 activity_mixed_compose.xml <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height="0dp"

    app:layout_constraintTop_toBottomOf="@id/appbar" app:layout_constraintBottom_toBottomOf="parent" tools:composableName="com.example.designsystem.HelloWorldComponent.GreetingPreview" />
  51. XML ComposeView 에 프리뷰 추가하기 activity_mixed_compose.xml <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height="0dp"

    app:layout_constraintTop_toBottomOf="@id/appbar" app:layout_constraintBottom_toBottomOf="parent" tools:composableName="com.example.designsystem.HelloWorldComponent.GreetingPreview" />
  52. 마이그레이션 - II. 기존 XML 뷰에 Compose 뷰 추가하기 ExampleFragment.kt

    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 { // 여기서부터 Compose 영역 MyAppTheme { Text("여기는 Compose로 그려지는 영역입니다") } } } return view }
  53. 마이그레이션 - III. 기존 XML 뷰를 Compose 뷰로 교체하기 sample_before.xml

    <include android:id="@+id/test_included_layout" layout="@layout/component_image_single" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="@{itemModel.images.size() == 1, default = visible}" app:handler="@{handler}" />
  54. 마이그레이션 - III. 기존 XML 뷰를 Compose 뷰로 교체하기 sample_after.xml

    <androidx.compose.ui.platform.ComposeView android:id="@+id/test_included_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="@{itemModel.images.size() == 1, default = visible}" tools:composableName="com.example.feature.phoeo.SinglePhoto.SinglePhotoPreview" />
  55. 마이그레이션 - III. 기존 XML 뷰를 Compose 뷰로 교체하기 ❏

    컨테이너 기반의 레이아웃은 Compose의 Box, Column, Row 기반으로 만들기
  56. 마이그레이션 - III. 기존 XML 뷰를 Compose 뷰로 교체하기 ❏

    Column, Row 기반이면 Alignment, Arrangement 잘 사용하기
  57. 마이그레이션 - IV. 커스텀 뷰를 Compose 기반으로 만들기 ❏ 기존에

    사용했던 커스텀 뷰의 문제는 난이도가 높고, 직접 Canvas로 그려주거나 해야하는데 Canvas API가 무엇보다도 어렵고 구현해야 할 코드의 양도 많음
  58. 마이그레이션 - IV. 커스텀 뷰를 Compose 기반으로 만들기 ❏ 기존에

    사용했던 커스텀 뷰의 문제는 난이도가 높고, 직접 Canvas로 그려주거나 해야하는데 Canvas API가 무엇보다도 어렵고 구현해야 할 코드의 양도 많음 ❏ 이미 커스텀 뷰가 많은 영역에서 사용중이어서 마이그레이션에 허들을 느꼈다면 해당 커스텀 뷰 자체를 일단 Compose로 만들어서 적용하는 것도 선택지임
  59. 마이그레이션 - IV. 커스텀 뷰를 Compose 기반으로 만들기 ❏ 기존에

    사용했던 커스텀 뷰의 문제는 난이도가 높고, 직접 Canvas로 그려주거나 해야하는데 Canvas API가 무엇보다도 어렵고 구현해야 할 코드의 양도 많음 ❏ 이미 커스텀 뷰가 많은 영역에서 사용중이어서 마이그레이션에 허들을 느꼈다면 해당 커스텀 뷰 자체를 일단 Compose로 만들어서 적용하는 것도 선택지임 ❏ 커스텀 뷰의 내부 구현체를 AbstractComposeView로 변경한 후 Canvas API 혹은 ViewGroup.addView 같은 코드를 모두 Compose 컴포넌트로 변경
  60. 마이그레이션 - IV. 커스텀 뷰를 Compose 기반으로 만들기 CustomView.kt class

    CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { // 이하 생략 }
  61. 마이그레이션 - IV. 커스텀 뷰를 Compose 기반으로 만들기 CustomView.kt class

    CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AbstractComposeView(context, attrs, defStyleAttr) { // 이하 생략 }
  62. 마이그레이션 - IV. 커스텀 뷰를 Compose 기반으로 만들기 CustomView.kt class

    CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AbstractComposeView(context, attrs, defStyleAttr) { @Composable override fun Content() { CustomComponent() } }
  63. AbstractComposeView - XML 파라미터 주기 CustomView.kt class CustomView @JvmOverloads constructor(

    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AbstractComposeView(context, attrs, defStyleAttr) { var message by mutableStateOf("") @Composable override fun Content() { CustomComponent(message = message) } }
  64. AbstractComposeView - XML 파라미터 주기 CustomView.kt init { attrs?.let {

    context.obtainStyledAttributes(it, R.styleable.CustomView).run { message = getString(R.styleable.CustomView_message) recycle() } } }
  65. XML ComposeView 에 프리뷰 추가하기 activity_mixed_compose.xml <com.example.abstract.compose.example.CustomView android:id="@+id/custom_view" android:layout_width="match_parent" android:layout_height="100dp"

    app:message="XML에 파라미터 넘겼을 때 텍스트가 잘 표시되나?!" app:layout_constraintTop_toBottomOf="@id/appbar" app:layout_constraintBottom_toBottomOf="parent" />
  66. 마이그레이션 - V. RecyclerView → Compose ❏ Step 1. RecyclerView의

    각 ViewHolder를 ComposeView로 마이그레이션 ❏ Step 2. RecyclerView를 Compose의 리스트 컴포넌트로 마이그레이션 ❏ Case 1. LinearLayoutManager → LazyColumn / LazyRow ❏ Case 2. GridLayoutManager ㄴ LazyVerticalGrid / LazyHorizontalGrid ❏ Case 3. StaggeredGridLayoutManager ㄴ LazyVerticalStaggeredGrid / LazyHorizontalStaggeredGrid
  67. 마이그레이션 - V. RecyclerView → Compose ❏ Step 1. RecyclerView의

    각 ViewHolder를 ComposeView로 마이그레이션 ❏ Step 2. RecyclerView를 Compose의 리스트 컴포넌트로 마이그레이션 ❏ Case 1. LinearLayoutManager → LazyColumn / LazyRow ❏ Case 2. GridLayoutManager ㄴ LazyVerticalGrid / LazyHorizontalGrid ❏ Case 3. StaggeredGridLayoutManager ㄴ LazyVerticalStaggeredGrid / LazyHorizontalStaggeredGrid
  68. RecyclerView의 각 ViewHolder Compose화 하기 ComposeViewHolder.kt abstract class ComposeViewHolder(val composeView:

    ComposeView) : RecyclerView.ViewHolder(composeView) { init { composeView.setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool ) } @Composable abstract fun Content() fun bind() { composeView.setContent { MyAppTheme { Content() } } } }
  69. RecyclerView의 각 ViewHolder Compose화 하기 ComposeViewHolder.kt abstract class ComposeViewHolder(val composeView:

    ComposeView) : RecyclerView.ViewHolder(composeView) { init { composeView.setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool ) } @Composable abstract fun Content() fun bind() { composeView.setContent { MyAppTheme { Content() } } } }
  70. RecyclerView의 각 ViewHolder Compose화 하기 ComposeViewHolder.kt abstract class ComposeViewHolder(val composeView:

    ComposeView) : RecyclerView.ViewHolder(composeView) { init { composeView.setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool ) } @Composable abstract fun Content() fun bind() { composeView.setContent { MyAppTheme { Content() } } } } Composition 전략 설정하기 매우 중요!!
  71. Compose의 ViewCompositionStrategy ❏ 왜 사용할까? ❏ Compose Composition을 dispose 하는

    방법 설정 ❏ 적절한 시기에 해제를 하지 않으면 Memory Leak이 발생 할 수 있음
  72. Compose의 ViewCompositionStrategy ❏ DisposeOnDetachedFromWindow ❏ Compose 뷰 자체가 현재 Window로부터

    detach 됐을 때 composition dispose ❏ Activity에서 ComposeView 를 사용하거나 ViewGroup.removeView~ 에서 없어졌을 때 트리거
  73. Compose의 ViewCompositionStrategy ❏ DisposeOnDetachedFromWindowOrReleasedFromPool (Default) ❏ Compose 뷰 자체가 Pooling

    Container 기반 내에서 돌아갈 때 사용 ❏ ex. RecyclerView ❏ RecyclerView 같은 경우 스크롤 중에 각 아이템에 대하여 Composition을 자주 dispose + recreate하면 버벅거릴 수 있는데 이를 방지하기 위함
  74. Compose의 ViewCompositionStrategy ❏ DisposeOnLifecycleDestroyed ❏ 해당 Strategy를 설정할 때 인자로

    받는 Lifecycle이 destroy 됐을 때 해제 ❏ Fragment 환경의 ComposeView에서 쓰기에 용이함
  75. Compose의 ViewCompositionStrategy ❏ DisposeOnViewTreeLifecycleDestroyed ❏ View가 attach 된 이후 현재의

    Window 에서 ViewTreeLifecycleOwner가 destroy 될 때 해제 ❏ ComposeView를 사용하는 부분에서 Lifecycle를 모를 때 사용
  76. RecyclerView의 각 ViewHolder Compose화 하기 ComposeViewHolder.kt abstract class ComposeViewHolder(val composeView:

    ComposeView) : RecyclerView.ViewHolder(composeView) { init { composeView.setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool ) } @Composable abstract fun Content() fun bind() { composeView.setContent { MyAppTheme { Content() } } } }
  77. RecyclerView의 각 ViewHolder Compose화 하기 HomeItemAdapter.kt class HomeItemAdapter : RecyclerView.Adapter<HomeItemViewHolder>()

    { class HomeItemViewHolder( val composeView: ComposeView ) : ComposeViewHolder(composeView) { @Composable override fun Content() { // 컴포즈 컴포넌트 그리기 } } }
  78. RecyclerView의 각 ViewHolder Compose화 하기 HomeItemAdapter.kt class HomeItemAdapter : RecyclerView.Adapter<HomeItemViewHolder>()

    { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, ): HomeItemViewHolder { return HomeItemViewHolder(ComposeView(parent.context)) } }
  79. RecyclerView의 각 ViewHolder Compose화 하기 HomeItemAdapter.kt class HomeItemAdapter : RecyclerView.Adapter<HomeItemViewHolder>()

    { override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind() } } ComposeViewHolder 에서 정의한 bind 함수 호출해서 실제 ComposeView에 그리기
  80. 마이그레이션 - V. RecyclerView → Compose ❏ Step 1. RecyclerView의

    각 ViewHolder를 ComposeView로 마이그레이션 ❏ Step 2. RecyclerView를 Compose의 리스트 컴포넌트로 마이그레이션 ❏ Case 1. LinearLayoutManager → LazyColumn / LazyRow ❏ Case 2. GridLayoutManager ㄴ LazyVerticalGrid / LazyHorizontalGrid ❏ Case 3. StaggeredGridLayoutManager ㄴ LazyVerticalStaggeredGrid / LazyHorizontalStaggeredGrid
  81. LazyColumn 예시 LazyColumnExample.kt @Composable fun HomeFeedList(){ LazyColumn { item {

    Text("Test1") } items(5) { index -> Text(text = "item :$index") } items(homeFeedList) { item -> HomeFeedItem(item = item) } } }
  82. LazyColumn 예시 LazyColumnExample.kt @Composable fun HomeFeedList(){ LazyColumn { item {

    Text("Test1") } items(5) { index -> Text(text = "item :$index") } items(homeFeedList) { item -> HomeFeedItem(item = item) } } }
  83. Lazy Staggered 예시 LazyStaggeredGridExample.kt LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Adaptive(200.dp), verticalItemSpacing =

    4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier.fillMaxWidth().wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
  84. Compose LazyList + StickyHeader 예시 LazyListStickyHeaderExample.kt val grouped = contacts.groupBy

    { it.firstName[0] } @OptIn(ExperimentalFoundationApi::class) @Composable fun ContactsList(grouped: Map<Char, List<Contact>>) { LazyColumn { grouped.forEach { (initial, contactsForInitial) -> stickyHeader { CharacterHeader(initial) } items(contactsForInitial) { contact -> ContactListItem(contact) } } } }
  85. 마이그레이션 - VI. Compose Interop ❏ Compose에서 기본적으로 지원하지 않는

    뷰 (ex. TextureView, SurfaceView, AdView 등)를 Compose 컴포넌트화 하기 위함
  86. 마이그레이션 - VI. Compose Interop ❏ Compose에서 기본적으로 지원하지 않는

    뷰 (ex. TextureView, SurfaceView, AdView 등)를 Compose 컴포넌트화 하기 위함 ❏ AndroidView 라는 Compose Interop API를 지원함
  87. Compose Interop API - AndroidView AndroidViewExample.kt @Composable fun CustomView() {

    var selectedItem by remember { mutableStateOf(0) } AndroidView( modifier = Modifier.fillMaxSize(), // Compose UI 트리에 뷰 크기 설정 factory = { context -> CustomView(context).apply { // 뷰 초기화 setOnClickListener { selectedItem = 1 } } }, update = { view -> // 뷰가 업데이트 되었거나 Compose UI 트리가 업데이트 되는 경우 view.selectedItem = selectedItem } ) }
  88. Compose Interop API - AndroidView AndroidViewExample.kt @Composable fun CustomView() {

    var selectedItem by remember { mutableStateOf(0) } AndroidView( modifier = Modifier.fillMaxSize(), // Compose UI 트리에 뷰 크기 설정 factory = { context -> CustomView(context).apply { // 뷰 초기화 setOnClickListener { selectedItem = 1 } } }, update = { view -> // 뷰가 업데이트 되었거나 Compose UI 트리가 업데이트 되는 경우 view.selectedItem = selectedItem } ) }
  89. Compose Interop API - AndroidViewBinding AndroidViewBindingExample.kt @Composable fun AndroidViewBindingExample() {

    AndroidViewBinding(ExampleLayoutBinding::inflate) { exampleView.setBackgroundColor(Color.GRAY) } }
  90. AndroidView in Compose LazyList AndroidViewInLazyList.kt @OptIn(ExperimentalComposeUiApi::class) @Composable fun AndroidViewInLazyList() {

    LazyColumn { items(100) { index -> AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> MyView(context) }, update = { view -> view.selectedItem = index }, onReset = { view -> view.clear() } ) } } }
  91. AndroidView in Compose LazyList AndroidViewInLazyList.kt @OptIn(ExperimentalComposeUiApi::class) @Composable fun AndroidViewInLazyList() {

    LazyColumn { items(100) { index -> AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> MyView(context) }, update = { view -> view.selectedItem = index }, onReset = { view -> view.clear() } ) } } } onReset Compose LazyList 시스템 상에서 해당 뷰가 재사용 될 때의 콜백 onRelease Compose LazyList 시스템 상에서 해당 뷰가 더이상 사용되지 않고 없어지려고 할 때 콜백
  92. 마이그레이션 - VII. 기타 안드로이드 구성요소 사용하기 ❏ 현재 Compose

    뷰가 호스팅 되는 Context 접근하기 ❏ LocalContext.current ❏ Bottom Sheet ❏ Compose Material2 / Material 3에서 사용 ❏ (Alert)Dialog ❏ Compose Material2 / Material3에서 AlertDialog 컴포넌트 사용 ❏ Compose Dialog 컴포넌트 사용
  93. 마이그레이션 - VII. 기타 안드로이드 구성요소 사용하기 ❏ Toast 사용하기

    ❏ 현재 Compose 자체에서는 Toast가 지원되지 않음 ❏ Third-party 라이브러리 사용하기 ❏ 혹은 LocalContext.current 를 사용하여 토스트 메시지 띄우기
  94. Toast in Compose ComposeToastExample.kt @Composable fun CustomButton() { val context

    = LocalContext.current Button( onClick = { Toast.makeText(context, "이것은 토스트 메시지", Toast.LENGTH_LONG).show() } ) }
  95. (번외) BroadcastReceiver in Compose ComposeBroadcastReceiverExample.kt @Composable fun SystemBroadcastReceiver( systemAction: String,

    onSystemEvent: (intent: Intent?) -> Unit ) { val context = LocalContext.current val currentOnSystemEvent by rememberUpdatedState(onSystemEvent) DisposableEffect(context, systemAction) { val intentFilter = IntentFilter(systemAction) val broadcast = object : BroadcastReceiver() {
  96. (번외) BroadcastReceiver in Compose ComposeBroadcastReceiverExample.kt @Composable fun SystemBroadcastReceiver(...) { //

    ... 생략 DisposableEffect(context, systemAction) { val intentFilter = IntentFilter(systemAction) val broadcast = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { currentOnSystemEvent(intent) } } context.registerReceiver(broadcast, intentFilter) onDispose { context.unregisterReceiver(broadcast) } } }
  97. (번외) BroadcastReceiver in Compose ComposeBroadcastReceiverExample.kt @Composable fun SystemBroadcastReceiver(...) { //

    ... 생략 DisposableEffect(context, systemAction) { val intentFilter = IntentFilter(systemAction) val broadcast = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { currentOnSystemEvent(intent) } } context.registerReceiver(broadcast, intentFilter) onDispose { context.unregisterReceiver(broadcast) } } } DisposableEffect Compose 시스템 상에서 해당 컴포넌트가 Disposable 되는 것을 캐치하는 Effect
  98. (번외) BroadcastReceiver in Compose ComposeBroadcastReceiverExample.kt @Composable fun SystemBroadcastReceiver(...) { //

    ... 생략 DisposableEffect(context, systemAction) { val intentFilter = IntentFilter(systemAction) val broadcast = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { currentOnSystemEvent(intent) } } context.registerReceiver(broadcast, intentFilter) onDispose { context.unregisterReceiver(broadcast) } } } 해당 SystemBroadcastReceiver 컴포넌트가 생길 때 1회 Register
  99. (번외) BroadcastReceiver in Compose ComposeBroadcastReceiverExample.kt @Composable fun SystemBroadcastReceiver(...) { //

    ... 생략 DisposableEffect(context, systemAction) { val intentFilter = IntentFilter(systemAction) val broadcast = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { currentOnSystemEvent(intent) } } context.registerReceiver(broadcast, intentFilter) onDispose { context.unregisterReceiver(broadcast) } } } 해당 SystemBroadcastReceiver 컴포넌트가 뷰 트리에서 없어질 때 receiver 해제
  100. 부담없이 Compose로 천천히 마이그레이션 하는 전략 ❏ Step 1. 새로운

    화면에서 Compose 사용하기 ❏ Step 2. 기존 뷰 시스템 (not RecyclerView) 으로 이루어진 화면에서 새로운 컴포넌트만 Compose 사용해서 코드베이스 공존시키기 ❏ Step 3. 기존 뷰 시스템으로 이루어진 컴포넌트를 Compose 컴포넌트로 교체하기 ❏ Step 4. RecyclerView의 각 ViewHolder Item을 Compose 컴포넌트로 교체하기 ❏ Step 5. Step 3~4가 모두 완료됐다면 해당 화면 모두 Compose 기반으로 바꾸기