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

Compose-View Interop in Practice

Compose-View Interop in Practice

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.

Presented at Droidcon San Francisco 2023, on 2023.06.09.

István Juhos

June 09, 2023
Tweet

More Decks by István Juhos

Other Decks in Programming

Transcript

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

    AndroidViewBinding • ComposeView • ComposeViewAdapter • Hybrid (Compose + Espresso) testing
  2. #dcsf23 – @stewemetal istvanjuhos.dev AndroidView • Including existing View hierarchies

    in Compose UI • ”no time / resources to rewrite” • Using UI components from libraries that are not yet available in Compose
  3. #dcsf23 – @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) } } }
  4. #dcsf23 – @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) } } }
  5. #dcsf23 – @stewemetal istvanjuhos.dev Existing custom View } ) }

    @Composable fun AndroidViewDemoScreen(...) { Scaffold( topBar = { ... }, content = { padding -> ...
  6. #dcsf23 – @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 ->
  7. #dcsf23 – @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 ->
  8. #dcsf23 – @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. #dcsf23 – @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. #dcsf23 – @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. #dcsf23 – @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 ->
  12. #dcsf23 – @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
  13. #dcsf23 – @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. #dcsf23 – @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. #dcsf23 – @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. #dcsf23 – @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. #dcsf23 – @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. #dcsf23 – @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. #dcsf23 – @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. #dcsf23 – @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. #dcsf23 – @stewemetal istvanjuhos.dev Adding ComposeView to 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>
  22. #dcsf23 – @stewemetal istvanjuhos.dev Adding ComposeView to 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>
  23. #dcsf23 – @stewemetal istvanjuhos.dev Adding ComposeView to a View class

    ComposeViewDemoActivity : AppCompatActivity() { private lateinit var binding: ActivityComposeViewDemoBinding ... } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityComposeViewDemoBinding.inflate(layoutInflater) setContentView(binding.root) }
  24. #dcsf23 – @stewemetal istvanjuhos.dev Adding ComposeView to a View binding.toolbar.title

    = "ComposeView Demo Title" 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) }
  25. #dcsf23 – @stewemetal istvanjuhos.dev Adding ComposeView to a View binding.toolbar.title

    = "ComposeView Demo Title" 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) }
  26. #dcsf23 – @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("ComposeView Demo Title")) .check(matches(isDisplayed())) onNodeWithText("Compose Button").assertIsDisplayed() } } }
  27. #dcsf23 – @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("ComposeView Demo Title")) .check(matches(isDisplayed())) onNodeWithText("Compose Button").assertIsDisplayed() } } }
  28. #dcsf23 – @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("ComposeView Demo Title")) .check(matches(isDisplayed())) onNodeWithText("Compose Button").assertIsDisplayed() } } }
  29. #dcsf23 – @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("ComposeView Demo Title")) .check(matches(isDisplayed())) onNodeWithText("Compose Button").assertIsDisplayed() } } }
  30. #dcsf23 – @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("ComposeView Demo Title")) .check(matches(isDisplayed())) onNodeWithText("Compose Button").assertIsDisplayed() } } } ✅
  31. #dcsf23 – @stewemetal istvanjuhos.dev ComposeViewAdapter • Wraps a ComposeView to

    render an XML preview • Supports parameterized previews
  32. #dcsf23 – @stewemetal istvanjuhos.dev Banners – a Composable to include

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

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

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

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

    in an XML layout class BannersPreviewDataProvider : PreviewParameterProvider<List<BannerData>> { ... } @Preview @Composable private fun SingleBannerPreview() { ... } @Composable fun Banners( banners: List<BannerData>, ) { ... }
  37. #dcsf23 – @stewemetal istvanjuhos.dev The layout <LinearLayout> <MaterialToolbar/> <View android:id="@+id/viewContent1"

    .../> <View android:id="@+id/viewContent2"/> </LinearLayout> <ComposeView android:id="@+id/banners"/>
  38. #dcsf23 – @stewemetal istvanjuhos.dev The layout <LinearLayout> <MaterialToolbar/> <View android:id="@+id/viewContent1"

    .../> <View android:id="@+id/viewContent2"/> </LinearLayout> <ComposeView android:id="@+id/banners"/>
  39. #dcsf23 – @stewemetal istvanjuhos.dev The layout <LinearLayout> <MaterialToolbar/> <View android:id="@+id/viewContent1"

    .../> <View android:id="@+id/viewContent2"/> </LinearLayout> <ComposeView android:id="@+id/banners"/>
  40. #dcsf23 – @stewemetal istvanjuhos.dev The layout <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>
  41. #dcsf23 – @stewemetal istvanjuhos.dev The layout <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>
  42. #dcsf23 – @stewemetal istvanjuhos.dev The layout <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. #dcsf23 – @stewemetal istvanjuhos.dev The layout <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. #dcsf23 – @stewemetal istvanjuhos.dev The layout <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. #dcsf23 – @stewemetal istvanjuhos.dev The layout <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>
  46. #dcsf23 – @stewemetal istvanjuhos.dev The layout <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>
  47. #dcsf23 – @stewemetal istvanjuhos.dev The layout <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>
  48. #dcsf23 – @stewemetal istvanjuhos.dev The layout <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>
  49. #dcsf23 – @stewemetal istvanjuhos.dev The layout <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>
  50. #dcsf23 – @stewemetal istvanjuhos.dev AbstractComposeView • Creating custom Views with

    Compose for rendering • Rendering widely used View components with Compose • e.g. in design systems
  51. #dcsf23 – @stewemetal istvanjuhos.dev AbstractComposeView • Creating custom Views with

    Compose for rendering • Rendering widely used View components with Compose • e.g. in design systems
  52. #dcsf23 – @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() } }
  53. #dcsf23 – @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() } }
  54. #dcsf23 – @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. #dcsf23 – @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. #dcsf23 – @stewemetal istvanjuhos.dev Views wrapping Composables class CustomButton @JvmOverloads

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

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

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

    constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : AbstractComposeView(context, attrs, defStyleAttr) { !!... }
  60. #dcsf23 – @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(...) {
  61. #dcsf23 – @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
  62. #dcsf23 – @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
  63. #dcsf23 – @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
  64. #dcsf23 – @stewemetal istvanjuhos.dev Views wrapping Composables ... } class

    CustomButton(...) : AbstractComposeView(...) { @Composable override fun Content() { YourAppTheme { CustomButton( text = text.value, enabled = isEnabled.value, loading = isLoading.value, ) } }
  65. #dcsf23 – @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" /> ⚠
  66. #dcsf23 – @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
  67. #dcsf23 – @stewemetal istvanjuhos.dev Wrapping up • Pros • Views

    can easily be added to Compose UI • We can use Compose UI in View hierarchies • Interop features help migration to Compose
  68. #dcsf23 – @stewemetal istvanjuhos.dev Wrapping up • Pros • Views

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

    • 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/
  70. #dcsf23 – @stewemetal istvanjuhos.dev Resources • developer.android.com/jetpack/compose/migrate/interoperability-apis • developer.android.com/reference/kotlin/androidx/compose/ui/platform/Abstrac tComposeView

    • 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
  71. stewemetal istvanjuhos.dev Compose-View Interop in Practice Photo by Dan Dennis

    on Unsplash • 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