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

Compose-View Interop in Practice (DevFest Venezia 2023)

Compose-View Interop in Practice (DevFest Venezia 2023)

If you’re working on an already established, large code base, there’s a good chance that your screens still use Views to some extent. However, these screens should still be maintained to keep UI consistency across your app. In this talk, we’ll look at how we can support the maintenance of such screens and custom UI components with Jetpack Compose’s interoperability features while discussing the ups and downs of having hybrid UIs in our apps.

István Juhos

December 02, 2023
Tweet

More Decks by István Juhos

Other Decks in Programming

Transcript

  1. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Compose-View interop features • AndroidView •

    ComposeView • ComposeViewAdapter • AbstractComposeView • Hybrid (Compose + Espresso) testing
  2. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Why? • No time to rewrite

    • Complex custom Views • Whole screens • A UI library the project uses doesn’t support Compose
  3. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Why? • No time to rewrite

    • Complex custom Views • Whole screens • A UI library the project uses doesn’t support Compose • New custom components are easier to implement in Compose
  4. #devfestvenezia2023 – @stewemetal istvanjuhos.dev AndroidView • Including existing View hierarchies

    in Compose UI • ”no time, resources, or business case to rewrite custom Views” • Using UI components not yet available in Compose
  5. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Existing custom View class CustomButtonView @JvmOverloads

    constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : ConstraintLayout(context, attrs, defStyleAttr) { private val button: Button private val progressBar: ProgressBar ... LayoutInflater.from(context).inflate(R.layout.view_custom_button, this, true) ... fun setText(text: String) { button.text = text } override fun setOnClickListener(listener: OnClickListener?) { button.setOnClickListener { listener?.onClick(button) } } }
  6. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Existing custom View class CustomButtonView @JvmOverloads

    constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : ConstraintLayout(context, attrs, defStyleAttr) { private val button: Button private val progressBar: ProgressBar ... LayoutInflater.from(context).inflate(R.layout.view_custom_button, this, true) ... fun setText(text: String) { button.text = text } override fun setOnClickListener(listener: OnClickListener?) { button.setOnClickListener { listener?.onClick(button) } } }
  7. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Existing custom View } ) }

    @Composable fun AndroidViewDemoScreen(...) { Scaffold( topBar = { ... }, content = { padding -> ...
  8. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Existing custom View AndroidView( modifier =

    Modifier..., factory = { context -> CustomButtonView(context) }, update = { view -> view.setText("View Button") view.setOnClickListener { ... } }, ) } ) } @Composable fun AndroidViewDemoScreen(...) { Scaffold( topBar = { ... }, content = { padding ->
  9. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Existing custom View AndroidView( modifier =

    Modifier..., factory = { context -> CustomButtonView(context) }, update = { view -> view.setText("View Button") view.setOnClickListener { ... } }, ) } ) } @Composable fun AndroidViewDemoScreen(...) { Scaffold( topBar = { ... }, content = { padding ->
  10. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Existing custom View AndroidView( modifier =

    Modifier..., factory = { context -> CustomButtonView(context) }, update = { view -> view.setText("View Button") view.setOnClickListener { ... } }, ) } ) } @Composable fun AndroidViewDemoScreen(...) { Scaffold( topBar = { ... }, content = { padding ->
  11. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Existing custom View AndroidView( modifier =

    Modifier..., factory = { context -> CustomButtonView(context) }, update = { view -> view.setText("View Button") view.setOnClickListener { ... } }, ) } ) } @Composable fun AndroidViewDemoScreen(...) { Scaffold( topBar = { ... }, content = { padding ->
  12. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Existing custom View AndroidView( modifier =

    Modifier..., factory = { context -> CustomButtonView(context) }, update = { view -> view.setText(state.buttonText) view.setOnClickListener { ... } }, ) } ) } @Composable fun AndroidViewDemoScreen(...) { Scaffold( topBar = { ... }, content = { padding ->
  13. #devfestvenezia2023 – @stewemetal istvanjuhos.dev AndroidView( modifier = Modifier.semantics { testTag

    = "stats_chart" }, factory = { context -> BarChart(context).apply { layoutParams = LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, ) } }, update = { barChart -> // Set up barChart }, ) View from a lib - MPAndroidChart
  14. #devfestvenezia2023 – @stewemetal istvanjuhos.dev @Test fun androidViewDemo_buttonClick() { composeTestRule.apply {

    var buttonClicked = false setContent { AndroidViewDemoScreen() { buttonClicked = true } } Espresso.onView(withText("View Button")).apply { check(matches(isDisplayed())) perform(click()) } assertTrue(buttonClicked) } } Testing AndroidView
  15. #devfestvenezia2023 – @stewemetal istvanjuhos.dev @Test fun androidViewDemo_buttonClick() { composeTestRule.apply {

    var buttonClicked = false setContent { AndroidViewDemoScreen() { buttonClicked = true } } Espresso.onView(withText("View Button")).apply { check(matches(isDisplayed())) perform(click()) } assertTrue(buttonClicked) } } Testing AndroidView
  16. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Testing AndroidView @Test fun androidViewDemo_buttonClick() {

    composeTestRule.apply { var buttonClicked = false setContent { AndroidViewDemoScreen() { buttonClicked = true } } Espresso.onView(withText("View Button")).apply { check(matches(isDisplayed())) perform(click()) } assertTrue(buttonClicked) } }
  17. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Testing AndroidView @Test fun androidViewDemo_buttonClick() {

    composeTestRule.apply { var buttonClicked = false setContent { AndroidViewDemoScreen() { buttonClicked = true } } Espresso.onView(withText("View Button")).apply { check(matches(isDisplayed())) perform(click()) } assertTrue(buttonClicked) } }
  18. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Testing AndroidView @Test fun androidViewDemo_buttonClick() {

    composeTestRule.apply { var buttonClicked = false setContent { AndroidViewDemoScreen() { buttonClicked = true } } Espresso.onView(withText("View Button")).apply { check(matches(isDisplayed())) perform(click()) } assertTrue(buttonClicked) } }
  19. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Testing AndroidView @Test fun androidViewDemo_buttonClick() {

    composeTestRule.apply { var buttonClicked = false setContent { AndroidViewDemoScreen() { buttonClicked = true } } Espresso.onView(withText("View Button")).apply { check(matches(isDisplayed())) perform(click()) } assertTrue(buttonClicked) } }
  20. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Testing AndroidView @Test fun androidViewDemo_buttonClick() {

    composeTestRule.apply { var buttonClicked = false setContent { AndroidViewDemoScreen() { buttonClicked = true } } Espresso.onView(withText("View Button")).apply { check(matches(isDisplayed())) perform(click()) } assertTrue(buttonClicked) } } ✅
  21. #devfestvenezia2023 – @stewemetal istvanjuhos.dev ComposeView • Including Compose UI in

    View hierarchies • ”We want to create new UI components in Compose…”
  22. #devfestvenezia2023 – @stewemetal istvanjuhos.dev ComposeView • Including Compose UI in

    View hierarchies • ”We want to create new UI components in Compose…” • ”…but we’ll have to use them on View-based screens.”
  23. #devfestvenezia2023 – @stewemetal istvanjuhos.dev ComposeView in a View <LinearLayout> <com.google.android.material.appbar.MaterialToolbar

    android:id="@+id/toolbar" android:layout_height="wrap_content" android:layout_width="match_parent" app:title="ComposeView Demo Title"/> </LinearLayout>
  24. #devfestvenezia2023 – @stewemetal istvanjuhos.dev ComposeView in a View <LinearLayout> <com.google.android.material.appbar.MaterialToolbar

    android:id="@+id/toolbar" android:layout_height="wrap_content" android:layout_width="match_parent" app:title="ComposeView Demo Title"/> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeView" android:layout_height="wrap_content" android:layout_width="match_parent" android:paddingTop="8dp"/> </LinearLayout>
  25. #devfestvenezia2023 – @stewemetal istvanjuhos.dev An Activity hosting a View with

    ComposeView class ComposeViewDemoActivity : AppCompatActivity() { private lateinit var binding: ActivityComposeViewDemoBinding ... } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityComposeViewDemoBinding.inflate(layoutInflater) setContentView(binding.root) }
  26. #devfestvenezia2023 – @stewemetal istvanjuhos.dev An Activity hosting a View with

    ComposeView binding.toolbar.title = "View Screen" binding.composeView.apply { setViewCompositionStrategy(DisposeOnDetachedFromWindow) setContent { CustomButton( text = "Compose Button", modifier = Modifier..., ) { ... } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityComposeViewDemoBinding.inflate(layoutInflater) setContentView(binding.root) }
  27. #devfestvenezia2023 – @stewemetal istvanjuhos.dev An Activity hosting a View with

    ComposeView binding.toolbar.title = "View Screen" binding.composeView.apply { setViewCompositionStrategy(DisposeOnDetachedFromWindow) setContent { CustomButton( text = "Compose Button", modifier = Modifier..., ) { ... } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityComposeViewDemoBinding.inflate(layoutInflater) setContentView(binding.root) }
  28. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Testing ComposeView @RunWith(AndroidJUnit4::class) class DemoComposeViewTest {

    @get:Rule val androidComposeTestRule = createAndroidComposeRule<ComposeViewDemoActivity>() @Test fun composeView_Button_isVisible() { androidComposeTestRule.apply { Espresso.onView(withText("View Screen")) .check(matches(isDisplayed())) onNodeWithText("Compose Button").assertIsDisplayed() } } }
  29. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Testing ComposeView @RunWith(AndroidJUnit4::class) class DemoComposeViewTest {

    @get:Rule val androidComposeTestRule = createAndroidComposeRule<ComposeViewDemoActivity>() @Test fun composeView_Button_isVisible() { androidComposeTestRule.apply { Espresso.onView(withText("View Screen")) .check(matches(isDisplayed())) onNodeWithText("Compose Button").assertIsDisplayed() } } }
  30. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Testing ComposeView @RunWith(AndroidJUnit4::class) class DemoComposeViewTest {

    @get:Rule val androidComposeTestRule = createAndroidComposeRule<ComposeViewDemoActivity>() @Test fun composeView_Button_isVisible() { androidComposeTestRule.apply { Espresso.onView(withText("View Screen")) .check(matches(isDisplayed())) onNodeWithText("Compose Button").assertIsDisplayed() } } }
  31. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Testing ComposeView @RunWith(AndroidJUnit4::class) class DemoComposeViewTest {

    @get:Rule val androidComposeTestRule = createAndroidComposeRule<ComposeViewDemoActivity>() @Test fun composeView_Button_isVisible() { androidComposeTestRule.apply { Espresso.onView(withText("View Screen")) .check(matches(isDisplayed())) onNodeWithText("Compose Button").assertIsDisplayed() } } }
  32. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Testing ComposeView @RunWith(AndroidJUnit4::class) class DemoComposeViewTest {

    @get:Rule val androidComposeTestRule = createAndroidComposeRule<ComposeViewDemoActivity>() @Test fun composeView_Button_isVisible() { androidComposeTestRule.apply { Espresso.onView(withText("View Screen")) .check(matches(isDisplayed())) onNodeWithText("Compose Button").assertIsDisplayed() } } } ✅
  33. #devfestvenezia2023 – @stewemetal istvanjuhos.dev @Composable fun Banners( banners: List<BannerData>, )

    { ... } class BannersPreviewDataProvider : PreviewParameterProvider<List<BannerData>> { ... } @Preview @Composable private fun BannersPreview( @PreviewParameter(BannersPreviewParameterProvider::class) banners: List<BannerData>, ) { ... } Banners, a Composable to include in an XML layout
  34. #devfestvenezia2023 – @stewemetal istvanjuhos.dev @Composable fun Banners( banners: List<BannerData>, )

    { ... } class BannersPreviewDataProvider : PreviewParameterProvider<List<BannerData>> { ... } @Preview @Composable private fun BannersPreview( @PreviewParameter(BannersPreviewParameterProvider::class) banners: List<BannerData>, ) { ... } Banners, a Composable to include in an XML layout
  35. #devfestvenezia2023 – @stewemetal istvanjuhos.dev @Composable fun Banners( banners: List<BannerData>, )

    { ... } class BannersPreviewDataProvider : PreviewParameterProvider<List<BannerData>> { ... } @Preview @Composable private fun BannersPreview( @PreviewParameter(BannersPreviewParameterProvider::class) banners: List<BannerData>, ) { ... } Banners, a Composable to include in an XML layout
  36. #devfestvenezia2023 – @stewemetal istvanjuhos.dev class BannersPreviewDataProvider : PreviewParameterProvider<List<BannerData>> { ...

    } @Preview @Composable private fun BannersPreview( @PreviewParameter(BannersPreviewParameterProvider::class) banners: List<BannerData>, ) { ... } @Composable fun Banners( banners: List<BannerData>, ) { ... } Banners, a Composable to include in an XML layout
  37. #devfestvenezia2023 – @stewemetal istvanjuhos.dev class BannersPreviewDataProvider : PreviewParameterProvider<List<BannerData>> { ...

    } @Preview @Composable private fun SingleBannerPreview() { ... } @Composable fun Banners( banners: List<BannerData>, ) { ... } Banners, a Composable to include in an XML layout
  38. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1" .../> <View android:id="@+id/viewContent2"/> </LinearLayout>
  39. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1" .../> <View android:id="@+id/viewContent2"/> </LinearLayout> <ComposeView android:id="@+id/banners"/>
  40. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1" .../> <View android:id="@+id/viewContent2"/> </LinearLayout> <ComposeView android:id="@+id/banners"/>
  41. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1" .../> <View android:id="@+id/viewContent2"/> </LinearLayout> <ComposeView android:id="@+id/banners"/>
  42. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1"/> <ComposeViewAdapter android:id="@+id/bannersPreview" tools:composableName= "hu.stewemetal.demo.BannersKt.SingleBannerPreview" > <ComposeView android:id="@+id/banners"/> </ComposeViewAdapter> <View android:id="@+id/viewContent2"/> </LinearLayout>
  43. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1"/> <ComposeViewAdapter android:id="@+id/bannersPreview" tools:composableName= "hu.stewemetal.demo.BannersKt.SingleBannerPreview" > <ComposeView android:id="@+id/banners"/> </ComposeViewAdapter> <View android:id="@+id/viewContent2"/> </LinearLayout>
  44. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1"/> <ComposeViewAdapter android:id="@+id/bannersPreview" tools:composableName= "hu.stewemetal.demo.BannersKt.SingleBannerPreview" > <ComposeView android:id="@+id/banners"/> </ComposeViewAdapter> <View android:id="@+id/viewContent2"/> </LinearLayout>
  45. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1"/> <ComposeViewAdapter android:id="@+id/bannersPreview" tools:composableName= "hu.stewemetal.demo.BannersKt.SingleBannerPreview" > <ComposeView android:id="@+id/banners"/> </ComposeViewAdapter> <View android:id="@+id/viewContent2"/> </LinearLayout>
  46. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1"/> <ComposeViewAdapter android:id="@+id/bannersPreview" tools:composableName= "hu.stewemetal.demo.BannersKt.SingleBannerPreview" > <ComposeView android:id="@+id/banners"/> </ComposeViewAdapter> <View android:id="@+id/viewContent2"/> </LinearLayout>
  47. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1"/> <ComposeViewAdapter android:id="@+id/bannersPreview" tools:composableName= "hu.stewemetal.demo.BannersKt.BannersPreview" tools:parameterProviderClass= "hu.stewemetal.demo.BannersKt. BannersPreviewDataProvider" > <ComposeView android:id="@+id/banners"/> </ComposeViewAdapter> <View android:id="@+id/viewContent2"/> </LinearLayout>
  48. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1"/> <ComposeViewAdapter android:id="@+id/bannersPreview" tools:composableName= "hu.stewemetal.demo.BannersKt.BannersPreview" tools:parameterProviderClass= "hu.stewemetal.demo.BannersKt. BannersPreviewDataProvider" > <ComposeView android:id="@+id/banners"/> </ComposeViewAdapter> <View android:id="@+id/viewContent2"/> </LinearLayout>
  49. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1"/> <ComposeViewAdapter android:id="@+id/bannersPreview" tools:composableName= "hu.stewemetal.demo.BannersKt.BannersPreview" tools:parameterProviderClass= "hu.stewemetal.demo.BannersKt. BannersPreviewDataProvider" tools:parameterProviderIndex="0"> <ComposeView android:id="@+id/banners"/> </ComposeViewAdapter> <View android:id="@+id/viewContent2"/> </LinearLayout>
  50. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1"/> <ComposeViewAdapter android:id="@+id/bannersPreview" tools:composableName= "hu.stewemetal.demo.BannersKt.BannersPreview" tools:parameterProviderClass= "hu.stewemetal.demo.BannersKt. BannersPreviewDataProvider" tools:parameterProviderIndex="0"> <ComposeView android:id="@+id/banners"/> </ComposeViewAdapter> <View android:id="@+id/viewContent2"/> </LinearLayout>
  51. #devfestvenezia2023 – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1"/> <ComposeViewAdapter android:id="@+id/bannersPreview" tools:composableName= "hu.stewemetal.demo.BannersKt.BannersPreview" tools:parameterProviderClass= "hu.stewemetal.demo.BannersKt. BannersPreviewDataProvider" tools:parameterProviderIndex="1"> <ComposeView android:id="@+id/banners"/> </ComposeViewAdapter> <View android:id="@+id/viewContent2"/> </LinearLayout>
  52. #devfestvenezia2023 – @stewemetal istvanjuhos.dev AbstractComposeView • Creating custom Views with

    Compose for rendering • Rendering widely used View components with Compose • e.g. design systems having Compose and View implementations
  53. #devfestvenezia2023 – @stewemetal istvanjuhos.dev AbstractComposeView • Creating custom Views with

    Compose for rendering • Rendering widely used View components with Compose • e.g. design systems having Compose and View implementations
  54. #devfestvenezia2023 – @stewemetal istvanjuhos.dev AbstractComposeView class ComposeView @JvmOverloads constructor( context:

    Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AbstractComposeView(...) { private val content = mutableStateOf<(@Composable () -> Unit)?>(null) @Composable override fun Content() { content.value?.invoke() } }
  55. #devfestvenezia2023 – @stewemetal istvanjuhos.dev AbstractComposeView class ComposeView @JvmOverloads constructor( context:

    Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AbstractComposeView(...) { private val content = mutableStateOf<(@Composable () -> Unit)?>(null) @Composable override fun Content() { content.value?.invoke() } }
  56. #devfestvenezia2023 – @stewemetal istvanjuhos.dev AbstractComposeView class ComposeView @JvmOverloads constructor( context:

    Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AbstractComposeView(...) { private val content = mutableStateOf<(@Composable () -> Unit)?>(null) @Composable override fun Content() { content.value?.invoke() } }
  57. #devfestvenezia2023 – @stewemetal istvanjuhos.dev AbstractComposeView class ComposeView @JvmOverloads constructor( context:

    Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AbstractComposeView(...) { private val content = mutableStateOf<(@Composable () -> Unit)?>(null) @Composable override fun Content() { content.value?.invoke() } }
  58. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Views wrapping Composables class CustomButton @JvmOverloads

    constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : FrameLayout(context, attrs, defStyleAttr) { !!... }
  59. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Views wrapping Composables class CustomButton @JvmOverloads

    constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : ConstraintLayout(context, attrs, defStyleAttr) { !!... }
  60. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Views wrapping Composables class CustomButton @JvmOverloads

    constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : AppCompatButton(context, attrs, defStyleAttr) { !!... }
  61. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Views wrapping Composables class CustomButton @JvmOverloads

    constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : AbstractComposeView(context, attrs, defStyleAttr) { !!... }
  62. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Views wrapping Composables init { context.theme

    .obtainStyledAttributes( attrs, R.styleable.CustomButton, 0, 0, ).apply { try { (this) } finally { recycle() } } } ... } initAttributes class CustomButton(...) : AbstractComposeView(...) {
  63. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Views wrapping Composables ... } class

    CustomButton(...) : AbstractComposeView(...) { fun (typedArray: TypedArray) { with(typedArray) { text.value = getString(R.styleable.CustomButton_text).orEmpty() isEnabled.value = getBoolean(R.styleable.CustomButton_enabled, true isLoading.value = getBoolean(R.styleable.CustomButton_loading, fals } } initAttributes
  64. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Views wrapping Composables class CustomButton(...) :

    AbstractComposeView(...) { ... } class CustomButton(...) : AbstractComposeView(...) { private val text = mutableStateOf("") private val isEnabled = mutableStateOf(true) private val isLoading = mutableStateOf(false) fun (typedArray: TypedArray) { with(typedArray) { text.value = getString(R.styleable.CustomButton_text).orEmpty() isEnabled.value = getBoolean(R.styleable.CustomButton_enabled, true isLoading.value = getBoolean(R.styleable.CustomButton_loading, fals } } initAttributes
  65. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Views wrapping Composables ... } class

    CustomButton(...) : AbstractComposeView(...) { @Composable override fun Content() { YourAppTheme { CustomButton( text = text.value, enabled = isEnabled.value, loading = isLoading.value, ) } }
  66. #devfestvenezia2023 – @stewemetal istvanjuhos.dev XML preview <CustomButton ... app:text="Custom Button"

    /> <CustomButton ... app:text="Custom Button" app:enabled="false" /> <CustomButton ... app:text="Custom Button" app:loading="true" /> ⚠
  67. #devfestvenezia2023 – @stewemetal istvanjuhos.dev XML preview <CustomButton ... app:text="Custom Button"

    /> <CustomButton ... app:text="Custom Button" app:enabled="false" /> <CustomButton ... app:text="Custom Button" app:loading="true" /> ⚠ Works out of the box after Android Studio Giraffe Canary 8 https://issuetracker.google.com/issues/187339385 ⚠
  68. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Compose-View Interoperability • Pros • Views

    can easily be added to Compose UI • We can use Compose UI in View hierarchies • Interop features support the migration to Compose
  69. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Wrapping up • Pros • Views

    can easily be added to Compose UI • We can use Compose UI in View hierarchies • Interop features support the migration to Compose • Cons • Every ComposeView is a new composition (performance overhead) • Maintenance overhead • Added testing complexity
  70. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Resources • developer.android.com/jetpack/compose/migrate/interoperability-apis • developer.android.com/reference/kotlin/androidx/compose/ui/platform/AbstractCo mposeView

    • developer.android.com/jetpack/compose/tooling/previews • developer.android.com/jetpack/compose/testing • istvanjuhos.dev/talks/2022/20221005-how-to-test-your-compose-ui/ • istvanjuhos.dev/talks/2023/20230530-composing-a-design-system/
  71. #devfestvenezia2023 – @stewemetal istvanjuhos.dev Resources • developer.android.com/jetpack/compose/migrate/interoperability-apis • developer.android.com/reference/kotlin/androidx/compose/ui/platform/AbstractCo mposeView

    • developer.android.com/jetpack/compose/tooling/previews • developer.android.com/jetpack/compose/testing • istvanjuhos.dev/talks/2022/20221005-how-to-test-your-compose-ui/ • istvanjuhos.dev/talks/2023/20230530-composing-a-design-system/ istvanjuhos.dev
  72. Compose-View Interop in Practice Photo by Dan Dennis on Unsplash

    @stewemetal istvanjuhos.dev István Juhos • Consider using the interop features when you introduce Compose to your apps • Hybrid UI previews and testing are supported • Keep the impact on performance and maintenance in mind • Use the right tools for the right job, don’t rush migrations
  73. Compose-View Interop in Practice Photo by Dan Dennis on Unsplash

    @stewemetal istvanjuhos.dev István Juhos Grazie per l’attenzione