@granite-js/screen 1.0.0
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 +7 -0
- package/GraniteScreen.podspec +25 -0
- package/LICENSE +202 -0
- package/android/CMakeLists.txt +62 -0
- package/android/build.gradle +63 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/BundleEvaluator.cpp +27 -0
- package/android/src/main/cpp/BundleEvaluator.h +17 -0
- package/android/src/main/cpp/onLoad.cpp +6 -0
- package/android/src/main/kotlin/run/granite/BundleEvaluator.kt +50 -0
- package/android/src/main/kotlin/run/granite/BundleLoader.kt +40 -0
- package/android/src/main/kotlin/run/granite/DefaultBundleLoader.kt +20 -0
- package/android/src/main/kotlin/run/granite/DefaultErrorView.kt +58 -0
- package/android/src/main/kotlin/run/granite/DefaultLoadingView.kt +51 -0
- package/android/src/main/kotlin/run/granite/GraniteReactDelegate.kt +76 -0
- package/android/src/main/kotlin/run/granite/GraniteReactDelegateImpl.kt +448 -0
- package/android/src/main/kotlin/run/granite/GraniteReactHost.kt +113 -0
- package/android/src/main/kotlin/run/granite/ReactHostFactory.kt +106 -0
- package/gradle-plugin/LICENSE +201 -0
- package/gradle-plugin/README.md +578 -0
- package/gradle-plugin/build.gradle.kts +97 -0
- package/gradle-plugin/gradle/libs.versions.toml +17 -0
- package/gradle-plugin/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle-plugin/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/gradle-plugin/gradle.properties +12 -0
- package/gradle-plugin/gradlew +248 -0
- package/gradle-plugin/gradlew.bat +93 -0
- package/gradle-plugin/settings.gradle.kts +1 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/GraniteExtension.kt +225 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/GranitePlugin.kt +784 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/GraniteRootExtension.kt +107 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/GraniteRootProjectPlugin.kt +290 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/BuildConfigConfigurator.kt +69 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/DependencyConfigurator.kt +232 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/DependencyCoordinates.kt +29 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/DevServerResourceConfigurator.kt +101 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/JniPackagingConfigurator.kt +160 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/NdkConfigurator.kt +135 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/RepositoryConfigurator.kt +148 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/config/ResourceConfigurator.kt +56 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/generators/CMakeGenerator.kt +105 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/generators/CppAutolinkingGenerator.kt +152 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/generators/EntryPointGenerator.kt +100 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/AndroidDependencyConfig.kt +23 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/AutolinkingConfig.kt +89 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/CMakeEntry.kt +47 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/models/NativeModule.kt +177 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/AssetPackagingTask.kt +194 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/AutolinkingTask.kt +431 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/BundleTask.kt +275 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/CodegenArtifactsTask.kt +218 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/tasks/CodegenSchemaTask.kt +186 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/AutolinkingParser.kt +128 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/ConflictDetector.kt +121 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/JdkValidator.kt +73 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/NodeExecutableFinder.kt +43 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/ReactNativeVersionReader.kt +329 -0
- package/gradle-plugin/src/main/kotlin/run/granite/gradle/utils/TaskDependencyValidator.kt +198 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/GraniteExtensionTest.kt +191 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/GranitePluginTest.kt +156 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/GraniteRootProjectPluginTest.kt +87 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/BuildConfigConfiguratorTest.kt +115 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/DependencyConfiguratorTest.kt +338 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/DevServerResourceConfiguratorTest.kt +205 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/config/ResourceConfiguratorTest.kt +131 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/fixtures/NativeModuleFixtures.kt +67 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/generators/CMakeGeneratorTest.kt +71 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/generators/CppAutolinkingGeneratorTest.kt +344 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/generators/EntryPointGeneratorTest.kt +40 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/models/AutolinkingConfigTest.kt +350 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/models/CMakeEntryTest.kt +200 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/models/NativeModuleTest.kt +562 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/AssetPackagingTaskTest.kt +318 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/AutolinkingTaskTest.kt +89 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/BundleTaskTest.kt +68 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/tasks/CodegenTasksTest.kt +410 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/AutolinkingParserTest.kt +335 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/ConflictDetectorTest.kt +75 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/JdkValidatorTest.kt +88 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/ReactNativeVersionReaderTest.kt +585 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/TaskDependencyValidatorTest.kt +123 -0
- package/gradle-plugin/src/test/kotlin/run/granite/gradle/utils/TaskTestUtils.kt +88 -0
- package/gradle-plugin/src/test/resources/fixtures/sample-rn-config.json +45 -0
- package/ios/BundleLoader/BundleEvaluator.h +16 -0
- package/ios/BundleLoader/BundleEvaluator.mm +76 -0
- package/ios/BundleLoader/BundleLoadable.swift +91 -0
- package/ios/GraniteBundleLoaderTypes.swift +7 -0
- package/ios/GraniteScreen.h +12 -0
- package/ios/ReactNativeHosting/DefaultViews.swift +138 -0
- package/ios/ReactNativeHosting/GraniteDefaultModuleProvider.h +24 -0
- package/ios/ReactNativeHosting/GraniteDefaultModuleProvider.mm +22 -0
- package/ios/ReactNativeHosting/GraniteHostingHelper.swift +103 -0
- package/ios/ReactNativeHosting/GraniteNativeFactory.swift +35 -0
- package/ios/ReactNativeHosting/GraniteNativeFactoryDelegateImpl.swift +30 -0
- package/ios/ReactNativeHosting/GraniteNativeFactoryImpl.swift +24 -0
- package/ios/ReactNativeHosting/GraniteReactHost.swift +39 -0
- package/ios/ReactNativeHosting/GraniteScreen-Bridging-Header.h +12 -0
- package/package.json +59 -0
- package/react-native.config.js +8 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package run.granite
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import android.view.View
|
|
5
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
6
|
+
import com.facebook.react.ReactHost
|
|
7
|
+
import com.facebook.react.ReactPackage
|
|
8
|
+
import com.facebook.react.runtime.ReactSurfaceView
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Delegate interface for Granite functionality. This allows integration with existing BaseActivity
|
|
12
|
+
* implementations.
|
|
13
|
+
*/
|
|
14
|
+
interface GraniteReactDelegate {
|
|
15
|
+
// Lifecycle methods
|
|
16
|
+
fun onCreate(
|
|
17
|
+
activity: AppCompatActivity,
|
|
18
|
+
savedInstanceState: Bundle?,
|
|
19
|
+
initialProps: Bundle,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
fun onResume(activity: AppCompatActivity)
|
|
23
|
+
|
|
24
|
+
fun onPause(activity: AppCompatActivity)
|
|
25
|
+
|
|
26
|
+
fun onDestroy(activity: AppCompatActivity)
|
|
27
|
+
|
|
28
|
+
// Configuration methods - these should be provided by the activity
|
|
29
|
+
fun setReactPackagesProvider(provider: () -> List<ReactPackage>)
|
|
30
|
+
|
|
31
|
+
fun setBundleLoaderProvider(provider: () -> BundleLoader)
|
|
32
|
+
|
|
33
|
+
// View creation methods - optional customization
|
|
34
|
+
fun setLoadingViewProvider(provider: (AppCompatActivity) -> View)
|
|
35
|
+
|
|
36
|
+
fun setErrorViewProvider(provider: (AppCompatActivity, Throwable) -> View)
|
|
37
|
+
|
|
38
|
+
fun setReactContainerProvider(provider: (AppCompatActivity) -> android.view.ViewGroup?)
|
|
39
|
+
|
|
40
|
+
// Consumer methods - for advanced view control
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set a consumer that will be called when loading starts.
|
|
44
|
+
* If not set, default loading view behavior is used.
|
|
45
|
+
*/
|
|
46
|
+
fun setLoadingViewConsumer(consumer: () -> Unit)
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Set a consumer that will receive the ReactSurface view when ready.
|
|
50
|
+
* Activity is responsible for adding this view to its layout.
|
|
51
|
+
* If not set, delegate will call setContentView() directly (fallback).
|
|
52
|
+
*/
|
|
53
|
+
fun setSurfaceViewConsumer(consumer: (ReactSurfaceView) -> Unit)
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Set a consumer that will be called when bundle loading fails.
|
|
57
|
+
* If not set, default error view behavior is used.
|
|
58
|
+
*/
|
|
59
|
+
fun setErrorViewConsumer(consumer: (Throwable) -> Unit)
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Start the ReactSurface. Call this after setting up the surface view in your layout.
|
|
63
|
+
* This triggers surface.start() and applies pending lifecycle states.
|
|
64
|
+
*/
|
|
65
|
+
fun startReactSurface()
|
|
66
|
+
|
|
67
|
+
// Access methods
|
|
68
|
+
fun getReactHost(): ReactHost?
|
|
69
|
+
|
|
70
|
+
fun getBundleLoader(): BundleLoader?
|
|
71
|
+
|
|
72
|
+
fun isReady(): Boolean
|
|
73
|
+
|
|
74
|
+
// Surface management
|
|
75
|
+
fun getContentView(): View?
|
|
76
|
+
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
package run.granite
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.widget.FrameLayout
|
|
7
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
8
|
+
import androidx.core.view.ViewCompat
|
|
9
|
+
import androidx.core.view.WindowInsetsCompat
|
|
10
|
+
import androidx.lifecycle.lifecycleScope
|
|
11
|
+
import com.facebook.react.ReactHost
|
|
12
|
+
import com.facebook.react.ReactInstanceEventListener
|
|
13
|
+
import com.facebook.react.ReactPackage
|
|
14
|
+
import com.facebook.react.bridge.ReactContext
|
|
15
|
+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
|
16
|
+
import com.facebook.react.fabric.ComponentFactory
|
|
17
|
+
import com.facebook.react.interfaces.fabric.ReactSurface
|
|
18
|
+
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
|
|
19
|
+
import com.facebook.react.runtime.ReactSurfaceView
|
|
20
|
+
import kotlinx.coroutines.launch
|
|
21
|
+
import java.lang.ref.WeakReference
|
|
22
|
+
|
|
23
|
+
/** Default implementation of GraniteReactDelegate. Manages ReactHost, ReactSurface */
|
|
24
|
+
@OptIn(UnstableReactNativeAPI::class)
|
|
25
|
+
class GraniteReactDelegateImpl : GraniteReactDelegate {
|
|
26
|
+
// Lifecycle state enum to track pending state when ReactHost is null
|
|
27
|
+
private enum class LifecycleState {
|
|
28
|
+
BEFORE_CREATE,
|
|
29
|
+
CREATED,
|
|
30
|
+
RESUMED,
|
|
31
|
+
PAUSED,
|
|
32
|
+
DESTROYED,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private var reactHost: ReactHost? = null
|
|
36
|
+
private var reactSurface: ReactSurface? = null
|
|
37
|
+
private var currentBundleSource: BundleSource? = null
|
|
38
|
+
private var currentBundleLoader: BundleLoader? = null
|
|
39
|
+
private var contentView: View? = null
|
|
40
|
+
private var componentFactory: ComponentFactory? = null
|
|
41
|
+
private var reactInstanceEventListener: ReactInstanceEventListener? = null
|
|
42
|
+
|
|
43
|
+
// Track pending lifecycle state for when ReactHost is created asynchronously
|
|
44
|
+
private var pendingLifecycleState: LifecycleState = LifecycleState.BEFORE_CREATE
|
|
45
|
+
|
|
46
|
+
// Use WeakReference to prevent Activity memory leak
|
|
47
|
+
private var pendingActivityRef: WeakReference<AppCompatActivity>? = null
|
|
48
|
+
private val pendingActivity: AppCompatActivity?
|
|
49
|
+
get() = pendingActivityRef?.get()
|
|
50
|
+
|
|
51
|
+
// Provider functions - to be set by the activity
|
|
52
|
+
private var reactPackagesProvider: (() -> List<ReactPackage>)? = null
|
|
53
|
+
private var bundleLoaderProvider: (() -> BundleLoader)? = null
|
|
54
|
+
private var loadingViewProvider: ((AppCompatActivity) -> View)? = null
|
|
55
|
+
private var errorViewProvider: ((AppCompatActivity, Throwable) -> View)? = null
|
|
56
|
+
private var reactContainerProvider: ((AppCompatActivity) -> android.view.ViewGroup?)? = null
|
|
57
|
+
|
|
58
|
+
// Consumer functions - for advanced view control
|
|
59
|
+
private var loadingViewConsumer: (() -> Unit)? = null
|
|
60
|
+
private var surfaceViewConsumer: ((ReactSurfaceView) -> Unit)? = null
|
|
61
|
+
private var errorViewConsumer: ((Throwable) -> Unit)? = null
|
|
62
|
+
|
|
63
|
+
override fun onCreate(
|
|
64
|
+
activity: AppCompatActivity,
|
|
65
|
+
savedInstanceState: Bundle?,
|
|
66
|
+
initialProps: Bundle,
|
|
67
|
+
) {
|
|
68
|
+
// Track lifecycle state
|
|
69
|
+
pendingLifecycleState = LifecycleState.CREATED
|
|
70
|
+
pendingActivityRef = WeakReference(activity)
|
|
71
|
+
|
|
72
|
+
// Get packages
|
|
73
|
+
val packages = reactPackagesProvider?.invoke() ?: emptyList()
|
|
74
|
+
|
|
75
|
+
Log.d(TAG, "Using ${packages.size} React packages")
|
|
76
|
+
packages.forEach { pkg ->
|
|
77
|
+
Log.d(TAG, "- ${pkg.javaClass.simpleName}")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (BuildConfig.DEBUG) {
|
|
81
|
+
Log.d(TAG, "Running in debug mode")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Require BundleLoader
|
|
85
|
+
val bundleLoader =
|
|
86
|
+
bundleLoaderProvider?.invoke()
|
|
87
|
+
?: throw IllegalStateException("BundleLoader provider not set")
|
|
88
|
+
currentBundleLoader = bundleLoader
|
|
89
|
+
|
|
90
|
+
// Show loading view and start bundle loading
|
|
91
|
+
// Use consumer if set, otherwise use provider/default
|
|
92
|
+
loadingViewConsumer?.invoke()
|
|
93
|
+
?: run {
|
|
94
|
+
val loadingView =
|
|
95
|
+
loadingViewProvider?.invoke(activity) ?: DefaultLoadingView(activity)
|
|
96
|
+
contentView = loadingView
|
|
97
|
+
activity.setContentView(loadingView)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
loadBundleWithLoader(activity, bundleLoader, initialProps)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
override fun onResume(activity: AppCompatActivity) {
|
|
104
|
+
// Track pending lifecycle state
|
|
105
|
+
pendingLifecycleState = LifecycleState.RESUMED
|
|
106
|
+
pendingActivityRef = WeakReference(activity)
|
|
107
|
+
|
|
108
|
+
// Call onHostResume if ReactHost is ready, otherwise it will be called in setupReactHost
|
|
109
|
+
val backBtnHandler = activity as? DefaultHardwareBackBtnHandler
|
|
110
|
+
?: throw IllegalStateException(
|
|
111
|
+
"Activity ${activity.javaClass.simpleName} must implement DefaultHardwareBackBtnHandler",
|
|
112
|
+
)
|
|
113
|
+
reactHost?.onHostResume(activity, backBtnHandler)
|
|
114
|
+
?: Log.w(
|
|
115
|
+
TAG,
|
|
116
|
+
"ReactHost not ready yet, will call onHostResume after creation",
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
override fun onPause(activity: AppCompatActivity) {
|
|
121
|
+
// Track pending lifecycle state
|
|
122
|
+
pendingLifecycleState = LifecycleState.PAUSED
|
|
123
|
+
pendingActivityRef = WeakReference(activity)
|
|
124
|
+
|
|
125
|
+
// Call onHostPause only if ReactHost exists
|
|
126
|
+
reactHost?.onHostPause(activity)
|
|
127
|
+
?: Log.w(TAG, "ReactHost not ready, skipping onHostPause")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override fun onDestroy(activity: AppCompatActivity) {
|
|
131
|
+
// Track pending lifecycle state
|
|
132
|
+
pendingLifecycleState = LifecycleState.DESTROYED
|
|
133
|
+
pendingActivityRef = null
|
|
134
|
+
|
|
135
|
+
// Clean up ReactSurface
|
|
136
|
+
// stop() is async (TaskInterface<Void>), but clear() and detach() can be called concurrently:
|
|
137
|
+
// - clear() removes view children via UiThreadUtil.runOnUiThread (thread-safe)
|
|
138
|
+
// - detach() releases host reference via AtomicReference.set(null) (thread-safe)
|
|
139
|
+
reactSurface?.let { surface ->
|
|
140
|
+
surface.stop()
|
|
141
|
+
surface.clear()
|
|
142
|
+
surface.detach()
|
|
143
|
+
}
|
|
144
|
+
reactSurface = null
|
|
145
|
+
|
|
146
|
+
// Remove ReactInstanceEventListener
|
|
147
|
+
reactInstanceEventListener?.let { listener ->
|
|
148
|
+
reactHost?.removeReactInstanceEventListener(listener)
|
|
149
|
+
}
|
|
150
|
+
reactInstanceEventListener = null
|
|
151
|
+
|
|
152
|
+
// Clean up ReactHost
|
|
153
|
+
// invalidate() internally runs destroy() asynchronously on bgExecutor.
|
|
154
|
+
// ReactHostImpl instance is kept alive by its own internal threading,
|
|
155
|
+
// so nulling the delegate's reactHost field won't interrupt the async destroy.
|
|
156
|
+
reactHost?.let { host ->
|
|
157
|
+
host.onHostDestroy(activity)
|
|
158
|
+
host.invalidate()
|
|
159
|
+
}
|
|
160
|
+
?: Log.w(TAG, "ReactHost not ready, skipping onHostDestroy")
|
|
161
|
+
reactHost = null
|
|
162
|
+
|
|
163
|
+
// Clean up internal references
|
|
164
|
+
contentView = null
|
|
165
|
+
currentBundleSource = null
|
|
166
|
+
currentBundleLoader = null
|
|
167
|
+
componentFactory = null
|
|
168
|
+
|
|
169
|
+
// Clean up Provider/Consumer lambdas (may capture Activity via closure)
|
|
170
|
+
reactPackagesProvider = null
|
|
171
|
+
bundleLoaderProvider = null
|
|
172
|
+
loadingViewProvider = null
|
|
173
|
+
errorViewProvider = null
|
|
174
|
+
reactContainerProvider = null
|
|
175
|
+
loadingViewConsumer = null
|
|
176
|
+
surfaceViewConsumer = null
|
|
177
|
+
errorViewConsumer = null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
override fun setReactPackagesProvider(provider: () -> List<ReactPackage>) {
|
|
181
|
+
this.reactPackagesProvider = provider
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
override fun setBundleLoaderProvider(provider: () -> BundleLoader) {
|
|
185
|
+
this.bundleLoaderProvider = provider
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
override fun setLoadingViewProvider(provider: (AppCompatActivity) -> View) {
|
|
189
|
+
this.loadingViewProvider = provider
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
override fun setErrorViewProvider(provider: (AppCompatActivity, Throwable) -> View) {
|
|
193
|
+
this.errorViewProvider = provider
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
override fun setReactContainerProvider(provider: (AppCompatActivity) -> android.view.ViewGroup?) {
|
|
197
|
+
this.reactContainerProvider = provider
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
override fun setLoadingViewConsumer(consumer: () -> Unit) {
|
|
201
|
+
this.loadingViewConsumer = consumer
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
override fun setSurfaceViewConsumer(consumer: (ReactSurfaceView) -> Unit) {
|
|
205
|
+
this.surfaceViewConsumer = consumer
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
override fun setErrorViewConsumer(consumer: (Throwable) -> Unit) {
|
|
209
|
+
this.errorViewConsumer = consumer
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
override fun getReactHost(): ReactHost? = reactHost
|
|
213
|
+
|
|
214
|
+
override fun getBundleLoader(): BundleLoader? = currentBundleLoader
|
|
215
|
+
|
|
216
|
+
override fun isReady(): Boolean = reactHost != null && reactSurface != null
|
|
217
|
+
|
|
218
|
+
override fun getContentView(): View? = contentView
|
|
219
|
+
|
|
220
|
+
private fun getMainComponentName(): String =
|
|
221
|
+
currentBundleSource?.componentName
|
|
222
|
+
?: throw IllegalStateException(
|
|
223
|
+
"mainComponentName is missing. Use a BundleLoader that provides componentName.",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
// Default (no-loader) ReactHost creation removed; BundleLoader is mandatory.
|
|
227
|
+
|
|
228
|
+
private fun setupReactHost(
|
|
229
|
+
activity: AppCompatActivity,
|
|
230
|
+
initialProps: Bundle,
|
|
231
|
+
) {
|
|
232
|
+
reactHost?.let { host ->
|
|
233
|
+
// Get component name using the method
|
|
234
|
+
val componentName = getMainComponentName()
|
|
235
|
+
|
|
236
|
+
Log.d(TAG, "Using component name: $componentName")
|
|
237
|
+
|
|
238
|
+
// Create surface
|
|
239
|
+
val surface = host.createSurface(activity, componentName, initialProps)
|
|
240
|
+
reactSurface = surface
|
|
241
|
+
contentView = surface.view
|
|
242
|
+
|
|
243
|
+
(surface.view as? ReactSurfaceView)?.let { surfaceView ->
|
|
244
|
+
surfaceViewConsumer?.invoke(surfaceView)
|
|
245
|
+
?: run {
|
|
246
|
+
// Fallback: Use default behavior if consumer not set
|
|
247
|
+
surfaceView.id = View.NO_ID
|
|
248
|
+
activity.setContentView(surfaceView)
|
|
249
|
+
setupWindowInsets(surfaceView)
|
|
250
|
+
|
|
251
|
+
// Start surface immediately if consumer not used
|
|
252
|
+
startReactSurface()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private fun setupWindowInsets(surfaceView: View) {
|
|
259
|
+
val insetsType: Int =
|
|
260
|
+
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
|
|
261
|
+
|
|
262
|
+
val windowInsetsListener = { view: View, windowInsets: WindowInsetsCompat ->
|
|
263
|
+
val insets = windowInsets.getInsets(insetsType)
|
|
264
|
+
|
|
265
|
+
(view.layoutParams as? FrameLayout.LayoutParams)?.apply {
|
|
266
|
+
setMargins(insets.left, insets.top, insets.right, insets.bottom)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
WindowInsetsCompat.CONSUMED
|
|
270
|
+
}
|
|
271
|
+
ViewCompat.setOnApplyWindowInsetsListener(surfaceView, windowInsetsListener)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
override fun startReactSurface() {
|
|
275
|
+
reactHost?.let { host ->
|
|
276
|
+
reactSurface?.let { surface ->
|
|
277
|
+
Log.d(TAG, "Starting ReactSurface")
|
|
278
|
+
|
|
279
|
+
// Add ReactInstanceEventListener
|
|
280
|
+
val listener = object : ReactInstanceEventListener {
|
|
281
|
+
override fun onReactContextInitialized(context: ReactContext) {
|
|
282
|
+
Log.d(TAG, "Granite onReactContextInitialized called")
|
|
283
|
+
Log.d(
|
|
284
|
+
TAG,
|
|
285
|
+
"Granite onReactContextInitialized on currentActivity = ${context.currentActivity}",
|
|
286
|
+
)
|
|
287
|
+
// Call the host's onReactContextInitialized method
|
|
288
|
+
pendingActivity?.let { activity ->
|
|
289
|
+
if (activity is GraniteReactHost) {
|
|
290
|
+
Log.d(
|
|
291
|
+
TAG,
|
|
292
|
+
"Granite activity.onReactContextInitialized called",
|
|
293
|
+
)
|
|
294
|
+
activity.onReactContextInitialized(context)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
reactInstanceEventListener = listener
|
|
300
|
+
host.addReactInstanceEventListener(listener)
|
|
301
|
+
|
|
302
|
+
// Start the surface
|
|
303
|
+
Log.d(TAG, "About to call surface.start()")
|
|
304
|
+
Log.d(TAG, "surface = $surface")
|
|
305
|
+
Log.d(
|
|
306
|
+
TAG,
|
|
307
|
+
"If AppRegistry.registerComponent was not called, this will fail!",
|
|
308
|
+
)
|
|
309
|
+
surface.start()
|
|
310
|
+
Log.d(TAG, "surface.start() completed")
|
|
311
|
+
|
|
312
|
+
// Apply pending lifecycle state
|
|
313
|
+
Log.d(
|
|
314
|
+
TAG,
|
|
315
|
+
"Applying pending lifecycle state: $pendingLifecycleState",
|
|
316
|
+
)
|
|
317
|
+
when (pendingLifecycleState) {
|
|
318
|
+
LifecycleState.RESUMED -> {
|
|
319
|
+
// Activity is resumed, call onHostResume
|
|
320
|
+
pendingActivity?.let { act ->
|
|
321
|
+
val handler = act as? DefaultHardwareBackBtnHandler
|
|
322
|
+
?: throw IllegalStateException(
|
|
323
|
+
"Activity ${act.javaClass.simpleName} must implement DefaultHardwareBackBtnHandler",
|
|
324
|
+
)
|
|
325
|
+
host.onHostResume(act, handler)
|
|
326
|
+
Log.d(
|
|
327
|
+
TAG,
|
|
328
|
+
"Called onHostResume with activity: $act",
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
LifecycleState.PAUSED -> {
|
|
333
|
+
// Activity is paused, call onHostPause
|
|
334
|
+
pendingActivity?.let { act ->
|
|
335
|
+
host.onHostPause(act)
|
|
336
|
+
Log.d(
|
|
337
|
+
TAG,
|
|
338
|
+
"Called onHostPause with activity: $act",
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
LifecycleState.DESTROYED -> {
|
|
343
|
+
// Activity is being destroyed, clean up immediately
|
|
344
|
+
surface.stop()
|
|
345
|
+
pendingActivity?.let { act -> host.onHostDestroy(act) }
|
|
346
|
+
host.invalidate()
|
|
347
|
+
Log.d(TAG, "Activity destroyed, cleaned up ReactHost")
|
|
348
|
+
}
|
|
349
|
+
else -> {
|
|
350
|
+
Log.d(TAG, "No lifecycle state to apply")
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
?: Log.w(TAG, "ReactSurface is null, cannot start")
|
|
355
|
+
}
|
|
356
|
+
?: Log.w(TAG, "ReactHost is null, cannot start")
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private fun loadBundleWithLoader(
|
|
360
|
+
activity: AppCompatActivity,
|
|
361
|
+
bundleLoader: BundleLoader,
|
|
362
|
+
initialProps: Bundle,
|
|
363
|
+
) {
|
|
364
|
+
activity.lifecycleScope.launch {
|
|
365
|
+
try {
|
|
366
|
+
Log.d(TAG, "loadBundleWithLoader started")
|
|
367
|
+
Log.d(
|
|
368
|
+
TAG,
|
|
369
|
+
"bundleLoader type = ${bundleLoader.javaClass.simpleName}",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
// Load bundle (suspend, may run on IO dispatcher)
|
|
373
|
+
val bundleSource = bundleLoader.loadBundle()
|
|
374
|
+
|
|
375
|
+
Log.d(TAG, "bundleSource loaded = $bundleSource")
|
|
376
|
+
when (bundleSource) {
|
|
377
|
+
is BundleSource.Production -> {
|
|
378
|
+
Log.d(
|
|
379
|
+
TAG,
|
|
380
|
+
"bundleSource.location = ${bundleSource.location}",
|
|
381
|
+
)
|
|
382
|
+
Log.d(
|
|
383
|
+
TAG,
|
|
384
|
+
"bundleSource.componentName = ${bundleSource.componentName}",
|
|
385
|
+
)
|
|
386
|
+
if (bundleSource.location is ProductionLocation.FileSystemBundle) {
|
|
387
|
+
Log.d(
|
|
388
|
+
TAG,
|
|
389
|
+
"ACTUAL FILE PATH = ${(bundleSource.location as ProductionLocation.FileSystemBundle).filePath}",
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
is BundleSource.DevServer -> {
|
|
394
|
+
Log.d(
|
|
395
|
+
TAG,
|
|
396
|
+
"DevServer mode - ${bundleSource.host}:${bundleSource.port}",
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Create ReactHost with the loaded bundle (safe off main thread)
|
|
402
|
+
val factoryResult = createReactHostWithBundle(activity, bundleSource)
|
|
403
|
+
|
|
404
|
+
// Assign shared state and setup on UI thread to serialize with onDestroy
|
|
405
|
+
activity.runOnUiThread {
|
|
406
|
+
if (pendingLifecycleState != LifecycleState.DESTROYED) {
|
|
407
|
+
currentBundleSource = bundleSource
|
|
408
|
+
reactHost = factoryResult.reactHost
|
|
409
|
+
componentFactory = factoryResult.componentFactory
|
|
410
|
+
setupReactHost(activity, initialProps)
|
|
411
|
+
} else {
|
|
412
|
+
// Activity already destroyed; clean up the host we just created
|
|
413
|
+
factoryResult.reactHost.invalidate()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} catch (e: Exception) {
|
|
417
|
+
// Show error view on UI thread
|
|
418
|
+
activity.runOnUiThread {
|
|
419
|
+
if (pendingLifecycleState != LifecycleState.DESTROYED) {
|
|
420
|
+
errorViewConsumer?.invoke(e)
|
|
421
|
+
?: run {
|
|
422
|
+
// Fallback: Use default behavior if consumer not set
|
|
423
|
+
val errorView =
|
|
424
|
+
errorViewProvider?.invoke(activity, e)
|
|
425
|
+
?: DefaultErrorView(activity, e)
|
|
426
|
+
contentView = errorView
|
|
427
|
+
activity.setContentView(errorView)
|
|
428
|
+
}
|
|
429
|
+
Log.e(TAG, "Failed to load bundle", e)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private fun createReactHostWithBundle(
|
|
437
|
+
activity: AppCompatActivity,
|
|
438
|
+
bundleSource: BundleSource,
|
|
439
|
+
): ReactHostFactory.Result {
|
|
440
|
+
val packages = reactPackagesProvider?.invoke() ?: emptyList()
|
|
441
|
+
return ReactHostFactory.create(activity.applicationContext, bundleSource, packages)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
companion object {
|
|
445
|
+
// Note: BuildConfig.DEBUG reflects this library's build variant, not the consuming app's.
|
|
446
|
+
private const val TAG = "GraniteReactDelegate"
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
package run.granite
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import android.view.View
|
|
5
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
6
|
+
import com.facebook.react.ReactHost
|
|
7
|
+
import com.facebook.react.ReactPackage
|
|
8
|
+
import com.facebook.react.bridge.ReactContext
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Host interface for integrating React Native into activities. This interface allows you to "host"
|
|
12
|
+
* React Native components in any Activity, regardless of your existing inheritance hierarchy.
|
|
13
|
+
*
|
|
14
|
+
* The GraniteReactHost pattern provides maximum flexibility for integrating React Native into
|
|
15
|
+
* existing Android applications with their own BaseActivity implementations.
|
|
16
|
+
*
|
|
17
|
+
* Usage example:
|
|
18
|
+
* ```kotlin
|
|
19
|
+
* class MyActivity : CompanyBaseActivity(), GraniteReactHost {
|
|
20
|
+
* override val graniteDelegate = GraniteActivityDelegateImpl()
|
|
21
|
+
*
|
|
22
|
+
* override fun onCreate(savedInstanceState: Bundle?) {
|
|
23
|
+
* super.onCreate(savedInstanceState)
|
|
24
|
+
* setupHost(savedInstanceState) // Automatically manages all lifecycle events
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* override fun getReactPackages() = PackageList(this).packages
|
|
28
|
+
* override fun getGraniteModules(context: ReactContext) = listOf(MyModule())
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
interface GraniteReactHost {
|
|
33
|
+
/** The delegate that handles all GraniteModule logic */
|
|
34
|
+
val graniteDelegate: GraniteReactDelegate
|
|
35
|
+
|
|
36
|
+
/** Initialize the React Native host. Call this in your activity's onCreate() method. */
|
|
37
|
+
fun AppCompatActivity.setupHost(
|
|
38
|
+
savedInstanceState: Bundle?,
|
|
39
|
+
initialProps: Bundle,
|
|
40
|
+
) {
|
|
41
|
+
// Configure delegate with providers
|
|
42
|
+
graniteDelegate.setReactPackagesProvider { getReactPackages() }
|
|
43
|
+
graniteDelegate.setBundleLoaderProvider { createBundleLoader() }
|
|
44
|
+
graniteDelegate.setLoadingViewProvider { activity -> createLoadingView() }
|
|
45
|
+
graniteDelegate.setErrorViewProvider { activity, error -> createErrorView(error) }
|
|
46
|
+
|
|
47
|
+
// Initialize
|
|
48
|
+
graniteDelegate.onCreate(this, savedInstanceState, initialProps)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Resume the React Native host. Call this in your activity's onResume() method. */
|
|
52
|
+
fun AppCompatActivity.resumeHost() {
|
|
53
|
+
graniteDelegate.onResume(this)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Pause the React Native host. Call this in your activity's onPause() method. */
|
|
57
|
+
fun AppCompatActivity.pauseHost() {
|
|
58
|
+
graniteDelegate.onPause(this)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Destroy the React Native host. Call this in your activity's onDestroy() method. */
|
|
62
|
+
fun AppCompatActivity.destroyHost() {
|
|
63
|
+
graniteDelegate.onDestroy(this)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Abstract methods to be implemented by the activity
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get React packages including core packages and custom modules. Typically returns
|
|
70
|
+
* PackageList(this).packages
|
|
71
|
+
*/
|
|
72
|
+
fun getReactPackages(): List<ReactPackage>
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Provide a BundleLoader implementation. This is now required and must return a valid loader
|
|
76
|
+
* that supplies the component (module) name via BundleSource.
|
|
77
|
+
*/
|
|
78
|
+
fun createBundleLoader(): BundleLoader
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a custom loading view displayed while bundle is loading. Optional - defaults to
|
|
82
|
+
* DefaultLoadingView.
|
|
83
|
+
*/
|
|
84
|
+
fun createLoadingView(): View = DefaultLoadingView(this as AppCompatActivity)
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a custom error view displayed when bundle loading fails. Optional - defaults to
|
|
88
|
+
* DefaultErrorView.
|
|
89
|
+
*/
|
|
90
|
+
fun createErrorView(error: Throwable): View = DefaultErrorView(this as AppCompatActivity, error)
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Called when ReactContext is initialized and ready. Override this method to perform actions
|
|
94
|
+
* that require ReactContext, such as registering Brick modules.
|
|
95
|
+
*
|
|
96
|
+
* @param context The initialized ReactContext
|
|
97
|
+
*/
|
|
98
|
+
fun onReactContextInitialized(context: ReactContext) {
|
|
99
|
+
// Default implementation does nothing
|
|
100
|
+
// Override in your activity to register modules or perform other initialization
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Convenience accessors
|
|
104
|
+
|
|
105
|
+
/** Get the current bundle loader if any. */
|
|
106
|
+
fun getBundleLoader(): BundleLoader? = graniteDelegate.getBundleLoader()
|
|
107
|
+
|
|
108
|
+
/** Get the ReactHost for advanced use cases. */
|
|
109
|
+
fun getReactHost(): ReactHost? = graniteDelegate.getReactHost()
|
|
110
|
+
|
|
111
|
+
/** Check if the React Native host is ready (ReactHost and surface initialized). */
|
|
112
|
+
fun isHostReady(): Boolean = graniteDelegate.isReady()
|
|
113
|
+
}
|