@callstack/react-native-brownfield 1.0.1 → 1.2.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/README.md +151 -37
- package/android/build.gradle +13 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactDelegateWrapper.kt +31 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +88 -43
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +37 -43
- package/android/src/main/java/com/callstack/reactnativebrownfield/utils/VersionUtils.kt +17 -0
- package/ios/ReactNativeBrownfield.swift +11 -1
- package/ios/ReactNativeView.swift +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<
|
|
2
|
-
<img alt="React Native Brownfield" src="https://
|
|
3
|
-
</
|
|
1
|
+
<a href="https://www.callstack.com/open-source?utm_campaign=generic&utm_source=github&utm_medium=referral&utm_content=react-native-brownfield" align="center">
|
|
2
|
+
<img alt="React Native Brownfield" src="https://github.com/user-attachments/assets/55fcdff5-54f0-4081-adf6-55dfa5c29af2">
|
|
3
|
+
</a>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
|
|
6
|
+
A set of helpers to make your brownfield integration smooth and easy.
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -21,86 +21,199 @@
|
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
24
|
-
- **Easily integrate** React Native with existing native app
|
|
24
|
+
- **Easily integrate** React Native with an existing native app
|
|
25
25
|
- Start React Native with **one method** and invoke code as soon as it's loaded
|
|
26
|
-
- Compatible with **both
|
|
26
|
+
- Compatible with **both legacy and new React Native architecture**!
|
|
27
27
|
- Reuse the same instance of React Native between different components
|
|
28
28
|
- Use predefined **native building blocks** - crafted for React Native
|
|
29
29
|
- Disable and enable **native gestures and hardware buttons** from JavaScript
|
|
30
|
-
- Works well with **any native navigation** pattern, as well as
|
|
30
|
+
- Works well with **any native navigation** pattern, as well as any React Native JavaScript-based navigation
|
|
31
31
|
- Compatible with all native languages **Objective-C**, **Swift**, **Java** and **Kotlin**
|
|
32
32
|
- Supports UIKit and SwiftUI on iOS and Fragments and Jetpack Compose on Android
|
|
33
33
|
|
|
34
|
-
|
|
35
34
|
## Installation
|
|
36
35
|
|
|
36
|
+
The React Native Brownfield library is intended to be installed in a React Native app that is later consumed as a framework artifact by your native iOS or Android app.
|
|
37
|
+
|
|
38
|
+
In your React Native project run:
|
|
39
|
+
|
|
37
40
|
```sh
|
|
38
41
|
npm install @callstack/react-native-brownfield
|
|
39
42
|
```
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
## Usage
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
<a href="https://www.callstack.com/ebooks/incremental-react-native-adoption-in-native-apps?utm_campaign=brownfield&utm_source=github&utm_medium=referral&utm_content=react-native-brownfield" align="center">
|
|
47
|
+
<img alt="Download a free copy of Incremental React Native adoption in native apps ebook" src="https://github.com/user-attachments/assets/ba42bb29-1e7a-4683-80c5-2602afb1a7e6">
|
|
48
|
+
</a>
|
|
49
|
+
|
|
50
|
+
### Packaging React Native app as a framework
|
|
51
|
+
|
|
52
|
+
First, we need to package our React Native app as an XCFramework or Fat-AAR.
|
|
53
|
+
|
|
54
|
+
#### With RNEF
|
|
55
|
+
|
|
56
|
+
Follow [Integrating with Native Apps](https://www.rnef.dev/docs/brownfield/intro) steps in RNEF docs and run:
|
|
57
|
+
|
|
58
|
+
- `rnef package:ios` for iOS
|
|
59
|
+
- `rnef package:aar` for Android
|
|
60
|
+
|
|
61
|
+
#### With custom scripts
|
|
62
|
+
|
|
63
|
+
Instead of using RNEF, you can create your own custom packaging scripts. Here are base versions for iOS and Android that you'll need to adjust for your project-specific setup:
|
|
64
|
+
|
|
65
|
+
- [Example iOS script](https://github.com/callstackincubator/modern-brownfield-ref/blob/main/scripts/build-xcframework.sh)
|
|
66
|
+
- [Example Android script](https://github.com/callstackincubator/modern-brownfield-ref/blob/main/scripts/build-aar.sh)
|
|
67
|
+
|
|
68
|
+
### Native iOS app
|
|
69
|
+
|
|
70
|
+
In your native iOS app, initialize React Native and display it where you like. For example, to display React Native views in SwiftUI, use the provided `ReactNativeView` component:
|
|
71
|
+
|
|
72
|
+
```swift
|
|
73
|
+
import SwiftUI
|
|
74
|
+
import ReactBrownfield # exposed by RN app framework
|
|
75
|
+
|
|
76
|
+
@main
|
|
77
|
+
struct MyApp: App {
|
|
78
|
+
init() {
|
|
79
|
+
ReactNativeBrownfield.shared.startReactNative {
|
|
80
|
+
print("React Native bundle loaded")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var body: some Scene {
|
|
85
|
+
WindowGroup {
|
|
86
|
+
ContentView()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
struct ContentView: View {
|
|
92
|
+
var body: some View {
|
|
93
|
+
NavigationView {
|
|
94
|
+
VStack {
|
|
95
|
+
Text("Welcome to the Native App")
|
|
96
|
+
.padding()
|
|
97
|
+
|
|
98
|
+
NavigationLink("Push React Native Screen") {
|
|
99
|
+
ReactNativeView(moduleName: "ReactNative")
|
|
100
|
+
.navigationBarHidden(true)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
45
106
|
```
|
|
46
107
|
|
|
47
|
-
|
|
108
|
+
For more detailed instructions and API for iOS, see docs for:
|
|
109
|
+
|
|
110
|
+
- [Objective C](docs/OBJECTIVE_C.md)
|
|
111
|
+
- [Swift](docs/SWIFT.md)
|
|
112
|
+
|
|
113
|
+
### Native Android app
|
|
48
114
|
|
|
49
|
-
|
|
50
|
-
Add the following to your `android/gradle.properties`:
|
|
115
|
+
In your native Android app, create a new `RNAppFragment.kt`:
|
|
51
116
|
|
|
117
|
+
```kt
|
|
118
|
+
|
|
119
|
+
import android.os.Bundle
|
|
120
|
+
import android.view.LayoutInflater
|
|
121
|
+
import android.view.View
|
|
122
|
+
import android.view.ViewGroup
|
|
123
|
+
import androidx.fragment.app.Fragment
|
|
124
|
+
import com.callstack.rnbrownfield.RNViewFactory # exposed by RN app framework
|
|
125
|
+
|
|
126
|
+
class RNAppFragment : Fragment() {
|
|
127
|
+
override fun onCreateView(
|
|
128
|
+
inflater: LayoutInflater,
|
|
129
|
+
container: ViewGroup?,
|
|
130
|
+
savedInstanceState: Bundle?,
|
|
131
|
+
): View? =
|
|
132
|
+
this.context?.let {
|
|
133
|
+
RNViewFactory.createFrameLayout(it)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
52
136
|
```
|
|
53
|
-
|
|
54
|
-
|
|
137
|
+
|
|
138
|
+
Add a button to your `activity_main.xml`:
|
|
139
|
+
|
|
140
|
+
```xml
|
|
141
|
+
<Button
|
|
142
|
+
android:id="@+id/show_rn_app_btn"
|
|
143
|
+
android:layout_width="wrap_content"
|
|
144
|
+
android:layout_height="wrap_content"
|
|
145
|
+
android:text="Show RN App"
|
|
146
|
+
app:layout_constraintBottom_toBottomOf="parent"
|
|
147
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
148
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
149
|
+
app:layout_constraintTop_toTopOf="parent" />
|
|
55
150
|
```
|
|
56
151
|
|
|
57
|
-
|
|
58
|
-
Install cocoapods with the flag:
|
|
152
|
+
Add a fragment container:
|
|
59
153
|
|
|
60
|
-
```
|
|
61
|
-
|
|
154
|
+
```xml
|
|
155
|
+
<FrameLayout
|
|
156
|
+
android:id="@+id/fragmentContainer"
|
|
157
|
+
android:layout_width="match_parent"
|
|
158
|
+
android:layout_height="match_parent" />
|
|
62
159
|
```
|
|
63
160
|
|
|
64
|
-
|
|
65
|
-
> New Architecture is enabled by default from React Native 0.76
|
|
161
|
+
Update your `MainActivity` to initialize React Native and show the fragment:
|
|
66
162
|
|
|
67
|
-
|
|
163
|
+
```kt
|
|
164
|
+
class MainActivity : AppCompatActivity() {
|
|
165
|
+
private lateinit var showRNAppBtn: Button
|
|
166
|
+
|
|
167
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
168
|
+
super.onCreate(savedInstanceState)
|
|
169
|
+
ReactNativeHostManager.shared.initialize(this.application)
|
|
170
|
+
|
|
171
|
+
showRNAppBtn = findViewById(R.id.show_rn_app_btn)
|
|
172
|
+
showRNAppBtn.setOnClickListener {
|
|
173
|
+
supportFragmentManager
|
|
174
|
+
.beginTransaction()
|
|
175
|
+
.replace(R.id.fragmentContainer, RNAppFragment())
|
|
176
|
+
.commit()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
}
|
|
181
|
+
```
|
|
68
182
|
|
|
69
|
-
|
|
183
|
+
For more detailed instructions and API for Android, see docs for:
|
|
70
184
|
|
|
71
|
-
|
|
72
|
-
|
|
185
|
+
- [Java](docs/JAVA.md)
|
|
186
|
+
- [Kotlin](docs/KOTLIN.md)
|
|
73
187
|
|
|
74
188
|
### JavaScript Module
|
|
75
189
|
|
|
76
|
-
Besides native components, we are exposing JavaScript functions to control the behavior of those components.
|
|
190
|
+
Besides native components, we are exposing JavaScript functions to control the behavior of those components from React Native app.
|
|
77
191
|
|
|
78
|
-
To use the module,
|
|
192
|
+
To use the module, import it:
|
|
79
193
|
|
|
80
194
|
```js
|
|
81
195
|
import ReactNativeBrownfield from '@callstack/react-native-brownfield';
|
|
82
196
|
```
|
|
83
197
|
|
|
84
|
-
|
|
198
|
+
and use the available methods:
|
|
85
199
|
|
|
86
|
-
|
|
200
|
+
#### setNativeBackGestureAndButtonEnabled(enabled: boolean)
|
|
87
201
|
|
|
88
|
-
A method used to toggle iOS native back gesture and Android hardware back button.
|
|
202
|
+
A method used to toggle iOS native back gesture and Android hardware back button.
|
|
89
203
|
|
|
90
|
-
```
|
|
204
|
+
```ts
|
|
91
205
|
ReactNativeBrownfield.setNativeBackGestureAndButtonEnabled(true);
|
|
92
206
|
```
|
|
93
207
|
|
|
94
|
-
|
|
208
|
+
#### popToNative(animated[iOS only]: boolean)
|
|
95
209
|
|
|
96
|
-
A method to pop to native screen used to push React Native experience.
|
|
210
|
+
A method to pop to native screen used to push React Native experience.
|
|
97
211
|
|
|
98
|
-
```
|
|
212
|
+
```ts
|
|
99
213
|
ReactNativeBrownfield.popToNative(true);
|
|
100
214
|
```
|
|
101
215
|
|
|
102
|
-
>
|
|
103
|
-
|
|
216
|
+
> **Note:** These methods work only with native components provided by this library.
|
|
104
217
|
|
|
105
218
|
## Made with ❤️ at Callstack
|
|
106
219
|
|
|
@@ -122,6 +235,7 @@ Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds
|
|
|
122
235
|
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
|
|
123
236
|
|
|
124
237
|
<!-- badges -->
|
|
238
|
+
|
|
125
239
|
[build-badge]: https://img.shields.io/circleci/build/github/callstack/react-native-brownfield/master.svg?style=flat-square
|
|
126
240
|
[build]: https://circleci.com/gh/callstack/react-native-brownfield
|
|
127
241
|
[version-badge]: https://img.shields.io/npm/v/@callstack/react-native-brownfield.svg?style=flat-square
|
package/android/build.gradle
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import groovy.json.JsonSlurper
|
|
2
|
+
|
|
3
|
+
def reactNativeVersion = null
|
|
4
|
+
def packageJsonFile = rootProject.file("../node_modules/react-native/package.json")
|
|
5
|
+
|
|
6
|
+
if (packageJsonFile.exists()) {
|
|
7
|
+
def json = new JsonSlurper().parse(packageJsonFile)
|
|
8
|
+
reactNativeVersion = json.version
|
|
9
|
+
} else {
|
|
10
|
+
reactNativeVersion = "unknown"
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
buildscript {
|
|
2
14
|
// Buildscript is evaluated before everything else so we can't use getExtOrDefault
|
|
3
15
|
def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["RNBrownfield_kotlinVersion"]
|
|
@@ -64,6 +76,7 @@ android {
|
|
|
64
76
|
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
65
77
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
|
66
78
|
buildConfigField "boolean", "IS_HERMES_ENABLED", isHermesEnabled().toString()
|
|
79
|
+
buildConfigField "String", "RN_VERSION", "\"$reactNativeVersion\""
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
buildFeatures {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.callstack.reactnativebrownfield
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.activity.ComponentActivity
|
|
5
|
+
import com.facebook.react.ReactDelegate
|
|
6
|
+
import com.facebook.react.ReactHost
|
|
7
|
+
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
|
|
8
|
+
|
|
9
|
+
class ReactDelegateWrapper(
|
|
10
|
+
private val activity: ComponentActivity?,
|
|
11
|
+
private val reactHost: ReactHost,
|
|
12
|
+
moduleName: String,
|
|
13
|
+
launchOptions: Bundle?
|
|
14
|
+
): ReactDelegate(activity, reactHost, moduleName, launchOptions){
|
|
15
|
+
private lateinit var hardwareBackHandler: () -> Unit
|
|
16
|
+
private val backBtnHandler = DefaultHardwareBackBtnHandler {
|
|
17
|
+
hardwareBackHandler()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This is invoked when there is no more RN Stack to pop.
|
|
22
|
+
* What it means that this is now the initial RN screen.
|
|
23
|
+
*/
|
|
24
|
+
fun setHardwareBackHandler(backHandler: () -> Unit) {
|
|
25
|
+
hardwareBackHandler = backHandler
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun onHostResume() {
|
|
29
|
+
reactHost.onHostResume(activity, backBtnHandler)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -4,10 +4,11 @@ import android.app.Application
|
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.os.Bundle
|
|
6
6
|
import android.widget.FrameLayout
|
|
7
|
+
import androidx.activity.OnBackPressedCallback
|
|
7
8
|
import androidx.fragment.app.FragmentActivity
|
|
8
9
|
import androidx.lifecycle.DefaultLifecycleObserver
|
|
9
10
|
import androidx.lifecycle.LifecycleOwner
|
|
10
|
-
import com.
|
|
11
|
+
import com.callstack.reactnativebrownfield.utils.VersionUtils
|
|
11
12
|
import com.facebook.react.ReactInstanceEventListener
|
|
12
13
|
import com.facebook.react.ReactInstanceManager
|
|
13
14
|
import com.facebook.react.ReactNativeHost
|
|
@@ -21,10 +22,17 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|
|
21
22
|
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
|
22
23
|
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
|
23
24
|
|
|
24
|
-
interface
|
|
25
|
+
fun interface OnJSBundleLoaded {
|
|
25
26
|
operator fun invoke(initialized: Boolean)
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The threshold RN version based on which we decide whether to
|
|
31
|
+
* load JNI libs or not. We only load JNI libs on version less
|
|
32
|
+
* than this.
|
|
33
|
+
*/
|
|
34
|
+
private const val RN_THRESHOLD_VERSION = "0.80.0"
|
|
35
|
+
|
|
28
36
|
class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNativeHost) {
|
|
29
37
|
companion object {
|
|
30
38
|
private lateinit var instance: ReactNativeBrownfield
|
|
@@ -33,16 +41,34 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative
|
|
|
33
41
|
@JvmStatic
|
|
34
42
|
val shared: ReactNativeBrownfield get() = instance
|
|
35
43
|
|
|
44
|
+
private fun loadNativeLibs (application: Application) {
|
|
45
|
+
val rnVersion = BuildConfig.RN_VERSION
|
|
46
|
+
|
|
47
|
+
if (VersionUtils.isVersionLessThan(rnVersion, RN_THRESHOLD_VERSION)) {
|
|
48
|
+
SoLoader.init(application.applicationContext, OpenSourceMergedSoMapping)
|
|
49
|
+
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
|
50
|
+
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
|
51
|
+
load()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
@JvmStatic
|
|
37
|
-
|
|
57
|
+
@JvmOverloads
|
|
58
|
+
fun initialize(application: Application, rnHost: ReactNativeHost, onJSBundleLoaded: OnJSBundleLoaded? = null) {
|
|
38
59
|
if (!initialized.getAndSet(true)) {
|
|
60
|
+
loadNativeLibs(application)
|
|
39
61
|
instance = ReactNativeBrownfield(rnHost)
|
|
40
|
-
|
|
62
|
+
|
|
63
|
+
preloadReactNative {
|
|
64
|
+
onJSBundleLoaded?.invoke(true)
|
|
65
|
+
}
|
|
41
66
|
}
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
@JvmStatic
|
|
45
|
-
|
|
70
|
+
@JvmOverloads
|
|
71
|
+
fun initialize(application: Application, options: HashMap<String, Any>, onJSBundleLoaded: OnJSBundleLoaded? = null) {
|
|
46
72
|
val reactNativeHost: ReactNativeHost =
|
|
47
73
|
object : DefaultReactNativeHost(application) {
|
|
48
74
|
|
|
@@ -61,37 +87,27 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative
|
|
|
61
87
|
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
|
62
88
|
}
|
|
63
89
|
|
|
64
|
-
initialize(application, reactNativeHost)
|
|
90
|
+
initialize(application, reactNativeHost, onJSBundleLoaded)
|
|
65
91
|
}
|
|
66
92
|
|
|
67
93
|
@JvmStatic
|
|
68
|
-
|
|
94
|
+
@JvmOverloads
|
|
95
|
+
fun initialize(application: Application, packages: List<ReactPackage>, onJSBundleLoaded: OnJSBundleLoaded? = null) {
|
|
69
96
|
val options = hashMapOf("packages" to packages, "mainModuleName" to "index")
|
|
70
97
|
|
|
71
|
-
initialize(application, options)
|
|
98
|
+
initialize(application, options, onJSBundleLoaded)
|
|
72
99
|
}
|
|
73
100
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
ReactInstanceEventListener {
|
|
85
|
-
override fun onReactContextInitialized(reactContext: ReactContext) {
|
|
86
|
-
callback?.let { it(true) }
|
|
87
|
-
reactNativeHost.reactInstanceManager.removeReactInstanceEventListener(this)
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
reactNativeHost.reactInstanceManager?.createReactContextInBackground()
|
|
91
|
-
|
|
92
|
-
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
|
93
|
-
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
|
94
|
-
load()
|
|
101
|
+
private fun preloadReactNative(callback: ((Boolean) -> Unit)) {
|
|
102
|
+
val reactInstanceManager = shared.reactNativeHost.reactInstanceManager
|
|
103
|
+
reactInstanceManager.addReactInstanceEventListener(object :
|
|
104
|
+
ReactInstanceEventListener {
|
|
105
|
+
override fun onReactContextInitialized(reactContext: ReactContext) {
|
|
106
|
+
callback(true)
|
|
107
|
+
reactInstanceManager.removeReactInstanceEventListener(this)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
reactInstanceManager?.createReactContextInBackground()
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
|
|
@@ -99,6 +115,7 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative
|
|
|
99
115
|
context: Context,
|
|
100
116
|
activity: FragmentActivity?,
|
|
101
117
|
moduleName: String,
|
|
118
|
+
reactDelegate: ReactDelegateWrapper? = null,
|
|
102
119
|
launchOptions: Bundle? = null,
|
|
103
120
|
): FrameLayout {
|
|
104
121
|
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
|
@@ -106,25 +123,36 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative
|
|
|
106
123
|
context,
|
|
107
124
|
shared.reactNativeHost
|
|
108
125
|
)
|
|
109
|
-
val reactDelegate = ReactDelegate(activity, reactHost, moduleName, launchOptions)
|
|
110
126
|
|
|
111
|
-
activity
|
|
112
|
-
override fun onResume(owner: LifecycleOwner) {
|
|
113
|
-
reactDelegate.onHostResume()
|
|
114
|
-
}
|
|
127
|
+
val resolvedDelegate = reactDelegate ?: ReactDelegateWrapper(activity, reactHost, moduleName, launchOptions)
|
|
115
128
|
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
val mBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
|
|
130
|
+
override fun handleOnBackPressed() {
|
|
131
|
+
// invoked for JS stack back navigation
|
|
132
|
+
resolvedDelegate.onBackPressed()
|
|
118
133
|
}
|
|
134
|
+
}
|
|
119
135
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
// Register back press callback
|
|
137
|
+
activity?.onBackPressedDispatcher?.addCallback(mBackPressedCallback)
|
|
138
|
+
// invoked on the last RN screen exit
|
|
139
|
+
resolvedDelegate.setHardwareBackHandler {
|
|
140
|
+
mBackPressedCallback.isEnabled = false
|
|
141
|
+
activity?.onBackPressedDispatcher?.onBackPressed()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* When createView method is called in ReactNativeFragment, a reactDelegate
|
|
146
|
+
* instance is required. In such a case, we use the lifeCycle events of the fragment.
|
|
147
|
+
* When createView method is called elsewhere, then reactDelegate is not required.
|
|
148
|
+
* In such a case, we set the lifeCycle observer.
|
|
149
|
+
*/
|
|
150
|
+
if (reactDelegate == null) {
|
|
151
|
+
activity?.lifecycle?.addObserver(getLifeCycleObserver(resolvedDelegate))
|
|
152
|
+
}
|
|
125
153
|
|
|
126
|
-
|
|
127
|
-
return
|
|
154
|
+
resolvedDelegate.loadApp()
|
|
155
|
+
return resolvedDelegate.reactRootView!!
|
|
128
156
|
}
|
|
129
157
|
|
|
130
158
|
val instanceManager: ReactInstanceManager? = shared.reactNativeHost?.reactInstanceManager
|
|
@@ -137,5 +165,22 @@ class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNative
|
|
|
137
165
|
|
|
138
166
|
return reactView
|
|
139
167
|
}
|
|
168
|
+
|
|
169
|
+
private fun getLifeCycleObserver(reactDelegate: ReactDelegateWrapper): DefaultLifecycleObserver {
|
|
170
|
+
return object : DefaultLifecycleObserver {
|
|
171
|
+
override fun onResume(owner: LifecycleOwner) {
|
|
172
|
+
reactDelegate.onHostResume()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
override fun onPause(owner: LifecycleOwner) {
|
|
176
|
+
reactDelegate.onHostPause()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
override fun onDestroy(owner: LifecycleOwner) {
|
|
180
|
+
reactDelegate.onHostDestroy()
|
|
181
|
+
owner.lifecycle.removeObserver(this) // Cleanup to avoid leaks
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
140
185
|
}
|
|
141
186
|
|
|
@@ -3,17 +3,19 @@ package com.callstack.reactnativebrownfield;
|
|
|
3
3
|
import android.annotation.TargetApi
|
|
4
4
|
import android.os.Build
|
|
5
5
|
import android.os.Bundle
|
|
6
|
+
import android.util.Log
|
|
6
7
|
import android.view.KeyEvent
|
|
8
|
+
import android.view.LayoutInflater
|
|
9
|
+
import android.view.View
|
|
10
|
+
import android.view.ViewGroup
|
|
7
11
|
import com.facebook.infer.annotation.Assertions
|
|
8
12
|
import com.facebook.react.ReactFragment
|
|
9
13
|
import com.facebook.react.ReactHost
|
|
10
14
|
import com.facebook.react.ReactNativeHost
|
|
11
15
|
import com.facebook.react.bridge.Callback
|
|
12
16
|
import com.facebook.react.bridge.WritableMap
|
|
13
|
-
import com.facebook.react.common.LifecycleState
|
|
14
17
|
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
|
15
18
|
import com.facebook.react.devsupport.DoubleTapReloadRecognizer
|
|
16
|
-
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
|
|
17
19
|
import com.facebook.react.modules.core.PermissionAwareActivity
|
|
18
20
|
import com.facebook.react.modules.core.PermissionListener
|
|
19
21
|
|
|
@@ -21,12 +23,40 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
21
23
|
private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer
|
|
22
24
|
private lateinit var permissionsCallback: Callback
|
|
23
25
|
private var permissionListener: PermissionListener? = null
|
|
26
|
+
private lateinit var moduleName: String
|
|
24
27
|
|
|
25
28
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
26
|
-
|
|
29
|
+
/**
|
|
30
|
+
* ReactFragment.onCreate will throw an exception if we do not provide arg_component_name as arguments.
|
|
31
|
+
* We silently catch this exception. The reason is we want to invoke the super<Fragment>.onCreate in
|
|
32
|
+
* ReactFragment. Then initialise the mReactDelegate with ReactDelegateWrapper instead of ReactDelegate.
|
|
33
|
+
*
|
|
34
|
+
* So we purposely force ReactFragment.onCreate to throw an exception, so that we can provide our own
|
|
35
|
+
* implementation for mReactDelegate: ReactDelegateWrapper
|
|
36
|
+
*/
|
|
37
|
+
try{
|
|
38
|
+
super.onCreate(savedInstanceState)
|
|
39
|
+
} catch (e: IllegalStateException){
|
|
40
|
+
Log.w("ReactNativeFragment", "ReactFragment threw due to missing arg_component_name: ${e.message} - This is an expected behaviour.")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
moduleName = arguments?.getString(ARG_MODULE_NAME)!!
|
|
44
|
+
this.mReactDelegate = this.reactHost?.let {
|
|
45
|
+
ReactDelegateWrapper(activity,
|
|
46
|
+
it, moduleName, arguments?.getBundle("arg_launch_options"))
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
doubleTapReloadRecognizer = DoubleTapReloadRecognizer()
|
|
28
50
|
}
|
|
29
51
|
|
|
52
|
+
override fun onCreateView(
|
|
53
|
+
inflater: LayoutInflater,
|
|
54
|
+
container: ViewGroup?,
|
|
55
|
+
savedInstanceState: Bundle?
|
|
56
|
+
): View {
|
|
57
|
+
return ReactNativeBrownfield.shared.createView(this.requireContext(), activity, moduleName, this.mReactDelegate as ReactDelegateWrapper)
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
override fun getReactHost(): ReactHost? {
|
|
31
61
|
return activity?.let {
|
|
32
62
|
getDefaultReactHost(
|
|
@@ -40,36 +70,6 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
40
70
|
return ReactNativeBrownfield.shared.reactNativeHost
|
|
41
71
|
}
|
|
42
72
|
|
|
43
|
-
override fun onResume() {
|
|
44
|
-
super.onResume()
|
|
45
|
-
if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
|
|
46
|
-
ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager?.onHostResume(
|
|
47
|
-
activity,
|
|
48
|
-
activity as DefaultHardwareBackBtnHandler
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
override fun onPause() {
|
|
54
|
-
super.onPause()
|
|
55
|
-
if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
|
|
56
|
-
ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager?.onHostPause(
|
|
57
|
-
activity
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
override fun onDestroy() {
|
|
63
|
-
super.onDestroy()
|
|
64
|
-
if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
|
|
65
|
-
val reactInstanceMgr = ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager
|
|
66
|
-
|
|
67
|
-
if (reactInstanceMgr.lifecycleState != LifecycleState.RESUMED) {
|
|
68
|
-
reactInstanceMgr.onHostDestroy(activity)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
73
|
override fun onRequestPermissionsResult(
|
|
74
74
|
requestCode: Int,
|
|
75
75
|
permissions: Array<String>,
|
|
@@ -118,22 +118,16 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
118
118
|
.didDoubleTapR(keyCode, it)
|
|
119
119
|
}
|
|
120
120
|
if (didDoubleTapR == true) {
|
|
121
|
-
|
|
121
|
+
reactDelegate.reload()
|
|
122
122
|
handled = true
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
return handled
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
fun onBackPressed(backBtnHandler: DefaultHardwareBackBtnHandler) {
|
|
129
|
-
if (ReactNativeBrownfieldModule.shouldPopToNative) {
|
|
130
|
-
backBtnHandler.invokeDefaultOnBackPressed()
|
|
131
|
-
} else if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
|
|
132
|
-
ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager.onBackPressed()
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
128
|
companion object {
|
|
129
|
+
private const val ARG_MODULE_NAME = "arg_module_name"
|
|
130
|
+
|
|
137
131
|
@JvmStatic
|
|
138
132
|
@JvmOverloads
|
|
139
133
|
fun createReactNativeFragment(
|
|
@@ -142,7 +136,7 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
|
|
|
142
136
|
): ReactNativeFragment {
|
|
143
137
|
val fragment = ReactNativeFragment()
|
|
144
138
|
val args = Bundle()
|
|
145
|
-
args.putString(
|
|
139
|
+
args.putString(ARG_MODULE_NAME, moduleName)
|
|
146
140
|
if (initialProps != null) {
|
|
147
141
|
args.putBundle(ARG_LAUNCH_OPTIONS, initialProps)
|
|
148
142
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.callstack.reactnativebrownfield.utils
|
|
2
|
+
|
|
3
|
+
object VersionUtils {
|
|
4
|
+
fun isVersionLessThan(version: String, threshold: String): Boolean {
|
|
5
|
+
val versionParts = version.split(".").map { it.toIntOrNull() ?: 0 }
|
|
6
|
+
val thresholdParts = threshold.split(".").map { it.toIntOrNull() ?: 0 }
|
|
7
|
+
|
|
8
|
+
val maxLength = maxOf(versionParts.size, thresholdParts.size)
|
|
9
|
+
for (i in 0 until maxLength) {
|
|
10
|
+
val vPart = versionParts.getOrNull(i) ?: 0
|
|
11
|
+
val tPart = thresholdParts.getOrNull(i) ?: 0
|
|
12
|
+
if (vPart != tPart) return vPart < tPart
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return false // equal versions are not less than
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -6,6 +6,7 @@ internal import ReactAppDependencyProvider
|
|
|
6
6
|
class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate {
|
|
7
7
|
var entryFile = "index"
|
|
8
8
|
var bundlePath = "main.jsbundle"
|
|
9
|
+
var bundle = Bundle.main
|
|
9
10
|
// MARK: - RCTReactNativeFactoryDelegate Methods
|
|
10
11
|
|
|
11
12
|
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
|
@@ -21,7 +22,7 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate {
|
|
|
21
22
|
let resourceName = withoutLast.joined()
|
|
22
23
|
let fileExtension = resourceURLComponents.last ?? ""
|
|
23
24
|
|
|
24
|
-
return
|
|
25
|
+
return bundle.url(forResource: resourceName, withExtension: fileExtension)
|
|
25
26
|
#endif
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -54,6 +55,15 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate {
|
|
|
54
55
|
delegate.bundlePath = bundlePath
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Bundle instance to lookup the JavaScript bundle.
|
|
60
|
+
* Default value: Bundle.main
|
|
61
|
+
*/
|
|
62
|
+
@objc public var bundle: Bundle = Bundle.main {
|
|
63
|
+
didSet {
|
|
64
|
+
delegate.bundle = bundle
|
|
65
|
+
}
|
|
66
|
+
}
|
|
57
67
|
/**
|
|
58
68
|
* React Native factory instance created when starting React Native.
|
|
59
69
|
* Default value: nil
|
|
@@ -8,7 +8,10 @@ struct ReactNativeViewRepresentable: UIViewControllerRepresentable {
|
|
|
8
8
|
var initialProperties: [String: Any] = [:]
|
|
9
9
|
|
|
10
10
|
func makeUIViewController(context: Context) -> UIViewController {
|
|
11
|
-
return ReactNativeViewController(
|
|
11
|
+
return ReactNativeViewController(
|
|
12
|
+
moduleName: moduleName,
|
|
13
|
+
initialProperties: initialProperties
|
|
14
|
+
)
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
|