Skip to content

[ui][Android] Refactor leaf components to DSL pattern#41622

Merged
Kudo merged 2 commits intoexpo:mainfrom
kimchi-developer:@kimchi-developer/expo-ui-android-dsl-refactor
Jan 19, 2026
Merged

[ui][Android] Refactor leaf components to DSL pattern#41622
Kudo merged 2 commits intoexpo:mainfrom
kimchi-developer:@kimchi-developer/expo-ui-android-dsl-refactor

Conversation

@kimchi-developer
Copy link
Copy Markdown
Contributor

@kimchi-developer kimchi-developer commented Dec 14, 2025

Summary

This PR refactors Android expo-ui leaf components from class-based ExpoComposeView pattern to the new DSL View() pattern (introduced in #40653).

Components refactored:

  • SwitchView
  • PickerView
  • SliderView
  • ProgressView
  • ChipView
  • DatePickerView
  • ShapeView
  • AlertDialogView
  • CarouselView

Not refactored:

  • TextInputView: Remains class-based because it requires instance properties (text getter/setter) for the ref system to support imperative text access.

Key changes:

  1. Props pattern: MutableState<T> → plain T values
  2. Component structure: Class with Content() method → standalone @Composable function
  3. Module registration: View(Class::class)View("Name") { props -> Content(props) }

Example transformation:

Before (class pattern):

class SwitchView(...) : ExpoComposeView<SwitchProps>(...) {
  override val props = SwitchProps()
  @Composable
  override fun ComposableScope.Content() { ... }
}

After (DSL pattern):

@Composable
fun ExpoViewComposableScope.SwitchContent(props: SwitchProps, ...) { ... }

// In ExpoUIModule.kt:
View("SwitchView") { props: SwitchProps ->
  SwitchContent(props) { ... }
}

Related

This PR builds on top of the following merged PRs:

Test plan

  • Run native-component-list on Android and verify all expo-ui screens work correctly
  • Test Switch, Picker, Slider, Progress, Chip, DatePicker, Shape, AlertDialog, Carousel components
  • Verify TextInput controlled and uncontrolled modes work

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 14, 2025

Subscribed to pull request

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

Generated by CodeMention

@expo-bot expo-bot added the bot: suggestions ExpoBot has some suggestions label Dec 14, 2025
@Kudo Kudo self-requested a review December 14, 2025 15:08
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch from 0d2013b to 1b81b02 Compare December 14, 2025 15:16
@kimchi-developer kimchi-developer changed the title [ui][Android] Refactor components to DSL pattern and add matchContents to HostView [ui][Android] Refactor leaf components to DSL pattern Dec 14, 2025
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch 3 times, most recently from c6965b8 to b4427b4 Compare December 14, 2025 15:44
@expo-bot expo-bot added bot: passed checks ExpoBot has nothing to complain about and removed bot: suggestions ExpoBot has some suggestions labels Dec 14, 2025
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch 3 times, most recently from d753b23 to 4f287bf Compare December 14, 2025 17:40
@expo-bot expo-bot added bot: suggestions ExpoBot has some suggestions and removed bot: passed checks ExpoBot has nothing to complain about labels Dec 15, 2025
@kimchi-developer
Copy link
Copy Markdown
Contributor Author

kimchi-developer commented Dec 15, 2025

Bug Fixes

  1. ComposeViewProp.kt - Use current props state instead of initial instance

    • When updating props, was copying from initial props instance
    • Now correctly uses propsMutableState.value to preserve other prop changes
  2. ViewEvent.kt - Smart validation for DSL views

    • DSL views share the same class (ComposeFunctionHolder)
    • Skip event validation when multiple view definitions share same viewType
    • Prevents incorrect validation errors for DSL components
  3. HostView.kt - Bounded infinite constraints

    • When matchContents is used, constraints can have infinite maxWidth/maxHeight
    • DatePicker and segmented Picker use horizontal scrolling internally and crash with infinite constraints
    • Fixed by bounding infinite values to screen size
  4. PickerView.kt - PickerOptionSelectedEvent properly extends Record

    • Event classes need @Field annotations to be serializable to JS
    • Was causing crash when clicking Picker options

@expo-bot expo-bot added bot: passed checks ExpoBot has nothing to complain about and removed bot: suggestions ExpoBot has some suggestions labels Dec 15, 2025
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch from 26ecbda to 65f9767 Compare December 15, 2025 05:00
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch from 4db34f5 to a426080 Compare December 15, 2025 15:42
@kimchi-developer kimchi-developer marked this pull request as draft December 15, 2025 15:43
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch 6 times, most recently from c712cfc to 6881139 Compare December 15, 2025 17:52
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch 3 times, most recently from 1717e2c to bc0c34e Compare December 20, 2025 13:40
@kimchi-developer
Copy link
Copy Markdown
Contributor Author

kimchi-developer commented Dec 20, 2025

DSL Pattern Refactoring + Modifiers Fix

Changes

  1. DSL Pattern Migration - Migrated Button, Chip, IconButton, Shape, ContextMenu, and other components to DSL pattern

    // Before (class-based)
    class ButtonView(context: Context, appContext: AppContext) : ExpoComposeView<ButtonProps>(...) {
      @Composable
      override fun Content() { ... }
    }
    
    // After (DSL)
    View("Button") { props: ButtonProps ->
      ButtonContent(props)
    }
  2. Modifiers Fix - Removed __expo_shared_object_id__ mapping in JS

    // Before (broken - ModifierConfig doesn't have __expo_shared_object_id__)
    modifiers={props.modifiers?.map((m) => m.__expo_shared_object_id__)}
    
    // After (fixed - pass JSON Config directly)
    modifiers={props.modifiers}
  3. expo-modules-core fixes

    • Fixed DSL view props using stale state when updating (ComposeViewProp.kt)

Architecture Decision: JSON Config Pattern for Modifiers

Context

expo-ui needs to pass native view modifiers (Jetpack Compose Modifier, SwiftUI view modifiers) from JavaScript to native layers.

Technical Constraints:

  • Fabric codegen generates typed C++ structs from TS/Flow component specs
  • JSI HostObject props are not supported in stable Fabric; view props must be JSON-serializable
  • Jetpack Compose Modifier is an opaque chain of function calls and cannot be serialized directly

Decision: JSON Config Pattern

JavaScript
  const mod = paddingAll(10);
  // { $type: 'paddingAll', all: 10 }

React Native
  <Button modifiers={[mod]} />

Android (Kotlin)
  - ModifierConfig Record parses the JSON
  - ModifierRegistry applies the config to build a Compose Modifier chain

ModifierConfig (JS)

export interface ModifierConfig {
  $type: string;
  $scope?: string;
  [key: string]: unknown;
}

ModifierConfig (Android Record)

data class ModifierConfig(
  @Field(key = "\$type") val type: String = "",
  ...
) : Record

Why JSON Config over SharedRef?

Aspect JSON Config SharedRef (deprecated)
Fabric compatibility ✅ JSON-serializable ❌ HostObject not supported
Platform parity ✅ Same shape on Android/iOS ❌ Platform-specific
Internal IDs ✅ None exposed __expo_shared_object_id__
JS mapping required ✅ No ❌ Yes (per-component)

The SharedRef + ID Registry approach is deprecated. ExpoModifier is now a TypeScript alias of ModifierConfig.


References

PR Title Author Date
#24446 [core][Android] Add SharedRefs @tsapeta 2022-10
#24431 [core][iOS] Using shared ref as a prop @tsapeta 2023-09
#30218 [core] Expose SharedRef class in JS @tsapeta 2024-07
#31776 [core] Add nativeRefType to SharedRef @tsapeta 2024-07
#38630 [expo-ui] Add compose modifiers @aleqsio 2025-08
#39155 [ui] Add scoped compose modifiers @aleqsio 2025-08

@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch 3 times, most recently from 7976f13 to 7270ce2 Compare December 20, 2025 13:55
@kimchi-developer kimchi-developer marked this pull request as ready for review December 20, 2025 13:57
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch from 7270ce2 to 2b7912c Compare December 20, 2025 15:00
@kimchi-developer kimchi-developer marked this pull request as draft December 20, 2025 15:02
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch 4 times, most recently from aed511a to c1fb650 Compare December 20, 2025 15:11
@expo-bot expo-bot added bot: passed checks ExpoBot has nothing to complain about and removed bot: suggestions ExpoBot has some suggestions labels Dec 20, 2025
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch from c1fb650 to f0abf6a Compare December 20, 2025 15:25
@kimchi-developer kimchi-developer marked this pull request as ready for review December 20, 2025 15:28
@kimchi-developer
Copy link
Copy Markdown
Contributor Author

Hi @kimchi-developer – awesome PR and a lot of work saved – thank you!

Noticed too late you saw my PR and found that source set issue so I merged that one, here's a PR following your suggestion – I like it but I renamed the interface – feel free to review it.

#41685

I'll try to review the rest of your PR asap.

@aleqsio Hey! This builds on your compose modifiers work (#38630, #39155). Would appreciate your review when you have time.

@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch 3 times, most recently from 86bb3a1 to 6cc5803 Compare December 20, 2025 18:37
- Migrate Button, Chip, IconButton, Shape, and other components to DSL pattern
- Fix modifiers passing: remove __expo_shared_object_id__ mapping for JSON Config
- Restore onSizeChanged in HostView for proper React Native layout integration
- ContextMenu remains class-based due to positioning requirements
@kimchi-developer kimchi-developer force-pushed the @kimchi-developer/expo-ui-android-dsl-refactor branch from 6cc5803 to e4c80be Compare December 24, 2025 06:59
Copy link
Copy Markdown
Contributor

@Kudo Kudo left a comment

Choose a reason for hiding this comment

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

such an awesome pr! thanks for having all together.
leaving some nit comments but it's generally good to me. the only question is the modifiers change. let me discuss this with team.

/**
* The background color of the button.
*/
containerColor?: string;
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
containerColor?: string;
containerColor?: ColorValue;

import { type ColorValue } from 'react-native';

personally prefer to have explicit ColorValue type if that's supported

/**
* The text color of the button.
*/
contentColor?: string;
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
contentColor?: string;
contentColor?: ColorValue;

ditto


// Utility
@Field val testID: String? = null,
@Field val shape: ShapeRecord? = null,
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.

currently the modifier registry is not scalable. would be good to improve that. in our mind, we also have plan to make swift-ui ModifierRegistry extensible, so that people can add their own modifiers

@Kudo
Copy link
Copy Markdown
Contributor

Kudo commented Jan 19, 2026

i would update expo-ui code soon and don't want to keep the pr conflict again. i'm going to merge it and will follow my comment in a separate pr. thanks again for the great work 👏

@Kudo Kudo merged commit 251f6ab into expo:main Jan 19, 2026
14 of 17 checks passed
Kudo added a commit that referenced this pull request Jan 30, 2026
# Why

following up #41622 and improve integration for jetpack-compose

# How

- [js] use ColorValue type for colors
- [js] move modifiers into `@expo/ui/jetpack-compose/modifiers` as
swift-ui
- [core] rename `ExpoViewComposableScope` to
`FunctionalComposableScope`. it's confusing between ComposableScope and
ExpoViewComposableScope
- [ui] introduce `ExpoUIView` DSL as swift-ui. currently it's the same
as View DSL. keeping that allows us to introduce more expo-ui dedicated
features.
- [ui] fix broken scoped modifiers like `matchParentSize`.
- [ui] make ModifierRegistry extensible. each modifier can have it's own
parameter convertible (Record).
- [core] add `recordFromMap` that allows Record conversion from Map.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot: passed checks ExpoBot has nothing to complain about

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants