expo-modules-core 55.0.9 → 55.0.10
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.
- package/CHANGELOG.md +13 -0
- package/android/build.gradle +2 -2
- package/android/src/compose/expo/modules/kotlin/views/ExpoComposeAndroidView.kt +10 -1
- package/android/src/compose/expo/modules/kotlin/views/ExpoComposeView.kt +62 -2
- package/android/src/compose/expo/modules/kotlin/views/ModuleDefinitionBuilderComposeExtension.kt +12 -3
- package/android/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMapBackingFieldAccessor.java +12 -0
- package/android/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMapHelper.kt +1 -19
- package/android/src/main/java/expo/modules/adapters/react/apploader/RNHeadlessAppLoader.kt +37 -82
- package/android/src/main/java/expo/modules/adapters/react/services/UIManagerModuleWrapper.java +0 -119
- package/android/src/main/java/expo/modules/apploader/AppLoaderProvider.kt +2 -2
- package/android/src/main/java/expo/modules/apploader/HeadlessAppLoader.java +1 -15
- package/android/src/main/java/expo/modules/core/interfaces/services/UIManager.java +0 -30
- package/android/src/main/java/expo/modules/core/utilities/KotlinUtilities.kt +0 -11
- package/android/src/main/java/expo/modules/kotlin/apifeatures/Features.kt +1 -0
- package/android/src/main/java/expo/modules/kotlin/records/RecordTypeConverter.kt +5 -0
- package/android/src/main/java/expo/modules/kotlin/types/Either.kt +0 -4
- package/android/src/main/java/expo/modules/kotlin/types/EitherTypeConverter.kt +0 -4
- package/android/src/main/java/expo/modules/kotlin/types/TypeConverterProvider.kt +0 -2
- package/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/ProjectConfiguration.kt +4 -1
- package/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/android/MavenPublicationExtension.kt +50 -25
- package/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/gradle/ExpoModuleExtension.kt +13 -3
- package/ios/Core/AppContext.swift +1 -28
- package/ios/Core/Functions/AsyncFunctionDefinition.swift +0 -2
- package/ios/Core/Logging/PersistentFileLog.swift +5 -3
- package/ios/Core/ModuleHolder.swift +0 -17
- package/ios/Core/Views/ExpoView.swift +0 -39
- package/ios/Core/Views/SwiftUI/SwiftUIHostingView.swift +0 -6
- package/ios/Core/Views/SwiftUI/SwiftUIViewDefinition.swift +0 -4
- package/ios/Core/Views/ViewDefinition.swift +0 -5
- package/ios/ExpoModulesCore.h +0 -2
- package/ios/Fabric/ExpoFabricView.swift +0 -4
- package/ios/Fabric/ExpoFabricViewObjC.h +2 -10
- package/ios/Fabric/ExpoFabricViewObjC.mm +0 -4
- package/ios/Legacy/Services/EXReactNativeAdapter.h +1 -2
- package/package.json +2 -2
- package/FormatterTests.swift +0 -111
- package/android/src/main/java/expo/modules/adapters/react/apploader/HeadlessAppLoaderNotifier.kt +0 -31
- package/android/src/main/java/expo/modules/apploader/AppLoaderPackagesProviderInterface.java +0 -17
- package/android/src/main/java/expo/modules/core/MapHelper.java +0 -142
- package/android/src/main/java/expo/modules/core/errors/CurrentActivityNotFoundException.java +0 -14
- package/android/src/main/java/expo/modules/core/errors/ModuleNotFoundException.java +0 -14
- package/android/src/main/java/expo/modules/core/interfaces/Function.java +0 -7
- package/android/src/main/java/expo/modules/core/interfaces/services/KeepAwakeManager.java +0 -11
- package/android/src/main/java/expo/modules/interfaces/facedetector/FaceDetectionError.java +0 -7
- package/android/src/main/java/expo/modules/interfaces/facedetector/FaceDetectionSkipped.java +0 -7
- package/android/src/main/java/expo/modules/interfaces/facedetector/FaceDetectionUnspecifiedError.java +0 -4
- package/android/src/main/java/expo/modules/interfaces/facedetector/FaceDetectorInterface.java +0 -18
- package/android/src/main/java/expo/modules/interfaces/facedetector/FaceDetectorProviderInterface.java +0 -7
- package/android/src/main/java/expo/modules/interfaces/facedetector/FacesDetectionCompleted.java +0 -11
- package/ios/EXLegacyExpoViewProtocol.h +0 -13
- package/ios/Legacy/EXBridgeModule.h +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 55.0.10 — 2026-02-20
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [iOS] Fix crash from `PersistentFileLog`. ([#43283](https://github.com/expo/expo/pull/43283) by [@alanjhughes](https://github.com/alanjhughes))
|
|
18
|
+
|
|
19
|
+
### 💡 Others
|
|
20
|
+
|
|
21
|
+
- Fixed view updates for Jetpack Compose integration. ([#42732](https://github.com/expo/expo/pull/42732) by [@kudo](https://github.com/kudo))
|
|
22
|
+
- [Android] Promoted `Either` type stable. ([#43267](https://github.com/expo/expo/pull/43267) by [@lukmccall](https://github.com/lukmccall))
|
|
23
|
+
|
|
13
24
|
## 55.0.9 — 2026-02-16
|
|
14
25
|
|
|
15
26
|
### 🐛 Bug fixes
|
|
@@ -20,6 +31,7 @@
|
|
|
20
31
|
### 💡 Others
|
|
21
32
|
|
|
22
33
|
- Removed needless warning when `NativeModulesProxy` is absent. ([#43020](https://github.com/expo/expo/pull/43020) by [@tsapeta](https://github.com/tsapeta))
|
|
34
|
+
- [iOS] Removed some unused code. ([#42949](https://github.com/expo/expo/pull/42949) by [@tsapeta](https://github.com/tsapeta))
|
|
23
35
|
|
|
24
36
|
## 55.0.8 — 2026-02-08
|
|
25
37
|
|
|
@@ -47,6 +59,7 @@
|
|
|
47
59
|
|
|
48
60
|
- [iOS] Fixed a crash in Fabric when unmounting a view while a geometry change event is being dispatched. ([#42628](https://github.com/expo/expo/issues/42628) by [@danishshaik](https://github.com/danishshaik)) ([#42634](https://github.com/expo/expo/pull/42634) by [@danishshaik](https://github.com/danishshaik))
|
|
49
61
|
- [iOS] Fix crashes when converting a single JSValue into an Array. ([#42694](https://github.com/expo/expo/pull/42694) by [@behenate](https://github.com/behenate))
|
|
62
|
+
- Added more Jetpack Compose support. ([#42734](https://github.com/expo/expo/pull/42734) by [@kudo](https://github.com/kudo))
|
|
50
63
|
|
|
51
64
|
## 55.0.5 — 2026-01-27
|
|
52
65
|
|
package/android/build.gradle
CHANGED
|
@@ -29,7 +29,7 @@ if (shouldIncludeCompose) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
group = 'host.exp.exponent'
|
|
32
|
-
version = '55.0.
|
|
32
|
+
version = '55.0.10'
|
|
33
33
|
|
|
34
34
|
def isExpoModulesCoreTests = {
|
|
35
35
|
Gradle gradle = getGradle()
|
|
@@ -96,7 +96,7 @@ android {
|
|
|
96
96
|
defaultConfig {
|
|
97
97
|
consumerProguardFiles 'proguard-rules.pro'
|
|
98
98
|
versionCode 1
|
|
99
|
-
versionName "55.0.
|
|
99
|
+
versionName "55.0.10"
|
|
100
100
|
buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
|
|
101
101
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
|
|
102
102
|
|
|
@@ -10,6 +10,13 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|
|
10
10
|
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
11
11
|
import expo.modules.kotlin.AppContext
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Marks a view as capable of crossing the Jetpack Compose -> React Native boundary.
|
|
15
|
+
*/
|
|
16
|
+
interface RNHostViewInterface {
|
|
17
|
+
var matchContents: Boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
21
|
* An ExpoComposeView for [AndroidView] wrapping with existing view
|
|
15
22
|
*/
|
|
@@ -17,7 +24,9 @@ import expo.modules.kotlin.AppContext
|
|
|
17
24
|
internal class ExpoComposeAndroidView(
|
|
18
25
|
private val view: View,
|
|
19
26
|
appContext: AppContext
|
|
20
|
-
) : ExpoComposeView<ComposeProps>(view.context, appContext) {
|
|
27
|
+
) : ExpoComposeView<ComposeProps>(view.context, appContext), RNHostViewInterface {
|
|
28
|
+
override var matchContents = false
|
|
29
|
+
|
|
21
30
|
@Composable
|
|
22
31
|
override fun ComposableScope.Content() {
|
|
23
32
|
AndroidView(
|
|
@@ -8,22 +8,34 @@ import androidx.compose.foundation.layout.BoxScope
|
|
|
8
8
|
import androidx.compose.foundation.layout.ColumnScope
|
|
9
9
|
import androidx.compose.foundation.layout.RowScope
|
|
10
10
|
import androidx.compose.runtime.Composable
|
|
11
|
+
import androidx.compose.runtime.RecomposeScope
|
|
12
|
+
import androidx.compose.runtime.currentRecomposeScope
|
|
11
13
|
import androidx.compose.runtime.getValue
|
|
12
14
|
import androidx.compose.runtime.mutableStateOf
|
|
15
|
+
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
13
16
|
import androidx.compose.ui.platform.ComposeView
|
|
14
17
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
15
18
|
import androidx.core.view.size
|
|
16
19
|
import expo.modules.kotlin.AppContext
|
|
17
20
|
import expo.modules.kotlin.viewevent.CoalescingKey
|
|
18
21
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
22
|
+
import expo.modules.kotlin.viewevent.ViewEvent
|
|
19
23
|
import expo.modules.kotlin.viewevent.ViewEventDelegate
|
|
20
24
|
|
|
21
25
|
data class ComposableScope(
|
|
22
26
|
val rowScope: RowScope? = null,
|
|
23
27
|
val columnScope: ColumnScope? = null,
|
|
24
|
-
val boxScope: BoxScope? = null
|
|
28
|
+
val boxScope: BoxScope? = null,
|
|
29
|
+
val nestedScrollConnection: NestedScrollConnection? = null
|
|
25
30
|
)
|
|
26
31
|
|
|
32
|
+
inline fun ComposableScope.withIf(
|
|
33
|
+
condition: Boolean,
|
|
34
|
+
block: ComposableScope.() -> ComposableScope
|
|
35
|
+
): ComposableScope {
|
|
36
|
+
return if (condition) block() else this
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
fun ComposableScope.with(rowScope: RowScope?): ComposableScope {
|
|
28
40
|
return this.copy(rowScope = rowScope)
|
|
29
41
|
}
|
|
@@ -36,6 +48,10 @@ fun ComposableScope.with(boxScope: BoxScope?): ComposableScope {
|
|
|
36
48
|
return this.copy(boxScope = boxScope)
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
fun ComposableScope.with(nestedScrollConnection: NestedScrollConnection?): ComposableScope {
|
|
52
|
+
return this.copy(nestedScrollConnection = nestedScrollConnection)
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
/**
|
|
40
56
|
* A base class that should be used by compose views.
|
|
41
57
|
*/
|
|
@@ -45,6 +61,16 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
45
61
|
private val withHostingView: Boolean = false
|
|
46
62
|
) : ExpoView(context, appContext) {
|
|
47
63
|
open val props: T? = null
|
|
64
|
+
protected var recomposeScope: RecomposeScope? = null
|
|
65
|
+
|
|
66
|
+
private val globalEvent = ViewEvent<Pair<String, Map<String, Any?>>>(GLOBAL_EVENT_NAME, this, null)
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A global event dispatcher
|
|
70
|
+
*/
|
|
71
|
+
val globalEventDispatcher: (String, Map<String, Any?>) -> Unit = { name, params ->
|
|
72
|
+
globalEvent.invoke(Pair(name, params))
|
|
73
|
+
}
|
|
48
74
|
|
|
49
75
|
@Composable
|
|
50
76
|
abstract fun ComposableScope.Content()
|
|
@@ -78,8 +104,25 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
78
104
|
|
|
79
105
|
@Composable
|
|
80
106
|
fun Children(composableScope: ComposableScope?) {
|
|
107
|
+
recomposeScope = currentRecomposeScope
|
|
108
|
+
for (index in 0..<this.size) {
|
|
109
|
+
val child = getChildAt(index) as? ExpoComposeView<*> ?: continue
|
|
110
|
+
with(composableScope ?: ComposableScope()) {
|
|
111
|
+
with(child) {
|
|
112
|
+
Content()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Composable
|
|
119
|
+
fun Children(composableScope: ComposableScope?, filter: (child: ExpoComposeView<*>) -> Boolean) {
|
|
120
|
+
recomposeScope = currentRecomposeScope
|
|
81
121
|
for (index in 0..<this.size) {
|
|
82
122
|
val child = getChildAt(index) as? ExpoComposeView<*> ?: continue
|
|
123
|
+
if (!filter(child)) {
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
83
126
|
with(composableScope ?: ComposableScope()) {
|
|
84
127
|
with(child) {
|
|
85
128
|
Content()
|
|
@@ -90,6 +133,7 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
90
133
|
|
|
91
134
|
@Composable
|
|
92
135
|
fun Child(composableScope: ComposableScope, index: Int) {
|
|
136
|
+
recomposeScope = currentRecomposeScope
|
|
93
137
|
val child = getChildAt(index) as? ExpoComposeView<*> ?: return
|
|
94
138
|
with(composableScope) {
|
|
95
139
|
with(child) {
|
|
@@ -133,13 +177,23 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
133
177
|
}
|
|
134
178
|
|
|
135
179
|
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) {
|
|
136
|
-
val view = if (child !is ExpoComposeView<*> && child !is ComposeView) {
|
|
180
|
+
val view = if (child !is ExpoComposeView<*> && child !is ComposeView && this !is RNHostViewInterface) {
|
|
137
181
|
ExpoComposeAndroidView(child, appContext)
|
|
138
182
|
} else {
|
|
139
183
|
child
|
|
140
184
|
}
|
|
141
185
|
super.addView(view, index, params)
|
|
142
186
|
}
|
|
187
|
+
|
|
188
|
+
override fun onViewAdded(child: View?) {
|
|
189
|
+
super.onViewAdded(child)
|
|
190
|
+
recomposeScope?.invalidate()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
override fun onViewRemoved(child: View?) {
|
|
194
|
+
super.onViewRemoved(child)
|
|
195
|
+
recomposeScope?.invalidate()
|
|
196
|
+
}
|
|
143
197
|
}
|
|
144
198
|
|
|
145
199
|
/**
|
|
@@ -153,6 +207,7 @@ class FunctionalComposableScope(
|
|
|
153
207
|
val composableScope: ComposableScope
|
|
154
208
|
) {
|
|
155
209
|
val appContext = view.appContext
|
|
210
|
+
val globalEventDispatcher = view.globalEventDispatcher
|
|
156
211
|
|
|
157
212
|
@Composable
|
|
158
213
|
fun Child(composableScope: ComposableScope, index: Int) {
|
|
@@ -169,6 +224,11 @@ class FunctionalComposableScope(
|
|
|
169
224
|
view.Children(composableScope)
|
|
170
225
|
}
|
|
171
226
|
|
|
227
|
+
@Composable
|
|
228
|
+
fun Children(composableScope: ComposableScope?, filter: (child: ExpoComposeView<*>) -> Boolean) {
|
|
229
|
+
view.Children(composableScope, filter)
|
|
230
|
+
}
|
|
231
|
+
|
|
172
232
|
inline fun <reified T> EventDispatcher(noinline coalescingKey: CoalescingKey<T>? = null): ViewEventDelegate<T> {
|
|
173
233
|
return view.EventDispatcher<T>(coalescingKey)
|
|
174
234
|
}
|
package/android/src/compose/expo/modules/kotlin/views/ModuleDefinitionBuilderComposeExtension.kt
CHANGED
|
@@ -14,6 +14,11 @@ import kotlin.reflect.full.createInstance
|
|
|
14
14
|
import kotlin.reflect.full.memberProperties
|
|
15
15
|
import kotlin.reflect.typeOf
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The name for the global event dispatcher
|
|
19
|
+
*/
|
|
20
|
+
internal const val GLOBAL_EVENT_NAME = "onGlobalEvent"
|
|
21
|
+
|
|
17
22
|
open class ModuleDefinitionBuilderWithCompose(
|
|
18
23
|
module: Module? = null
|
|
19
24
|
) : InternalModuleDefinitionBuilder(module) {
|
|
@@ -54,7 +59,7 @@ class ComposeViewFunctionDefinitionBuilder<Props : ComposeProps>(
|
|
|
54
59
|
val propsClass: KClass<Props>,
|
|
55
60
|
val viewFunction: @Composable FunctionalComposableScope.(props: Props) -> Unit
|
|
56
61
|
) {
|
|
57
|
-
private var callbacksDefinition: CallbacksDefinition
|
|
62
|
+
private var callbacksDefinition: CallbacksDefinition = CallbacksDefinition(arrayOf(GLOBAL_EVENT_NAME))
|
|
58
63
|
|
|
59
64
|
fun build(): ViewManagerDefinition {
|
|
60
65
|
return ViewManagerDefinition(
|
|
@@ -80,7 +85,9 @@ class ComposeViewFunctionDefinitionBuilder<Props : ComposeProps>(
|
|
|
80
85
|
* Defines prop names that should be treated as callbacks.
|
|
81
86
|
*/
|
|
82
87
|
fun Events(vararg callbacks: String) {
|
|
83
|
-
callbacksDefinition = CallbacksDefinition(
|
|
88
|
+
callbacksDefinition = CallbacksDefinition(
|
|
89
|
+
arrayOf(GLOBAL_EVENT_NAME, *callbacks)
|
|
90
|
+
)
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
/**
|
|
@@ -88,6 +95,8 @@ class ComposeViewFunctionDefinitionBuilder<Props : ComposeProps>(
|
|
|
88
95
|
*/
|
|
89
96
|
@JvmName("EventsWithArray")
|
|
90
97
|
fun Events(callbacks: Array<String>) {
|
|
91
|
-
callbacksDefinition = CallbacksDefinition(
|
|
98
|
+
callbacksDefinition = CallbacksDefinition(
|
|
99
|
+
arrayOf(GLOBAL_EVENT_NAME, *callbacks)
|
|
100
|
+
)
|
|
92
101
|
}
|
|
93
102
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package com.facebook.react.uimanager;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Access the package private property declared inside of [ReactStylesDiffMap]
|
|
7
|
+
*/
|
|
8
|
+
public class ReactStylesDiffMapBackingFieldAccessor {
|
|
9
|
+
static ReadableMap getBackingMap(ReactStylesDiffMap diffMap) {
|
|
10
|
+
return diffMap.internal_backingMap();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,25 +1,7 @@
|
|
|
1
1
|
package com.facebook.react.uimanager
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.bridge.ReadableMap
|
|
4
|
-
import expo.modules.core.interfaces.DoNotStrip
|
|
5
|
-
import java.lang.reflect.Field
|
|
6
4
|
|
|
7
|
-
@get:DoNotStrip
|
|
8
|
-
private val backingMapField: Field by lazy {
|
|
9
|
-
ReactStylesDiffMap::class.java.getDeclaredField("backingMap").apply {
|
|
10
|
-
isAccessible = true
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Access the package private property declared inside of [ReactStylesDiffMap]
|
|
16
|
-
* TODO: We should stop using this field and find a better way to access the backing map:
|
|
17
|
-
* See: https://github.com/facebook/react-native/pull/51386
|
|
18
|
-
*/
|
|
19
5
|
fun ReactStylesDiffMap.getBackingMap(): ReadableMap {
|
|
20
|
-
return
|
|
21
|
-
backingMapField.get(this) as ReadableMap
|
|
22
|
-
} catch (e: ReflectiveOperationException) {
|
|
23
|
-
throw RuntimeException("Unable to access internal_backingMap via reflection", e)
|
|
24
|
-
}
|
|
6
|
+
return ReactStylesDiffMapBackingFieldAccessor.getBackingMap(this)
|
|
25
7
|
}
|
|
@@ -4,68 +4,50 @@ import android.annotation.SuppressLint
|
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import com.facebook.react.ReactApplication
|
|
6
6
|
import com.facebook.react.ReactInstanceEventListener
|
|
7
|
-
import com.facebook.react.ReactInstanceManager
|
|
8
7
|
import com.facebook.react.bridge.ReactContext
|
|
9
8
|
import com.facebook.react.common.LifecycleState
|
|
10
|
-
import expo.modules.BuildConfig
|
|
11
9
|
import expo.modules.apploader.HeadlessAppLoader
|
|
12
10
|
import expo.modules.core.interfaces.Consumer
|
|
13
11
|
import expo.modules.core.interfaces.DoNotStrip
|
|
14
12
|
|
|
15
13
|
private val appRecords: MutableMap<String, ReactContext> = mutableMapOf()
|
|
16
14
|
|
|
17
|
-
class RNHeadlessAppLoader @DoNotStrip constructor(
|
|
15
|
+
class RNHeadlessAppLoader @DoNotStrip constructor() : HeadlessAppLoader {
|
|
18
16
|
|
|
19
17
|
//region HeadlessAppLoader
|
|
20
18
|
|
|
21
|
-
override fun loadApp(
|
|
19
|
+
override fun loadApp(
|
|
20
|
+
context: Context,
|
|
21
|
+
params: HeadlessAppLoader.Params?,
|
|
22
|
+
alreadyRunning: Runnable?,
|
|
23
|
+
callback: Consumer<Boolean>?
|
|
24
|
+
) {
|
|
22
25
|
if (params == null || params.appScopeKey == null) {
|
|
23
26
|
throw IllegalArgumentException("Params must be set with appScopeKey!")
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
if (context.applicationContext is ReactApplication) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
)
|
|
42
|
-
// Ensure that we're starting the react host on the main thread
|
|
43
|
-
android.os.Handler(context.mainLooper).post {
|
|
44
|
-
reactHost.start()
|
|
45
|
-
}
|
|
46
|
-
} else {
|
|
47
|
-
// Old architecture
|
|
48
|
-
val reactInstanceManager = (context.applicationContext as ReactApplication).reactNativeHost.reactInstanceManager
|
|
49
|
-
reactInstanceManager.addReactInstanceEventListener(
|
|
50
|
-
object : ReactInstanceEventListener {
|
|
51
|
-
override fun onReactContextInitialized(context: ReactContext) {
|
|
52
|
-
HeadlessAppLoaderNotifier.notifyAppLoaded(params.appScopeKey)
|
|
53
|
-
reactInstanceManager.removeReactInstanceEventListener(this)
|
|
54
|
-
appRecords[params.appScopeKey] = context
|
|
55
|
-
callback?.apply(true)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
)
|
|
59
|
-
// Ensure that we're starting the react host on the main thread
|
|
60
|
-
android.os.Handler(context.mainLooper).post {
|
|
61
|
-
reactInstanceManager.createReactContextInBackground()
|
|
29
|
+
if (context.applicationContext !is ReactApplication) {
|
|
30
|
+
throw IllegalStateException("Your application must implement ReactApplication")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!appRecords.containsKey(params.appScopeKey)) {
|
|
34
|
+
val reactHost = (context.applicationContext as ReactApplication).reactHost
|
|
35
|
+
?: throw IllegalStateException("Your application does not have a valid reactHost")
|
|
36
|
+
reactHost.addReactInstanceEventListener(
|
|
37
|
+
object : ReactInstanceEventListener {
|
|
38
|
+
override fun onReactContextInitialized(context: ReactContext) {
|
|
39
|
+
reactHost.removeReactInstanceEventListener(this)
|
|
40
|
+
appRecords[params.appScopeKey] = context
|
|
41
|
+
callback?.apply(true)
|
|
62
42
|
}
|
|
63
43
|
}
|
|
64
|
-
|
|
65
|
-
|
|
44
|
+
)
|
|
45
|
+
// Ensure that we're starting the react host on the main thread
|
|
46
|
+
android.os.Handler(context.mainLooper).post {
|
|
47
|
+
reactHost.start()
|
|
66
48
|
}
|
|
67
49
|
} else {
|
|
68
|
-
|
|
50
|
+
alreadyRunning?.run()
|
|
69
51
|
}
|
|
70
52
|
}
|
|
71
53
|
|
|
@@ -73,35 +55,16 @@ class RNHeadlessAppLoader @DoNotStrip constructor(private val context: Context)
|
|
|
73
55
|
override fun invalidateApp(appScopeKey: String?): Boolean {
|
|
74
56
|
return if (appRecords.containsKey(appScopeKey) && appRecords[appScopeKey] != null) {
|
|
75
57
|
val reactContext = appRecords[appScopeKey] ?: return false
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
reactHost.destroy("Closing headless task app", null)
|
|
85
|
-
}
|
|
86
|
-
HeadlessAppLoaderNotifier.notifyAppDestroyed(appScopeKey)
|
|
87
|
-
appRecords.remove(appScopeKey)
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
// Old architecture
|
|
91
|
-
val reactNativeHost = (reactContext.applicationContext as ReactApplication).reactNativeHost
|
|
92
|
-
if (reactNativeHost.hasInstance()) {
|
|
93
|
-
val reactInstanceManager: ReactInstanceManager = reactNativeHost.reactInstanceManager
|
|
94
|
-
android.os.Handler(reactContext.mainLooper).post {
|
|
95
|
-
// Only destroy the `ReactInstanceManager` if it does not bind with an Activity.
|
|
96
|
-
// And The Activity would take over the ownership of `ReactInstanceManager`.
|
|
97
|
-
// This case happens when a user clicks a background task triggered notification immediately.
|
|
98
|
-
if (reactInstanceManager.lifecycleState == LifecycleState.BEFORE_CREATE) {
|
|
99
|
-
reactInstanceManager.destroy()
|
|
100
|
-
}
|
|
101
|
-
HeadlessAppLoaderNotifier.notifyAppDestroyed(appScopeKey)
|
|
102
|
-
appRecords.remove(appScopeKey)
|
|
103
|
-
}
|
|
58
|
+
val reactHost = (reactContext.applicationContext as ReactApplication).reactHost
|
|
59
|
+
?: throw IllegalStateException("Your application does not have a valid reactHost")
|
|
60
|
+
android.os.Handler(reactContext.mainLooper).post {
|
|
61
|
+
// Only destroy the `ReactInstanceManager` if it does not bind with an Activity.
|
|
62
|
+
// And The Activity would take over the ownership of `ReactInstanceManager`.
|
|
63
|
+
// This case happens when a user clicks a background task triggered notification immediately.
|
|
64
|
+
if (reactHost.lifecycleState == LifecycleState.BEFORE_CREATE) {
|
|
65
|
+
reactHost.destroy("Closing headless task app", null)
|
|
104
66
|
}
|
|
67
|
+
appRecords.remove(appScopeKey)
|
|
105
68
|
}
|
|
106
69
|
true
|
|
107
70
|
} else {
|
|
@@ -110,17 +73,9 @@ class RNHeadlessAppLoader @DoNotStrip constructor(private val context: Context)
|
|
|
110
73
|
}
|
|
111
74
|
|
|
112
75
|
override fun isRunning(appScopeKey: String?): Boolean {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// means that we've already called start on the reactHost
|
|
117
|
-
return true
|
|
118
|
-
} else {
|
|
119
|
-
// Old architecture
|
|
120
|
-
val reactNativeHost = (reactContext.applicationContext as ReactApplication).reactNativeHost
|
|
121
|
-
return reactNativeHost.reactInstanceManager.hasStartedCreatingInitialContext()
|
|
122
|
-
}
|
|
76
|
+
// New architecture - We can return true since the fact that we have a reactContext
|
|
77
|
+
// means that we've already called start on the reactHost
|
|
78
|
+
return appRecords[appScopeKey] != null
|
|
123
79
|
}
|
|
124
|
-
|
|
125
80
|
//endregion HeadlessAppLoader
|
|
126
81
|
}
|
package/android/src/main/java/expo/modules/adapters/react/services/UIManagerModuleWrapper.java
CHANGED
|
@@ -2,32 +2,20 @@ package expo.modules.adapters.react.services;
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity;
|
|
4
4
|
import android.content.Intent;
|
|
5
|
-
import android.util.Log;
|
|
6
|
-
import android.view.View;
|
|
7
5
|
|
|
8
6
|
import com.facebook.react.bridge.ReactContext;
|
|
9
7
|
import com.facebook.react.common.annotations.FrameworkAPI;
|
|
10
8
|
import com.facebook.react.common.annotations.UnstableReactNativeAPI;
|
|
11
|
-
import com.facebook.react.fabric.FabricUIManager;
|
|
12
|
-
import com.facebook.react.fabric.interop.UIBlockViewResolver;
|
|
13
9
|
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl;
|
|
14
|
-
import com.facebook.react.uimanager.IllegalViewOperationException;
|
|
15
|
-
import com.facebook.react.uimanager.NativeViewHierarchyManager;
|
|
16
|
-
import com.facebook.react.uimanager.UIManagerHelper;
|
|
17
|
-
import com.facebook.react.uimanager.UIManagerModule;
|
|
18
|
-
import com.facebook.react.uimanager.common.UIManagerType;
|
|
19
10
|
|
|
20
11
|
import java.lang.ref.WeakReference;
|
|
21
12
|
import java.util.ArrayList;
|
|
22
13
|
import java.util.Arrays;
|
|
23
14
|
import java.util.List;
|
|
24
15
|
import java.util.Map;
|
|
25
|
-
import java.util.Objects;
|
|
26
16
|
import java.util.WeakHashMap;
|
|
27
17
|
|
|
28
|
-
import androidx.annotation.Nullable;
|
|
29
18
|
import androidx.annotation.OptIn;
|
|
30
|
-
import expo.modules.BuildConfig;
|
|
31
19
|
import expo.modules.core.interfaces.ActivityEventListener;
|
|
32
20
|
import expo.modules.core.interfaces.ActivityProvider;
|
|
33
21
|
import expo.modules.core.interfaces.InternalModule;
|
|
@@ -62,99 +50,6 @@ public class UIManagerModuleWrapper implements
|
|
|
62
50
|
);
|
|
63
51
|
}
|
|
64
52
|
|
|
65
|
-
private void addToUIManager(final UIBlockInterface block) {
|
|
66
|
-
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
|
67
|
-
com.facebook.react.bridge.UIManager uiManager = UIManagerHelper.getUIManager(getContext(), UIManagerType.FABRIC);
|
|
68
|
-
Objects.requireNonNull(((FabricUIManager) uiManager)).addUIBlock(block);
|
|
69
|
-
} else {
|
|
70
|
-
UIManagerModule uiManager = getContext().getNativeModule(UIManagerModule.class);
|
|
71
|
-
Objects.requireNonNull(uiManager).addUIBlock(block);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
@Override
|
|
76
|
-
@SuppressWarnings("deprecation")
|
|
77
|
-
public <T> void addUIBlock(final int tag, final UIBlock<T> block, final Class<T> tClass) {
|
|
78
|
-
UIBlockInterface uiBlock = new UIBlockInterface() {
|
|
79
|
-
@Override
|
|
80
|
-
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
|
|
81
|
-
executeImpl(nativeViewHierarchyManager, null);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
@Override
|
|
85
|
-
public void execute(UIBlockViewResolver uiBlockViewResolver) {
|
|
86
|
-
executeImpl(null, uiBlockViewResolver);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private void executeImpl(NativeViewHierarchyManager nativeViewHierarchyManager, UIBlockViewResolver uiBlockViewResolver) {
|
|
90
|
-
View view = nativeViewHierarchyManager.resolveView(tag);
|
|
91
|
-
if (view == null) {
|
|
92
|
-
block.reject(new IllegalArgumentException("Expected view for this tag not to be null."));
|
|
93
|
-
} else {
|
|
94
|
-
try {
|
|
95
|
-
if (tClass.isInstance(view)) {
|
|
96
|
-
block.resolve(tClass.cast(view));
|
|
97
|
-
} else {
|
|
98
|
-
block.reject(new IllegalStateException(
|
|
99
|
-
"Expected view to be of " + tClass + "; found " + view.getClass() + " instead"));
|
|
100
|
-
}
|
|
101
|
-
} catch (Exception e) {
|
|
102
|
-
block.reject(e);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
addToUIManager(uiBlock);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
@Override
|
|
112
|
-
@SuppressWarnings("deprecation")
|
|
113
|
-
public void addUIBlock(final GroupUIBlock block) {
|
|
114
|
-
UIBlockInterface uiBlock = new UIBlockInterface() {
|
|
115
|
-
@Override
|
|
116
|
-
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
|
|
117
|
-
executeImpl(nativeViewHierarchyManager, null);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
@Override
|
|
121
|
-
public void execute(UIBlockViewResolver uiBlockViewResolver) {
|
|
122
|
-
executeImpl(null, uiBlockViewResolver);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private void executeImpl(NativeViewHierarchyManager nativeViewHierarchyManager, UIBlockViewResolver uiBlockViewResolver) {
|
|
126
|
-
block.execute(new ViewHolder() {
|
|
127
|
-
@Override
|
|
128
|
-
public View get(Object key) {
|
|
129
|
-
if (key instanceof Number) {
|
|
130
|
-
try {
|
|
131
|
-
return nativeViewHierarchyManager.resolveView(((Number) key).intValue());
|
|
132
|
-
} catch (IllegalViewOperationException e) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
} else {
|
|
136
|
-
Log.w("E_INVALID_TAG", "Provided tag is of class " + key.getClass() + " whereas React expects tags to be integers. Are you sure you're providing proper argument to addUIBlock?");
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
addToUIManager(uiBlock);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
@Nullable
|
|
148
|
-
@Override
|
|
149
|
-
@SuppressWarnings("deprecation")
|
|
150
|
-
public View resolveView(int viewTag) {
|
|
151
|
-
final com.facebook.react.bridge.UIManager uiManager = UIManagerHelper.getUIManagerForReactTag(getContext(), viewTag);
|
|
152
|
-
if (uiManager == null) {
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
return uiManager.resolveView(viewTag);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
53
|
@Override
|
|
159
54
|
public void runOnUiQueueThread(Runnable runnable) {
|
|
160
55
|
if (getContext().isOnUiQueueThread()) {
|
|
@@ -173,15 +68,6 @@ public class UIManagerModuleWrapper implements
|
|
|
173
68
|
}
|
|
174
69
|
}
|
|
175
70
|
|
|
176
|
-
public void runOnNativeModulesQueueThread(Runnable runnable) {
|
|
177
|
-
if (mReactContext.isOnNativeModulesQueueThread()) {
|
|
178
|
-
runnable.run();
|
|
179
|
-
} else {
|
|
180
|
-
mReactContext.runOnNativeModulesQueueThread(runnable);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
71
|
@Override
|
|
186
72
|
public void registerLifecycleEventListener(final LifecycleEventListener listener) {
|
|
187
73
|
final WeakReference<LifecycleEventListener> weakListener = new WeakReference<>(listener);
|
|
@@ -279,8 +165,3 @@ public class UIManagerModuleWrapper implements
|
|
|
279
165
|
return getContext().getCurrentActivity();
|
|
280
166
|
}
|
|
281
167
|
}
|
|
282
|
-
|
|
283
|
-
@OptIn(markerClass = UnstableReactNativeAPI.class)
|
|
284
|
-
@SuppressWarnings("deprecation")
|
|
285
|
-
interface UIBlockInterface extends com.facebook.react.uimanager.UIBlock, com.facebook.react.fabric.interop.UIBlock {
|
|
286
|
-
}
|
|
@@ -33,8 +33,8 @@ object AppLoaderProvider {
|
|
|
33
33
|
|
|
34
34
|
loaderClass = Class.forName(loaderClassName) as Class<out HeadlessAppLoader>
|
|
35
35
|
loaders[name] = loaderClass
|
|
36
|
-
.getDeclaredConstructor(
|
|
37
|
-
.newInstance(
|
|
36
|
+
.getDeclaredConstructor()
|
|
37
|
+
.newInstance() as HeadlessAppLoader
|
|
38
38
|
} catch (e: PackageManager.NameNotFoundException) {
|
|
39
39
|
throw IllegalStateException("Unable to instantiate AppLoader!", e)
|
|
40
40
|
}
|
|
@@ -5,19 +5,7 @@ import android.content.Context;
|
|
|
5
5
|
import expo.modules.core.interfaces.Consumer;
|
|
6
6
|
|
|
7
7
|
public interface HeadlessAppLoader {
|
|
8
|
-
|
|
9
|
-
class AppConfigurationError extends Exception {
|
|
10
|
-
|
|
11
|
-
public AppConfigurationError(String message) {
|
|
12
|
-
super(message);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
public AppConfigurationError(String message, Exception cause) {
|
|
16
|
-
super(message, cause);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
void loadApp(Context context, Params params, Runnable alreadyRunning, Consumer<Boolean> callback) throws AppConfigurationError;
|
|
8
|
+
void loadApp(Context context, Params params, Runnable alreadyRunning, Consumer<Boolean> callback);
|
|
21
9
|
|
|
22
10
|
boolean invalidateApp(String appScopeKey);
|
|
23
11
|
|
|
@@ -39,7 +27,5 @@ public interface HeadlessAppLoader {
|
|
|
39
27
|
public String getAppUrl() {
|
|
40
28
|
return appUrl;
|
|
41
29
|
}
|
|
42
|
-
|
|
43
30
|
}
|
|
44
|
-
|
|
45
31
|
}
|