@expo/ui 56.0.7 → 56.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +44 -3
  4. package/android/src/main/java/expo/modules/ui/HostView.kt +2 -0
  5. package/android/src/main/java/expo/modules/ui/LoadingView.kt +80 -0
  6. package/android/src/main/java/expo/modules/ui/ModifierRegistry.kt +50 -4
  7. package/android/src/main/java/expo/modules/ui/RNHostView.kt +8 -3
  8. package/android/src/main/java/expo/modules/ui/ShadowNodeSyncFlush.kt +28 -0
  9. package/android/src/main/java/expo/modules/ui/SnackbarView.kt +126 -0
  10. package/android/src/main/java/expo/modules/ui/state/ObservableState.kt +10 -0
  11. package/assets/keyboard_arrow_down.xml +10 -0
  12. package/build/State/useNativeState.d.ts +32 -3
  13. package/build/State/useNativeState.d.ts.map +1 -1
  14. package/build/community/bottom-sheet/BottomSheet.ios.d.ts.map +1 -1
  15. package/build/community/menu/MenuView.android.d.ts +16 -0
  16. package/build/community/menu/MenuView.android.d.ts.map +1 -0
  17. package/build/community/menu/MenuView.d.ts +19 -0
  18. package/build/community/menu/MenuView.d.ts.map +1 -0
  19. package/build/community/menu/MenuView.ios.d.ts +10 -0
  20. package/build/community/menu/MenuView.ios.d.ts.map +1 -0
  21. package/build/community/menu/index.d.ts +5 -0
  22. package/build/community/menu/index.d.ts.map +1 -0
  23. package/build/community/menu/types.d.ts +166 -0
  24. package/build/community/menu/types.d.ts.map +1 -0
  25. package/build/jetpack-compose/LoadingIndicator/index.d.ts +41 -0
  26. package/build/jetpack-compose/LoadingIndicator/index.d.ts.map +1 -0
  27. package/build/jetpack-compose/Snackbar/index.d.ts +94 -0
  28. package/build/jetpack-compose/Snackbar/index.d.ts.map +1 -0
  29. package/build/jetpack-compose/index.d.ts +2 -0
  30. package/build/jetpack-compose/index.d.ts.map +1 -1
  31. package/build/jetpack-compose/modifiers/index.d.ts +21 -2
  32. package/build/jetpack-compose/modifiers/index.d.ts.map +1 -1
  33. package/build/swift-ui/Alert/index.d.ts +42 -0
  34. package/build/swift-ui/Alert/index.d.ts.map +1 -0
  35. package/build/swift-ui/BottomSheet/index.d.ts +5 -1
  36. package/build/swift-ui/BottomSheet/index.d.ts.map +1 -1
  37. package/build/swift-ui/SlotView.d.ts +5 -2
  38. package/build/swift-ui/SlotView.d.ts.map +1 -1
  39. package/build/swift-ui/SwipeActions/index.d.ts +38 -0
  40. package/build/swift-ui/SwipeActions/index.d.ts.map +1 -0
  41. package/build/swift-ui/index.d.ts +3 -0
  42. package/build/swift-ui/index.d.ts.map +1 -1
  43. package/build/swift-ui/modifiers/index.d.ts +3 -1
  44. package/build/swift-ui/modifiers/index.d.ts.map +1 -1
  45. package/build/swift-ui/modifiers/symbolEffect.d.ts +103 -0
  46. package/build/swift-ui/modifiers/symbolEffect.d.ts.map +1 -0
  47. package/build/swift-ui/withAnimation.d.ts +26 -0
  48. package/build/swift-ui/withAnimation.d.ts.map +1 -0
  49. package/build/universal/BottomSheet/index.android.d.ts +1 -1
  50. package/build/universal/BottomSheet/index.android.d.ts.map +1 -1
  51. package/build/universal/BottomSheet/index.d.ts +1 -1
  52. package/build/universal/BottomSheet/index.d.ts.map +1 -1
  53. package/build/universal/BottomSheet/index.ios.d.ts +1 -1
  54. package/build/universal/BottomSheet/index.ios.d.ts.map +1 -1
  55. package/build/universal/BottomSheet/types.d.ts +27 -0
  56. package/build/universal/BottomSheet/types.d.ts.map +1 -1
  57. package/build/universal/Collapsible/index.android.d.ts +8 -0
  58. package/build/universal/Collapsible/index.android.d.ts.map +1 -0
  59. package/build/universal/Collapsible/index.d.ts +8 -0
  60. package/build/universal/Collapsible/index.d.ts.map +1 -0
  61. package/build/universal/Collapsible/index.ios.d.ts +7 -0
  62. package/build/universal/Collapsible/index.ios.d.ts.map +1 -0
  63. package/build/universal/Collapsible/types.d.ts +23 -0
  64. package/build/universal/Collapsible/types.d.ts.map +1 -0
  65. package/build/universal/Column/index.d.ts.map +1 -1
  66. package/build/universal/Host/index.d.ts +5 -7
  67. package/build/universal/Host/index.d.ts.map +1 -1
  68. package/build/universal/Host/types.d.ts +72 -0
  69. package/build/universal/Host/types.d.ts.map +1 -0
  70. package/build/universal/List/index.android.d.ts +9 -0
  71. package/build/universal/List/index.android.d.ts.map +1 -0
  72. package/build/universal/List/index.d.ts +8 -0
  73. package/build/universal/List/index.d.ts.map +1 -0
  74. package/build/universal/List/index.ios.d.ts +8 -0
  75. package/build/universal/List/index.ios.d.ts.map +1 -0
  76. package/build/universal/List/types.d.ts +26 -0
  77. package/build/universal/List/types.d.ts.map +1 -0
  78. package/build/universal/ListItem/ListItem.android.d.ts +8 -0
  79. package/build/universal/ListItem/ListItem.android.d.ts.map +1 -0
  80. package/build/universal/ListItem/ListItem.d.ts +9 -0
  81. package/build/universal/ListItem/ListItem.d.ts.map +1 -0
  82. package/build/universal/ListItem/ListItem.ios.d.ts +8 -0
  83. package/build/universal/ListItem/ListItem.ios.d.ts.map +1 -0
  84. package/build/universal/ListItem/ListItemSlots.d.ts +21 -0
  85. package/build/universal/ListItem/ListItemSlots.d.ts.map +1 -0
  86. package/build/universal/ListItem/index.d.ts +10 -0
  87. package/build/universal/ListItem/index.d.ts.map +1 -0
  88. package/build/universal/ListItem/types.d.ts +59 -0
  89. package/build/universal/ListItem/types.d.ts.map +1 -0
  90. package/build/universal/Picker/Picker.android.d.ts +9 -0
  91. package/build/universal/Picker/Picker.android.d.ts.map +1 -0
  92. package/build/universal/Picker/Picker.d.ts +8 -0
  93. package/build/universal/Picker/Picker.d.ts.map +1 -0
  94. package/build/universal/Picker/Picker.ios.d.ts +9 -0
  95. package/build/universal/Picker/Picker.ios.d.ts.map +1 -0
  96. package/build/universal/Picker/PickerItem.d.ts +9 -0
  97. package/build/universal/Picker/PickerItem.d.ts.map +1 -0
  98. package/build/universal/Picker/index.d.ts +8 -0
  99. package/build/universal/Picker/index.d.ts.map +1 -0
  100. package/build/universal/Picker/types.d.ts +69 -0
  101. package/build/universal/Picker/types.d.ts.map +1 -0
  102. package/build/universal/index.d.ts +4 -0
  103. package/build/universal/index.d.ts.map +1 -1
  104. package/expo-module.config.json +1 -1
  105. package/ios/Alert/Alert.swift +56 -0
  106. package/ios/Alert/AlertProps.swift +8 -0
  107. package/ios/BottomSheetView.swift +4 -1
  108. package/ios/ExpoUIModule.swift +43 -1
  109. package/ios/ExpoUITouchHandlerHelper.h +4 -1
  110. package/ios/ExpoUITouchHandlerHelper.mm +1 -0
  111. package/ios/Modifiers/AnimationConfig.swift +109 -0
  112. package/ios/Modifiers/SwipeActionsModifier.swift +97 -0
  113. package/ios/Modifiers/SymbolEffectModifier.swift +452 -0
  114. package/ios/Modifiers/ViewModifierRegistry.swift +5 -112
  115. package/ios/SlotView.swift +5 -0
  116. package/ios/State/ObservableState.swift +12 -1
  117. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7-sources.jar → 56.0.9/expo.modules.ui-56.0.9-sources.jar} +0 -0
  118. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.md5 +1 -0
  119. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha1 +1 -0
  120. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha256 +1 -0
  121. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha512 +1 -0
  122. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar +0 -0
  123. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.md5 +1 -0
  124. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha1 +1 -0
  125. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha256 +1 -0
  126. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha512 +1 -0
  127. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7.module → 56.0.9/expo.modules.ui-56.0.9.module} +22 -22
  128. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.md5 +1 -0
  129. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha1 +1 -0
  130. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha256 +1 -0
  131. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha512 +1 -0
  132. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7.pom → 56.0.9/expo.modules.ui-56.0.9.pom} +1 -1
  133. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.md5 +1 -0
  134. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha1 +1 -0
  135. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha256 +1 -0
  136. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha512 +1 -0
  137. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  138. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  139. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  140. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  141. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  142. package/package.json +7 -3
  143. package/src/State/useNativeState.ts +70 -10
  144. package/src/community/bottom-sheet/BottomSheet.ios.tsx +0 -17
  145. package/src/community/menu/MenuView.android.tsx +224 -0
  146. package/src/community/menu/MenuView.ios.tsx +149 -0
  147. package/src/community/menu/MenuView.tsx +36 -0
  148. package/src/community/menu/index.tsx +14 -0
  149. package/src/community/menu/types.tsx +171 -0
  150. package/src/jetpack-compose/LoadingIndicator/index.tsx +92 -0
  151. package/src/jetpack-compose/Snackbar/index.tsx +135 -0
  152. package/src/jetpack-compose/index.ts +2 -0
  153. package/src/jetpack-compose/modifiers/index.ts +30 -2
  154. package/src/swift-ui/Alert/index.tsx +87 -0
  155. package/src/swift-ui/BottomSheet/index.tsx +32 -15
  156. package/src/swift-ui/SlotView.tsx +17 -4
  157. package/src/swift-ui/SwipeActions/index.tsx +73 -0
  158. package/src/swift-ui/index.tsx +3 -0
  159. package/src/swift-ui/modifiers/index.ts +3 -0
  160. package/src/swift-ui/modifiers/symbolEffect.ts +181 -0
  161. package/src/swift-ui/withAnimation.ts +71 -0
  162. package/src/ts-declarations/react-native-web.d.ts +27 -0
  163. package/src/universal/BottomSheet/index.android.tsx +27 -3
  164. package/src/universal/BottomSheet/index.ios.tsx +30 -12
  165. package/src/universal/BottomSheet/index.tsx +46 -4
  166. package/src/universal/BottomSheet/types.ts +25 -0
  167. package/src/universal/Collapsible/index.android.tsx +72 -0
  168. package/src/universal/Collapsible/index.ios.tsx +16 -0
  169. package/src/universal/Collapsible/index.tsx +58 -0
  170. package/src/universal/Collapsible/types.ts +25 -0
  171. package/src/universal/Column/index.tsx +3 -1
  172. package/src/universal/Host/index.tsx +69 -5
  173. package/src/universal/Host/types.ts +70 -0
  174. package/src/universal/List/index.android.tsx +44 -0
  175. package/src/universal/List/index.ios.tsx +19 -0
  176. package/src/universal/List/index.tsx +26 -0
  177. package/src/universal/List/types.ts +28 -0
  178. package/src/universal/ListItem/ListItem.android.tsx +52 -0
  179. package/src/universal/ListItem/ListItem.ios.tsx +58 -0
  180. package/src/universal/ListItem/ListItem.tsx +72 -0
  181. package/src/universal/ListItem/ListItemSlots.tsx +66 -0
  182. package/src/universal/ListItem/index.ts +15 -0
  183. package/src/universal/ListItem/types.ts +67 -0
  184. package/src/universal/Picker/Picker.android.tsx +69 -0
  185. package/src/universal/Picker/Picker.ios.tsx +45 -0
  186. package/src/universal/Picker/Picker.tsx +52 -0
  187. package/src/universal/Picker/PickerItem.tsx +27 -0
  188. package/src/universal/Picker/index.ts +11 -0
  189. package/src/universal/Picker/types.ts +79 -0
  190. package/src/universal/index.ts +4 -0
  191. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.md5 +0 -1
  192. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha1 +0 -1
  193. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha256 +0 -1
  194. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha512 +0 -1
  195. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar +0 -0
  196. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.md5 +0 -1
  197. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha1 +0 -1
  198. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha256 +0 -1
  199. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha512 +0 -1
  200. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.md5 +0 -1
  201. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha1 +0 -1
  202. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha256 +0 -1
  203. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha512 +0 -1
  204. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.md5 +0 -1
  205. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha1 +0 -1
  206. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha256 +0 -1
  207. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha512 +0 -1
  208. package/src/community/bottom-sheet/CLAUDE.md +0 -55
package/CHANGELOG.md CHANGED
@@ -10,6 +10,36 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.9 — 2026-05-19
14
+
15
+ ### 🎉 New features
16
+
17
+ - [iOS][android] Added `onChange` listener to `useNativeState`. ([#45961](https://github.com/expo/expo/pull/45961) by [@nishan](https://github.com/intergalacticspacehighway))
18
+ - Allow writing to native state from the JS thread. ([#45901](https://github.com/expo/expo/pull/45901) by [@nishan](https://github.com/intergalacticspacehighway))
19
+ - [iOS] Added `withAnimation(animation, body)` in `@expo/ui/swift-ui`, mirroring SwiftUI's [`withAnimation(_:_:)`](<https://developer.apple.com/documentation/swiftui/withanimation(_:_:)>). ([#45893](https://github.com/expo/expo/pull/45893) by [@nishan](https://github.com/intergalacticspacehighway))
20
+ - [jetpack-compose] Added `Snackbar` component. ([#45667](https://github.com/expo/expo/pull/45667) by [@nishan](https://github.com/intergalacticspacehighway))
21
+ - [android] Added `LoadingIndicator` and `ContainedLoadingIndicator` components. ([#41169](https://github.com/expo/expo/pull/41169) by [@suveshmoza](https://github.com/suveshmoza))
22
+
23
+ ### 🐛 Bug fixes
24
+
25
+ - [iOS] Unmount `BottomSheet` when it is dismissed. ([#45846](https://github.com/expo/expo/pull/45846) by [@nishan](https://github.com/intergalacticspacehighway))
26
+
27
+ ### 💡 Others
28
+
29
+ - [universal] Add base styling to universal Picker on web ([#45932](https://github.com/expo/expo/pull/45932) by [@zoontek](https://github.com/zoontek))
30
+
31
+ ## 56.0.8 — 2026-05-15
32
+
33
+ ### 🎉 New features
34
+
35
+ - [universal] Added `matchContents`, `layoutDirection`, `onLayoutContent`, `useViewportSizeMeasurement`, and `ignoreSafeArea` support to the universal `Host`. ([#45776](https://github.com/expo/expo/pull/45776) by [@zoontek](https://github.com/zoontek))
36
+ - [iOS] Added `symbolEffect` modifier. ([#45727](https://github.com/expo/expo/pull/45727) by [@nishan](https://github.com/intergalacticspacehighway))
37
+ - [iOS] Added `Alert` component wrapping SwiftUI's `.alert(_:isPresented:actions:message:)` modifier, with `Alert.Trigger`, `Alert.Actions`, and optional `Alert.Message` slots. Mirrors the existing `ConfirmationDialog` shape so it composes the same way with `Button` actions and `isPresented` bindings. ([#45700](https://github.com/expo/expo/pull/45700) by [@ramonclaudio](https://github.com/ramonclaudio))
38
+
39
+ ### 💡 Others
40
+
41
+ - Fixed precompile xcframework warning. ([#45762](https://github.com/expo/expo/pull/45762) by [@kudo](https://github.com/kudo))
42
+
13
43
  ## 56.0.7 — 2026-05-13
14
44
 
15
45
  _This version does not introduce any user-facing changes._
@@ -20,10 +50,16 @@ _This version does not introduce any user-facing changes._
20
50
 
21
51
  - Added `@expo/ui/community/slider`, a drop-in replacement for `@react-native-community/slider`. ([#45623](https://github.com/expo/expo/pull/45623) by [@nishan](https://github.com/intergalacticspacehighway))
22
52
  - Make `ChartView` public. ([#45674](https://github.com/expo/expo/pull/45674) by [@jakex7](https://github.com/jakex7))
53
+ - Added `@expo/ui/community/menu`, a drop-in replacement for `@react-native-menu/menu`. ([#45670](https://github.com/expo/expo/pull/45670) by [@vonovak](https://github.com/vonovak))
54
+ - [android] Added Compose `combinedClickable` modifier. ([#45670](https://github.com/expo/expo/pull/45670) by [@vonovak](https://github.com/vonovak))
55
+ - [universal] Added `Collapsible`, `List`, `ListItem`, and `Picker` components. ([#45754](https://github.com/expo/expo/pull/45754) by [@kudo](https://github.com/kudo))
56
+ - [universal] Added `snapPoints` prop on `BottomSheet` and `colorScheme` / `layoutDirection` props on `Host`. ([#45754](https://github.com/expo/expo/pull/45754) by [@kudo](https://github.com/kudo))
57
+ - [jetpack-compose] `background(color, { animationSpec })` accepts an optional `animationSpec` and wraps the color in `animateColorAsState` so changes between renders animate smoothly. ([#45754](https://github.com/expo/expo/pull/45754) by [@kudo](https://github.com/kudo))
23
58
 
24
59
  ### 🐛 Bug fixes
25
60
 
26
61
  - Fix `useNativeState` recreating the `ObservableState` when initial value changes; the seed is now captured once via `useRef`. ([#45623](https://github.com/expo/expo/pull/45623) by [@nishan](https://github.com/intergalacticspacehighway))
62
+ - [Android] Fix layout shift in `Host` with `matchContents`. ([#45775](https://github.com/expo/expo/pull/45775) by [@nishan](https://github.com/intergalacticspacehighway))
27
63
 
28
64
  ## 56.0.5 — 2026-05-11
29
65
 
@@ -87,6 +123,7 @@ _This version does not introduce any user-facing changes._
87
123
  - [iOS] Add `WorkletCallback` shared object for synchronous UI thread callbacks. ([#44216](https://github.com/expo/expo/pull/44216) by [@nishan](https://github.com/intergalacticspacehighway))
88
124
  - [iOS] Added `scrollPosition` and `id` modifiers for tracking and scrolling to view-aligned targets in `ScrollView` and other scrollable containers (iOS 17+). ([#44652](https://github.com/expo/expo/pull/44652) by [@ramonclaudio](https://github.com/ramonclaudio))
89
125
  - Added `@expo/ui/datetimepicker` — an Android and iOS `DateTimePicker` drop-in replacement for `@react-native-community/datetimepicker`. ([#44014](https://github.com/expo/expo/pull/44014) by [@vonovak](https://github.com/vonovak))
126
+ - [iOS] Added `SwipeActions` component. ([#44689](https://github.com/expo/expo/pull/44689) by [@yousofabouhalawa](https://github.com/yousofabouhalawa))
90
127
  - [swift-ui] Added `LazyHStack` and `LazyVStack`. ([#44612](https://github.com/expo/expo/pull/44612) by [@kudo](https://github.com/kudo))
91
128
  - [jetpack-compose] Added `LazyRow` component and `onVisibilityChanged` modifier. ([#44615](https://github.com/expo/expo/pull/44615) by [@kudo](https://github.com/kudo))
92
129
  - Added universal components. ([#44601](https://github.com/expo/expo/pull/44601) by [@kudo](https://github.com/kudo))
@@ -115,6 +152,7 @@ _This version does not introduce any user-facing changes._
115
152
  ### 💡 Others
116
153
 
117
154
  - Moved `DateTimePicker` to `@expo/ui/community/datetime-picker`. The old `@expo/ui/datetimepicker` export still works but logs a deprecation warning in development. ([@vonovak](https://github.com/vonovak)) ([#45211](https://github.com/expo/expo/pull/45211) by [@vonovak](https://github.com/vonovak))
155
+ - [iOS] Added `extraProps` to `SlotView` for passing metadata to SwiftUI slot consumers. ([#44689](https://github.com/expo/expo/pull/45287) by [@yousofabouhalawa](https://github.com/yousofabouhalawa))
118
156
  - [jetpack-compose] Use view hash code as key for `Children`. ([#44521](https://github.com/expo/expo/pull/44521) by [@kudo](https://github.com/kudo))
119
157
  - Refactored `ComposableScope` and allow extensibility. ([#44698](https://github.com/expo/expo/pull/44698) by [@kudo](https://github.com/kudo))
120
158
  - [jetpack-compose] Reuse `HorizontalAlignment` converter in `LazyColumn`. ([#44755](https://github.com/expo/expo/pull/44755) by [@kudo](https://github.com/kudo))
@@ -316,7 +354,7 @@ _This version does not introduce any user-facing changes._
316
354
 
317
355
  - [iOS] Remove leftover `Switch` TypeScript exports from swift-ui package. Use `Toggle` instead. ([#42571](https://github.com/expo/expo/pull/42571) by [@shubh73](https://github.com/shubh73))
318
356
  - Improved Jetpack Compose integration for Expo UI. ([#42450](https://github.com/expo/expo/pull/42450) by [@kudo](https://github.com/kudo))
319
- - [iOS] Added `contentShape` modifier for SwiftUI ([#42813](https://github.com/expo/expo pull/42813) by [@sam-shubham](https://github.com/sam-shubham))
357
+ - [iOS] Added `contentShape` modifier for SwiftUI ([#42813](https://github.com/expo/expo/pull/42813) by [@sam-shubham](https://github.com/sam-shubham))
320
358
 
321
359
  ## 55.0.0-beta.3 — 2026-01-27
322
360
 
@@ -12,13 +12,13 @@ apply plugin: 'expo-module-gradle-plugin'
12
12
  apply plugin: 'org.jetbrains.kotlin.plugin.compose'
13
13
 
14
14
  group = 'expo.modules.ui'
15
- version = '56.0.7'
15
+ version = '56.0.9'
16
16
 
17
17
  android {
18
18
  namespace "expo.modules.ui"
19
19
  defaultConfig {
20
20
  versionCode 1
21
- versionName "56.0.7"
21
+ versionName "56.0.9"
22
22
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
23
23
  }
24
24
  buildFeatures {
@@ -2,6 +2,7 @@
2
2
 
3
3
  package expo.modules.ui
4
4
 
5
+ import android.os.Looper
5
6
  import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
6
7
  import androidx.compose.material3.SwitchDefaults
7
8
  import androidx.compose.material3.ToggleButtonDefaults
@@ -39,6 +40,7 @@ import expo.modules.ui.menu.ExposedDropdownMenuBoxContent
39
40
  import expo.modules.ui.menu.ExposedDropdownMenuBoxProps
40
41
  import expo.modules.ui.menu.ExposedDropdownMenuContent
41
42
  import expo.modules.ui.menu.ExposedDropdownMenuProps
43
+ import kotlinx.coroutines.launch
42
44
  import okhttp3.OkHttpClient
43
45
 
44
46
  class ExpoUIModule : Module() {
@@ -79,14 +81,28 @@ class ExpoUIModule : Module() {
79
81
  }
80
82
 
81
83
  Function("setValue") { state: ObservableState, wrapper: Map<String, Any?> ->
82
- state.value = wrapper["value"]
84
+ val newValue = wrapper["value"]
85
+ val mainLooper = Looper.getMainLooper()
86
+ // Update state on the UI thread
87
+ if (mainLooper.isCurrentThread) {
88
+ state.value = newValue
89
+ } else {
90
+ appContext.mainQueue.launch {
91
+ state.value = newValue
92
+ }
93
+ }
94
+ }
95
+
96
+ Function("setOnChange") { state: ObservableState, callback: WorkletCallback? ->
97
+ state.onChange = callback
83
98
  }
84
99
  }
85
100
 
86
101
  //region Views use expo-modules-core DSL for uncommon features
87
102
 
88
103
  View(HostView::class) {
89
- Events("onLayoutContent")
104
+ // See ShadowNodeSyncFlush.kt for why onExpoUISyncFlush is needed.
105
+ Events("onLayoutContent", "onExpoUISyncFlush")
90
106
 
91
107
  OnViewDidUpdateProps { view ->
92
108
  view.onViewDidUpdateProps()
@@ -123,7 +139,10 @@ class ExpoUIModule : Module() {
123
139
  colorScheme.toTokenMap()
124
140
  }
125
141
 
126
- View(RNHostView::class)
142
+ View(RNHostView::class) {
143
+ // See ShadowNodeSyncFlush.kt for why this internal phantom event is needed.
144
+ Events("onExpoUISyncFlush")
145
+ }
127
146
 
128
147
  View(SlotView::class) {
129
148
  Events("onSlotEvent")
@@ -135,6 +154,8 @@ class ExpoUIModule : Module() {
135
154
  // Class-based views so TooltipBoxView can detect them by type via findChildOfType
136
155
  View(PlainTooltipView::class)
137
156
  View(RichTooltipView::class)
157
+ // Class-based view so SnackbarHostView can read its styling via findChildOfType
158
+ View(SnackbarView::class)
138
159
 
139
160
  //endregion Views use expo-modules-core DSL for uncommon features
140
161
 
@@ -336,6 +357,18 @@ class ExpoUIModule : Module() {
336
357
  }
337
358
  }
338
359
 
360
+ ExpoUIView<LoadingIndicatorProps>("LoadingIndicatorView") {
361
+ Content { props ->
362
+ LoadingIndicatorContent(props)
363
+ }
364
+ }
365
+
366
+ ExpoUIView<ContainedLoadingIndicatorProps>("ContainedLoadingIndicatorView") {
367
+ Content { props ->
368
+ ContainedLoadingIndicatorContent(props)
369
+ }
370
+ }
371
+
339
372
  ExpoUIView<LinearProgressIndicatorProps>("LinearProgressIndicatorView") {
340
373
  Content { props ->
341
374
  LinearProgressIndicatorContent(props)
@@ -592,6 +625,14 @@ class ExpoUIModule : Module() {
592
625
  }
593
626
  }
594
627
 
628
+ ExpoUIView<SnackbarHostProps>("SnackbarHostView") {
629
+ val showSnackbar by AsyncFunction<SnackbarShowOptions>()
630
+
631
+ Content { props ->
632
+ SnackbarHostContent(props, showSnackbar)
633
+ }
634
+ }
635
+
595
636
  ExpoUIView<TooltipBoxViewProps>("TooltipBoxView") {
596
637
  val show by AsyncFunction()
597
638
  val dismiss by AsyncFunction()
@@ -193,6 +193,7 @@ internal class HostView(context: Context, appContext: AppContext) :
193
193
  if (constraints.maxWidth == 0) widthDp else Double.NaN,
194
194
  if (constraints.maxHeight == 0) heightDp else Double.NaN
195
195
  )
196
+ flushPendingStateUpdates()
196
197
  }
197
198
  }
198
199
 
@@ -221,6 +222,7 @@ internal class HostView(context: Context, appContext: AppContext) :
221
222
  val styleWidth = if (matchContentsHorizontal == true && width > 0) width else null
222
223
  val styleHeight = if (matchContentsVertical == true && height > 0) height else null
223
224
  shadowNodeProxy.setStyleSize(styleWidth?.toDouble(), styleHeight?.toDouble())
225
+ flushPendingStateUpdates()
224
226
  }
225
227
 
226
228
  onLayoutContent(LayoutContentEvent(width.toDouble(), height.toDouble()))
@@ -0,0 +1,80 @@
1
+ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
2
+
3
+ package expo.modules.ui
4
+
5
+ import android.graphics.Color
6
+ import androidx.compose.material3.ContainedLoadingIndicator
7
+ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
8
+ import androidx.compose.material3.LoadingIndicator
9
+ import androidx.compose.material3.LoadingIndicatorDefaults
10
+ import androidx.compose.runtime.Composable
11
+ import expo.modules.kotlin.views.ComposeProps
12
+ import expo.modules.kotlin.views.FunctionalComposableScope
13
+ import expo.modules.kotlin.views.OptimizedComposeProps
14
+ import expo.modules.ui.state.ObservableState
15
+
16
+ // region LoadingIndicator
17
+
18
+ @OptimizedComposeProps
19
+ data class LoadingIndicatorProps(
20
+ val progress: ObservableState? = null,
21
+ val color: Color? = null,
22
+ val modifiers: ModifierList = emptyList()
23
+ ) : ComposeProps
24
+
25
+ @Composable
26
+ fun FunctionalComposableScope.LoadingIndicatorContent(props: LoadingIndicatorProps) {
27
+ val modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
28
+ val indicatorColor = props.color.composeOrNull ?: LoadingIndicatorDefaults.indicatorColor
29
+
30
+ val progressState = props.progress
31
+ if (progressState != null) {
32
+ LoadingIndicator(
33
+ progress = { (progressState.value as? Number)?.toFloat() ?: 0f },
34
+ modifier = modifier,
35
+ color = indicatorColor
36
+ )
37
+ } else {
38
+ LoadingIndicator(
39
+ modifier = modifier,
40
+ color = indicatorColor
41
+ )
42
+ }
43
+ }
44
+
45
+ // endregion
46
+
47
+ // region ContainedLoadingIndicator
48
+
49
+ @OptimizedComposeProps
50
+ data class ContainedLoadingIndicatorProps(
51
+ val progress: ObservableState? = null,
52
+ val color: Color? = null,
53
+ val containerColor: Color? = null,
54
+ val modifiers: ModifierList = emptyList()
55
+ ) : ComposeProps
56
+
57
+ @Composable
58
+ fun FunctionalComposableScope.ContainedLoadingIndicatorContent(props: ContainedLoadingIndicatorProps) {
59
+ val modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
60
+ val indicatorColor = props.color.composeOrNull ?: LoadingIndicatorDefaults.containedIndicatorColor
61
+ val containerColor = props.containerColor.composeOrNull ?: LoadingIndicatorDefaults.containedContainerColor
62
+
63
+ val progressState = props.progress
64
+ if (progressState != null) {
65
+ ContainedLoadingIndicator(
66
+ progress = { (progressState.value as? Number)?.toFloat() ?: 0f },
67
+ modifier = modifier,
68
+ indicatorColor = indicatorColor,
69
+ containerColor = containerColor
70
+ )
71
+ } else {
72
+ ContainedLoadingIndicator(
73
+ modifier = modifier,
74
+ indicatorColor = indicatorColor,
75
+ containerColor = containerColor
76
+ )
77
+ }
78
+ }
79
+
80
+ // endregion
@@ -1,15 +1,21 @@
1
- @file:OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
1
+ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
2
2
 
3
3
  package expo.modules.ui
4
4
 
5
5
  import android.graphics.Color
6
+ import androidx.compose.animation.animateColorAsState
6
7
  import androidx.compose.animation.animateContentSize
8
+ import androidx.compose.animation.core.AnimationSpec
7
9
  import androidx.compose.animation.core.Spring
10
+ import androidx.compose.animation.core.snap
8
11
  import androidx.compose.animation.core.spring
12
+ import androidx.compose.animation.core.tween
9
13
  import androidx.compose.foundation.BorderStroke
14
+ import androidx.compose.foundation.ExperimentalFoundationApi
10
15
  import androidx.compose.foundation.background
11
16
  import androidx.compose.foundation.border
12
17
  import androidx.compose.foundation.clickable
18
+ import androidx.compose.foundation.combinedClickable
13
19
  import androidx.compose.foundation.horizontalScroll
14
20
  import androidx.compose.foundation.rememberScrollState
15
21
  import androidx.compose.foundation.verticalScroll
@@ -36,6 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
36
42
  import androidx.compose.material3.ExposedDropdownMenuAnchorType
37
43
  import androidx.compose.material3.toShape
38
44
  import androidx.compose.runtime.Composable
45
+ import androidx.compose.runtime.getValue
39
46
  import androidx.compose.ui.Modifier
40
47
  import androidx.compose.ui.draw.alpha
41
48
  import androidx.compose.ui.draw.blur
@@ -145,6 +152,24 @@ internal data class BackgroundParams(
145
152
  @Field val color: Color? = null
146
153
  ) : Record
147
154
 
155
+ // Color animation specs reuse the JS `$type` shape from `@expo/ui/jetpack-compose/modifiers/animation` (spring / tween / snap).
156
+ // Keyframes are float-only and aren't supported for colors.
157
+ private fun parseColorAnimationSpec(raw: Any?): AnimationSpec<androidx.compose.ui.graphics.Color>? {
158
+ if (raw !is Map<*, *>) return null
159
+ return when (raw["\$type"]) {
160
+ "spring" -> spring(
161
+ dampingRatio = (raw["dampingRatio"] as? Number)?.toFloat() ?: Spring.DampingRatioNoBouncy,
162
+ stiffness = (raw["stiffness"] as? Number)?.toFloat() ?: Spring.StiffnessMedium
163
+ )
164
+ "tween" -> tween(
165
+ durationMillis = (raw["durationMillis"] as? Number)?.toInt() ?: 300,
166
+ delayMillis = (raw["delayMillis"] as? Number)?.toInt() ?: 0
167
+ )
168
+ "snap" -> snap(delayMillis = (raw["delayMillis"] as? Number)?.toInt() ?: 0)
169
+ else -> null
170
+ }
171
+ }
172
+
148
173
  @OptimizedRecord
149
174
  internal data class BorderParams(
150
175
  @Field val borderWidth: Int = 1,
@@ -455,9 +480,14 @@ object ModifierRegistry {
455
480
  // Appearance modifiers
456
481
  register("background") { map, _, _, _ ->
457
482
  val params = recordFromMap<BackgroundParams>(map)
458
- params.color?.let { color ->
459
- Modifier.background(color.compose)
460
- } ?: Modifier
483
+ val color = params.color?.compose ?: return@register Modifier
484
+ val spec = parseColorAnimationSpec(map["animationSpec"])
485
+ if (spec != null) {
486
+ val animated by animateColorAsState(color, spec, label = "background-color")
487
+ Modifier.background(animated)
488
+ } else {
489
+ Modifier.background(color)
490
+ }
461
491
  }
462
492
 
463
493
  register("border") { map, _, _, _ ->
@@ -633,6 +663,22 @@ object ModifierRegistry {
633
663
  }
634
664
  }
635
665
 
666
+ register("combinedClickable") { map, _, _, eventDispatcher ->
667
+ val params = recordFromMap<ClickableParams>(map)
668
+ val onClick = { eventDispatcher("combinedClickable", mapOf("event" to "click")) }
669
+ val onLongClick = { eventDispatcher("combinedClickable", mapOf("event" to "longClick")) }
670
+ if (params.indication) {
671
+ Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick)
672
+ } else {
673
+ Modifier.combinedClickable(
674
+ interactionSource = null,
675
+ indication = null,
676
+ onClick = onClick,
677
+ onLongClick = onLongClick
678
+ )
679
+ }
680
+ }
681
+
636
682
  register("selectable") { map, _, _, eventDispatcher ->
637
683
  val params = recordFromMap<SelectableParams>(map)
638
684
  Modifier.selectable(
@@ -36,7 +36,8 @@ import expo.modules.kotlin.views.OptimizedComposeProps
36
36
 
37
37
  @OptimizedComposeProps
38
38
  internal data class RNHostViewProps(
39
- val matchContents: MutableState<Boolean?> = mutableStateOf(null)
39
+ val matchContents: MutableState<Boolean?> = mutableStateOf(null),
40
+ val modifiers: ModifierList = emptyList()
40
41
  ) : ComposeProps
41
42
 
42
43
  @SuppressLint("ViewConstructor")
@@ -86,23 +87,26 @@ internal class RNHostView(context: Context, appContext: AppContext) :
86
87
  @Composable
87
88
  override fun ComposableScope.Content() {
88
89
  val matchContents = props.matchContents.value ?: false
90
+ val scope: ComposableScope = this
89
91
 
90
92
  wrapperState.value?.let { wrapper ->
91
93
  val childView = childViewState.value ?: return@let
92
- val modifier = if (matchContents) {
94
+ val sizingModifier = if (matchContents) {
93
95
  applySizeFromYogaNodeModifier(childView)
94
96
  } else {
95
97
  Modifier
96
98
  .fillMaxSize()
97
99
  .then(reportSizeToYogaNodeModifier())
98
100
  }
101
+ val modifiers = sizingModifier
102
+ .then(ModifierRegistry.applyModifiers(props.modifiers, appContext, scope, globalEventDispatcher))
99
103
 
100
104
  AndroidView(
101
105
  factory = {
102
106
  (wrapper.parent as? ViewGroup)?.removeView(wrapper)
103
107
  wrapper
104
108
  },
105
- modifier = modifier
109
+ modifier = modifiers
106
110
  )
107
111
  }
108
112
  }
@@ -154,6 +158,7 @@ internal class RNHostView(context: Context, appContext: AppContext) :
154
158
  size.width.toDp().value.toDouble(),
155
159
  size.height.toDp().value.toDouble()
156
160
  )
161
+ flushPendingStateUpdates()
157
162
  }
158
163
  }
159
164
  }
@@ -0,0 +1,28 @@
1
+ package expo.modules.ui
2
+
3
+ import android.view.View
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.ReactContext
6
+ import com.facebook.react.bridge.WritableMap
7
+ import com.facebook.react.uimanager.UIManagerHelper
8
+ import com.facebook.react.uimanager.events.Event
9
+
10
+ // Workaround helper that triggers a synchronous event to flush a pending
11
+ // shadow-node state update in the current event beat. Mirrors iOS's
12
+ // `EventQueue::UpdateMode::unstable_Immediate`, which Android does not expose
13
+ // to Java/Kotlin as of now.
14
+ // TODO: Remove when a synchronous state update API is exposed on Android.
15
+ // https://github.com/facebook/react-native/pull/56311
16
+ internal fun View.flushPendingStateUpdates() {
17
+ val reactContext = context as? ReactContext ?: return
18
+ val surfaceId = UIManagerHelper.getSurfaceId(this)
19
+ UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
20
+ ?.dispatchEvent(SyncFlushEvent(surfaceId, id))
21
+ }
22
+
23
+ private class SyncFlushEvent(surfaceId: Int, viewTag: Int) : Event<SyncFlushEvent>(surfaceId, viewTag) {
24
+ override fun getEventName(): String = "topExpoUISyncFlush"
25
+ override fun getEventData(): WritableMap = Arguments.createMap()
26
+ override fun canCoalesce(): Boolean = true
27
+ override fun experimental_isSynchronous(): Boolean = true
28
+ }
@@ -0,0 +1,126 @@
1
+ @file:OptIn(ExperimentalMaterial3Api::class)
2
+
3
+ package expo.modules.ui
4
+
5
+ import android.annotation.SuppressLint
6
+ import android.content.Context
7
+ import android.graphics.Color
8
+ import androidx.compose.material3.ExperimentalMaterial3Api
9
+ import androidx.compose.material3.Snackbar
10
+ import androidx.compose.material3.SnackbarDefaults
11
+ import androidx.compose.material3.SnackbarDuration
12
+ import androidx.compose.material3.SnackbarHost
13
+ import androidx.compose.material3.SnackbarHostState
14
+ import androidx.compose.material3.SnackbarResult
15
+ import androidx.compose.runtime.Composable
16
+ import androidx.compose.runtime.MutableState
17
+ import androidx.compose.runtime.mutableStateOf
18
+ import androidx.compose.runtime.remember
19
+ import androidx.compose.runtime.rememberCoroutineScope
20
+ import expo.modules.kotlin.AppContext
21
+ import expo.modules.kotlin.records.Field
22
+ import expo.modules.kotlin.records.Record
23
+ import expo.modules.kotlin.types.OptimizedRecord
24
+ import expo.modules.kotlin.views.AsyncFunctionHandle
25
+ import expo.modules.kotlin.views.ComposableScope
26
+ import expo.modules.kotlin.views.ComposeProps
27
+ import expo.modules.kotlin.views.ExpoComposeView
28
+ import expo.modules.kotlin.views.FunctionalComposableScope
29
+ import expo.modules.kotlin.views.OptimizedComposeProps
30
+ import kotlinx.coroutines.withContext
31
+ import kotlin.coroutines.cancellation.CancellationException
32
+
33
+ // Holds styling props that `SnackbarHost` reads via `findChildOfType`.
34
+
35
+ @OptimizedComposeProps
36
+ data class SnackbarViewProps(
37
+ val containerColor: MutableState<Color?> = mutableStateOf(null),
38
+ val contentColor: MutableState<Color?> = mutableStateOf(null),
39
+ val actionContentColor: MutableState<Color?> = mutableStateOf(null),
40
+ val dismissActionContentColor: MutableState<Color?> = mutableStateOf(null),
41
+ val actionOnNewLine: MutableState<Boolean> = mutableStateOf(false),
42
+ val modifiers: MutableState<ModifierList> = mutableStateOf(emptyList())
43
+ ) : ComposeProps
44
+
45
+ @SuppressLint("ViewConstructor")
46
+ class SnackbarView(context: Context, appContext: AppContext) :
47
+ ExpoComposeView<SnackbarViewProps>(context, appContext) {
48
+ override val props = SnackbarViewProps()
49
+
50
+ @Composable
51
+ override fun ComposableScope.Content() {
52
+ // Empty by design: `SnackbarHost` renders the snackbar using the props above.
53
+ }
54
+ }
55
+
56
+ // --- SnackbarHostView ---
57
+
58
+ @OptimizedRecord
59
+ data class SnackbarShowOptions(
60
+ @Field val message: String = "",
61
+ @Field val actionLabel: String? = null,
62
+ @Field val withDismissAction: Boolean = false,
63
+ @Field val duration: String? = null
64
+ ) : Record
65
+
66
+ @OptimizedComposeProps
67
+ data class SnackbarHostProps(
68
+ val modifiers: ModifierList = emptyList()
69
+ ) : ComposeProps
70
+
71
+ @Composable
72
+ fun FunctionalComposableScope.SnackbarHostContent(
73
+ props: SnackbarHostProps,
74
+ showSnackbar: AsyncFunctionHandle<SnackbarShowOptions>
75
+ ) {
76
+ val hostState = remember { SnackbarHostState() }
77
+ val scope = rememberCoroutineScope()
78
+ val snackbarConfig = findChildOfType<SnackbarView>(view)
79
+
80
+ showSnackbar.handle { options ->
81
+ val duration = when (options.duration) {
82
+ "short" -> SnackbarDuration.Short
83
+ "long" -> SnackbarDuration.Long
84
+ "indefinite" -> SnackbarDuration.Indefinite
85
+ // M3 default: indefinite when there's an action label, else short.
86
+ else -> if (options.actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite
87
+ }
88
+ val result = try {
89
+ withContext(scope.coroutineContext) {
90
+ hostState.showSnackbar(
91
+ message = options.message,
92
+ actionLabel = options.actionLabel,
93
+ withDismissAction = options.withDismissAction,
94
+ duration = duration
95
+ )
96
+ }
97
+ } catch (_: CancellationException) {
98
+ // The compose scope can be cancelled if the view is disposed before user dismisses the snackbar or performs the action.
99
+ // In that case, we treat it as if the snackbar was dismissed.
100
+ SnackbarResult.Dismissed
101
+ }
102
+ when (result) {
103
+ SnackbarResult.ActionPerformed -> "actionPerformed"
104
+ SnackbarResult.Dismissed -> "dismissed"
105
+ }
106
+ }
107
+
108
+ SnackbarHost(
109
+ hostState = hostState,
110
+ modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
111
+ ) { data ->
112
+ Snackbar(
113
+ snackbarData = data,
114
+ modifier = snackbarConfig?.let {
115
+ ModifierRegistry.applyModifiers(it.props.modifiers.value, appContext, composableScope, globalEventDispatcher)
116
+ } ?: androidx.compose.ui.Modifier,
117
+ actionOnNewLine = snackbarConfig?.props?.actionOnNewLine?.value ?: false,
118
+ containerColor = snackbarConfig?.props?.containerColor?.value.composeOrNull ?: SnackbarDefaults.color,
119
+ contentColor = snackbarConfig?.props?.contentColor?.value.composeOrNull ?: SnackbarDefaults.contentColor,
120
+ actionContentColor = snackbarConfig?.props?.actionContentColor?.value.composeOrNull
121
+ ?: SnackbarDefaults.actionContentColor,
122
+ dismissActionContentColor = snackbarConfig?.props?.dismissActionContentColor?.value.composeOrNull
123
+ ?: SnackbarDefaults.dismissActionContentColor
124
+ )
125
+ }
126
+ }
@@ -11,11 +11,21 @@ import expo.modules.kotlin.sharedobjects.SharedObject
11
11
  */
12
12
  class ObservableState(initialValue: Any? = null) : SharedObject() {
13
13
  private val _state: MutableState<Any?> = mutableStateOf(initialValue)
14
+ internal var onChange: WorkletCallback? = null
15
+ private var isNotifying = false
14
16
 
15
17
  var value: Any?
16
18
  get() = _state.value
17
19
  set(v) {
18
20
  _state.value = v
21
+ // Skip re-invoking onChange if state.value was written from inside onChange.
22
+ if (isNotifying) return
23
+ isNotifying = true
24
+ try {
25
+ onChange?.invoke(v)
26
+ } finally {
27
+ isNotifying = false
28
+ }
19
29
  }
20
30
 
21
31
  @Suppress("UNCHECKED_CAST")
@@ -0,0 +1,10 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+ android:width="24dp"
3
+ android:height="24dp"
4
+ android:viewportWidth="960"
5
+ android:viewportHeight="960"
6
+ android:tint="?attr/colorControlNormal">
7
+ <path
8
+ android:fillColor="@android:color/white"
9
+ android:pathData="M480,616L240,376L296,320L480,504L664,320L720,376L480,616Z"/>
10
+ </vector>
@@ -5,11 +5,40 @@ import { type SharedObject } from 'expo-modules-core';
5
5
  */
6
6
  export type ObservableState<T> = SharedObject & {
7
7
  /**
8
- * The current value. Reads are safe from any thread; prefer writing from a worklet
9
- * so the update runs on the native UI thread. Updating state from the JS thread
10
- * might show a development warning.
8
+ * The current value.
9
+ *
10
+ * Writes from a UI worklet are synchronous and immediately readable. Writes
11
+ * from the JS thread are scheduled to the UI thread asynchronously, the new value is not readable until the update has been
12
+ * applied. Prefer writing from a worklet when you need synchronous updates
11
13
  */
12
14
  value: T;
15
+ /**
16
+ * A single listener invoked on the native UI runtime whenever the value changes
17
+ * (after iOS `didSet` and Android's setter). Assigning replaces the previous
18
+ * listener; assign `null` to clear. The initial value does not fire `onChange`.
19
+ *
20
+ * The callback must be a worklet so it can run synchronously on the UI thread.
21
+ * Attach it inside `useEffect` and clear it in the cleanup so the listener
22
+ * lifecycle matches the component lifecycle.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * const state = useNativeState(0);
27
+ *
28
+ * useEffect(() => {
29
+ * state.onChange = (value) => {
30
+ * 'worklet';
31
+ * console.log('changed to', value);
32
+ * };
33
+ * return () => {
34
+ * state.onChange = null;
35
+ * };
36
+ * }, []);
37
+ * ```
38
+ */
39
+ onChange: {
40
+ listener(value: T): void;
41
+ }['listener'] | null;
13
42
  };
14
43
  /**
15
44
  * Creates an observable native state that is automatically cleaned up when the