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

Compose-View Interop in Practice (mDevCamp 2024)

Compose-View Interop in Practice (mDevCamp 2024)

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

April 23, 2024
Tweet

More Decks by István Juhos

Other Decks in Programming

Transcript

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

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

    • Complex custom Views • Whole screens • A UI library the project uses doesn’t support Compose
  3. #mDevCamp – @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. #mDevCamp – @stewemetal istvanjuhos.dev AndroidView • Including existing View hierarchies

    in Compose UI • ”We don’t have the resources or a business case to rewrite custom Views”
  5. #mDevCamp – @stewemetal istvanjuhos.dev AndroidView • Including existing View hierarchies

    in Compose UI • ”We don’t have the resources or a business case to rewrite custom Views” • Using UI components not yet available in Compose
  6. #mDevCamp – @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. #mDevCamp – @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) } } }
  8. #mDevCamp – @stewemetal istvanjuhos.dev Existing custom View } ) }

    @Composable fun AndroidViewDemoScreen(...) { Scaffold( topBar = { ... }, content = { padding -> ...
  9. #mDevCamp – @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. #mDevCamp – @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. #mDevCamp – @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. #mDevCamp – @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 ->
  13. #mDevCamp – @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 ->
  14. #mDevCamp – @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
  15. #mDevCamp – @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
  16. #mDevCamp – @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
  17. #mDevCamp – @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
  18. #mDevCamp – @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. #mDevCamp – @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. #mDevCamp – @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. #mDevCamp – @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) } }
  22. #mDevCamp – @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) } } ✅
  23. #mDevCamp – @stewemetal istvanjuhos.dev ComposeView • Including Compose UI in

    View hierarchies • ”We want to create new UI components in Compose…”
  24. #mDevCamp – @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.”
  25. #mDevCamp – @stewemetal istvanjuhos.dev Adding ComposeView to a Layout <LinearLayout>

    <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_height="wrap_content" android:layout_width="match_parent" app:title="View Screen"/> </LinearLayout>
  26. #mDevCamp – @stewemetal istvanjuhos.dev Adding ComposeView to a Layout <LinearLayout>

    <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_height="wrap_content" android:layout_width="match_parent" app:title=”View Screen"/> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeView" android:layout_height="wrap_content" android:layout_width="match_parent" android:paddingTop="8dp"/> </LinearLayout>
  27. #mDevCamp – @stewemetal istvanjuhos.dev A hybrid Activity 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) }
  28. #mDevCamp – @stewemetal istvanjuhos.dev A hybrid Activity 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) }
  29. #mDevCamp – @stewemetal istvanjuhos.dev A hybrid Activity 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) }
  30. #mDevCamp – @stewemetal istvanjuhos.dev A hybrid Activity 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) }
  31. #mDevCamp – @stewemetal istvanjuhos.dev A hybrid Activity 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) } ❗ ❗ ❗ ❗
  32. #mDevCamp – @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. #mDevCamp – @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() } } }
  34. #mDevCamp – @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() } } }
  35. #mDevCamp – @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() } } }
  36. #mDevCamp – @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() } } } ✅
  37. #mDevCamp – @stewemetal istvanjuhos.dev ComposeViewAdapter • Wraps a ComposeView to

    render an XML preview • Supports parameterized previews
  38. #mDevCamp – @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
  39. #mDevCamp – @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
  40. #mDevCamp – @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
  41. #mDevCamp – @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
  42. #mDevCamp – @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
  43. #mDevCamp – @stewemetal istvanjuhos.dev The Layout and the Preview <LinearLayout>

    <MaterialToolbar .../> <View android:id="@+id/viewContent1" .../> <View android:id="@+id/viewContent2"/> </LinearLayout>
  44. #mDevCamp – @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"/>
  45. #mDevCamp – @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"/>
  46. #mDevCamp – @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"/>
  47. #mDevCamp – @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>
  48. #mDevCamp – @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>
  49. #mDevCamp – @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>
  50. #mDevCamp – @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>
  51. #mDevCamp – @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>
  52. #mDevCamp – @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>
  53. #mDevCamp – @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>
  54. #mDevCamp – @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>
  55. #mDevCamp – @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>
  56. #mDevCamp – @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>
  57. #mDevCamp – @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> ❗ ❗ ❗ ❗
  58. #mDevCamp – @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() } }
  59. #mDevCamp – @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() } }
  60. #mDevCamp – @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() } }
  61. #mDevCamp – @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() } }
  62. #mDevCamp – @stewemetal istvanjuhos.dev Views wrapping Composables class CustomButton @JvmOverloads

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

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

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

    constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : AbstractComposeView(context, attrs, defStyleAttr) { !!... }
  66. #mDevCamp – @stewemetal istvanjuhos.dev class CustomButton @JvmOverloads constructor( context: Context,

    attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : AbstractComposeView(context, attrs, defStyleAttr) { !!... } Views wrapping Composables @Composable fun CustomButtonComposable( text: String, enabled: Boolean, loading: Boolean, ) { !!... }
  67. #mDevCamp – @stewemetal istvanjuhos.dev class CustomButton @JvmOverloads constructor( context: Context,

    attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : AbstractComposeView(context, attrs, defStyleAttr) { !!... } Views wrapping Composables @Composable fun CustomButtonComposable( text: String, enabled: Boolean, loading: Boolean, ) { !!... } 🧑🎨
  68. #mDevCamp – @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(...) {
  69. #mDevCamp – @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
  70. #mDevCamp – @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
  71. #mDevCamp – @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
  72. #mDevCamp – @stewemetal istvanjuhos.dev Views wrapping Composables ... } class

    CustomButton(...) : AbstractComposeView(...) { @Composable override fun Content() { YourAppTheme { CustomButtonCompose( text = text.value, enabled = isEnabled.value, loading = isLoading.value, ) } }
  73. #mDevCamp – @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" /> ⚠
  74. #mDevCamp – @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 ⚠
  75. #mDevCamp – @stewemetal Resources • developer.android.com/jetpack/compose/migrate/interoperability-apis • developer.android.com/reference/kotlin/androidx/compose/ui/platform/AbstractCompose View •

    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/ • flaticon.com/free-icons/prague istvanjuhos.dev
  76. 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 in 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
  77. Compose-View Interop in Practice Photo by Dan Dennis on Unsplash

    @stewemetal istvanjuhos.dev István Juhos Děkuji