Skip to content

[core][ui] add matchContents support to jetpack-compose#41553

Merged
Kudo merged 4 commits intomainfrom
@kudo/jetpack-compose/host-match-contents
Dec 14, 2025
Merged

[core][ui] add matchContents support to jetpack-compose#41553
Kudo merged 4 commits intomainfrom
@kudo/jetpack-compose/host-match-contents

Conversation

@Kudo
Copy link
Copy Markdown
Contributor

@Kudo Kudo commented Dec 11, 2025

Why

add matchContents support for jetpack-compose Host as swift-ui
close ENG-18108

How

  • pass matchContents from js to native as swift-ui
  • call setStyleSize as swift-ui
  • add onLayoutContent callback as swift-ui
  • use Modifier.onSizeChanged to listen for size changed
  • when matchContents is true, use wrap_content layout and MeasureSpec.UNSPECIFIED to allow ComposeView exceeds its parent HostView
  • fix a bug where after setStyleSize layout change, the ComposeView doesn't stick position with HostView

Test Plan

  • verify NCL screens
    • Still some breaking screens because they have AutosizeingComposable inside: Switch / Progress / Picker / TextInput
  • tested with the code
import { Button, Host } from '@expo/ui/jetpack-compose';
import { useState } from 'react';
import { Text, View } from 'react-native';

export default function UIScreen() {
  const [layoutContent, setLayoutContent] = useState({ width: 0, height: 0 });
  return (
    <View
      style={{ flex: 1, marginTop: 64, marginHorizontal: 16, flexDirection: 'column', gap: 16 }}>
      <Host style={{ height: 100, backgroundColor: 'rgba(0, 0, 255, 0.2)' }}>
        <Button>Test</Button>
      </Host>

      <Host
        style={{ height: 100, backgroundColor: 'rgba(0, 0, 255, 0.2)' }}
        matchContents={{ vertical: true }}>
        <Button>Test Vertical</Button>
      </Host>

      <Host
        style={{ height: 100, backgroundColor: 'rgba(0, 0, 255, 0.2)' }}
        matchContents={{ horizontal: true }}>
        <Button>Test Horizontal</Button>
      </Host>

      <Host matchContents onLayoutContent={(event) => setLayoutContent(event.nativeEvent)}>
        <Button>Test Match Contents</Button>
      </Host>

      <Text>
        Layout Content: {layoutContent.width}x{layoutContent.height}
      </Text>
    </View>
  );
}

Checklist

Copy link
Copy Markdown
Contributor Author

Kudo commented Dec 11, 2025

@expo-bot expo-bot added the bot: suggestions ExpoBot has some suggestions label Dec 11, 2025
@linear
Copy link
Copy Markdown

linear bot commented Dec 11, 2025

@expo-bot expo-bot added bot: passed checks ExpoBot has nothing to complain about and removed bot: suggestions ExpoBot has some suggestions labels Dec 11, 2025
@expo-bot
Copy link
Copy Markdown
Collaborator

expo-bot commented Dec 11, 2025

The Pull Request introduced fingerprint changes against the base commit: 8c3eb21

Fingerprint diff
[
  {
    "op": "changed",
    "beforeSource": {
      "type": "dir",
      "filePath": "../../packages/expo-modules-core",
      "reasons": [
        "expoAutolinkingIos",
        "expoAutolinkingAndroid",
        "expoAutolinkingIos"
      ],
      "hash": "afd37a8c3be40784eded2300155c1f20fdee279d"
    },
    "afterSource": {
      "type": "dir",
      "filePath": "../../packages/expo-modules-core",
      "reasons": [
        "expoAutolinkingIos",
        "expoAutolinkingAndroid",
        "expoAutolinkingIos"
      ],
      "hash": "21de5fd9788af15123b136b66ca0b84111bce239"
    }
  },
  {
    "op": "changed",
    "beforeSource": {
      "type": "dir",
      "filePath": "../../packages/expo-ui/android",
      "reasons": [
        "expoAutolinkingAndroid"
      ],
      "hash": "f4beff0c1eca98c0a1e5f89a64c89f58d40c3d8a"
    },
    "afterSource": {
      "type": "dir",
      "filePath": "../../packages/expo-ui/android",
      "reasons": [
        "expoAutolinkingAndroid"
      ],
      "hash": "95b52132157464d4e3bfcc5da03b72fd4599928e"
    }
  }
]

Generated by PR labeler 🤖

@Kudo Kudo marked this pull request as ready for review December 11, 2025 11:56
@github-actions
Copy link
Copy Markdown
Contributor

Subscribed to pull request

File Patterns Mentions
packages/expo-modules-core/** @lukmccall
packages/expo-ui/** @aleqsio, @behenate, @douglowder

Generated by CodeMention

if (child is ComposeView) {
val offsetX = paddingLeft
val offsetY = paddingRight
child.layout(offsetX, offsetY, offsetX + width, offsetY + height)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
child.layout(offsetX, offsetY, offsetX + width, offsetY + height)
child.layout(left, top, right, bottom)

Can't we just pass data from onLayout to the children?

Also, will this work for more than one child?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just pass data from onLayout to the children?

that won't work. because the left/top/right/bottom from onLayout is the relative position for the ExpoComposeView to its parent. however, we need to layout the child with the relative position from child to the ExpoComposeView.

Also, will this work for more than one child?

no, i don't expect there were multiple ComposeView in its children. currently we add the ComposeView at

private fun addComposeView() {
val composeView = ComposeView(context).also {
it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
it.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
it.setContent {
with(ComposableScope()) {
Content()
}
}
it.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
it.disposeComposition()
}
override fun onViewDetachedFromWindow(v: View) = Unit
})
}
addView(composeView)
}

if there were multiple ComposeView, possibly some turbo module has direct ComposeView inserted as child in JSX. do you think if that's something we should supported? if so, we may need to keep our ComposeView reference or having tag for indication.

@Kudo Kudo requested a review from lukmccall December 12, 2025 03:38
@Kudo Kudo mentioned this pull request Dec 12, 2025
3 tasks
@kimchi-developer
Copy link
Copy Markdown
Contributor

Hi @Kudo!

I noticed in your test plan that some screens are still breaking due to AutoSizingComposable inside Switch / Progress / Picker / TextInput.

I've been working on a follow-up change that addresses this by:

  1. Removing AutoSizingComposable from all leaf components (Switch, Picker, Slider, Progress, TextInput, Chip, DatePicker, Shape, AlertDialog)
  2. Converting components to DSL pattern (similar to BottomSheet)
  3. Using matchContents on Host instead of individual component auto-sizing

This way, all sizing is handled by Host's matchContents prop as intended by this PR's architecture.

For example, instead of:

// Old: Component handles its own sizing
class SwitchView(...) : ExpoComposeView<SwitchProps>(...) {
  @Composable
  override fun ComposableScope.Content() {
    AutoSizingComposable(shadowNodeProxy, ...) {
      Switch(...)
    }
  }
}

It becomes:

// New: DSL pattern, Host handles sizing
View("SwitchView", events = { Events("onValueChange") }) { props: SwitchProps ->
  val onValueChange by remember { EventDispatcher<ValueChangeEvent>() }
  SwitchContent(props) { onValueChange(it) }
}

I'll submit a separate PR with these changes that builds on top of this PR's matchContents infrastructure.

@Kudo
Copy link
Copy Markdown
Contributor Author

Kudo commented Dec 14, 2025

@kimchi-developer for AutosizingCompable, there's the other pr for it: #41595. does it align what you had in mind?

@kimchi-developer
Copy link
Copy Markdown
Contributor

@Kudo Yes, PR #41595 aligns exactly with what we had in mind! Moving AutoSizingComposable to HostView and having leaf components rely on matchContents is the right approach.

In our PR #41622, we did the same thing but also included a DSL refactoring - converting class-based ExpoComposeView components to the new View() DSL pattern (introduced in #40653). This affects: SwitchView, PickerView, SliderView, ProgressView, ChipView, DatePickerView, ShapeView, AlertDialogView, and TextInputView.

Question: Would you like us to:

  1. Close our PR [ui][Android] Refactor leaf components to DSL pattern #41622 and submit the DSL refactoring as a separate PR after [core][ui] add matchContents support to jetpack-compose #41553 and [ui] remove AutoSizingComposable #41595 are merged?
  2. Or would you prefer to incorporate the DSL refactoring into [ui] remove AutoSizingComposable #41595?

Happy to go either way - just let us know what works best for the expo-ui roadmap!

@Kudo
Copy link
Copy Markdown
Contributor Author

Kudo commented Dec 14, 2025

@kimchi-developer yes, let's keep your refactoring. let me merge my prs first so that you can rebase #41622 onto latest main and keep pure refactoring work in #41622.

@Kudo Kudo changed the base branch from @kudo/jetpack-compose/remove-dynamic-theme to graphite-base/41553 December 14, 2025 15:00
@Kudo
Copy link
Copy Markdown
Contributor Author

Kudo commented Dec 14, 2025

@lukmccall going to merge this beforehand. if there's any further comments, please let me know and i'll follow up in a separate pr

@Kudo Kudo force-pushed the graphite-base/41553 branch from 3f745b1 to 8c3eb21 Compare December 14, 2025 15:05
@Kudo Kudo force-pushed the @kudo/jetpack-compose/host-match-contents branch from faef212 to 831d4c3 Compare December 14, 2025 15:05
@Kudo Kudo changed the base branch from graphite-base/41553 to main December 14, 2025 15:05
@Kudo Kudo merged commit ad3436a into main Dec 14, 2025
17 of 28 checks passed
@Kudo Kudo deleted the @kudo/jetpack-compose/host-match-contents branch December 14, 2025 15:06
kimchi-developer pushed a commit to kimchi-developer/expo that referenced this pull request Dec 15, 2025
# Why

add `matchContents` support for jetpack-compose Host as swift-ui 
close ENG-18108

# How

- pass matchContents from js to native as swift-ui
- call `setStyleSize` as swift-ui
- add `onLayoutContent` callback as swift-ui
- use `Modifier.onSizeChanged` to listen for size changed
- when matchContents is true, use wrap_content layout and MeasureSpec.UNSPECIFIED to allow ComposeView exceeds its parent HostView
- fix a bug where after `setStyleSize` layout change, the ComposeView doesn't stick position with HostView
aleqsio pushed a commit that referenced this pull request Dec 22, 2025
# Why

add `matchContents` support for jetpack-compose Host as swift-ui 
close ENG-18108

# How

- pass matchContents from js to native as swift-ui
- call `setStyleSize` as swift-ui
- add `onLayoutContent` callback as swift-ui
- use `Modifier.onSizeChanged` to listen for size changed
- when matchContents is true, use wrap_content layout and MeasureSpec.UNSPECIFIED to allow ComposeView exceeds its parent HostView
- fix a bug where after `setStyleSize` layout change, the ComposeView doesn't stick position with HostView
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot: fingerprint changed bot: passed checks ExpoBot has nothing to complain about

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants