diff --git a/README.md b/README.md index f9517d16..018267b9 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,13 @@ - Compatible with all native languages **Objective-C**, **Swift**, **Java** and **Kotlin** - Supports UIKit and SwiftUI on iOS and Fragments and Jetpack Compose on Android +## React Native version compatibility matrix + +| Tested React Native Version | React Native Brownfield Version | +| --------------------------- | ------------------------------- | +| 0.81.x, 0.82.x | ^2.0.0-rc.0 | +| 0.78.x | ^1.2.0 | + ## Installation 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. @@ -51,16 +58,16 @@ npm install @callstack/react-native-brownfield First, we need to package our React Native app as an XCFramework or Fat-AAR. -#### With RNEF +#### With Rock -Follow [Integrating with Native Apps](https://www.rnef.dev/docs/brownfield/intro) steps in RNEF docs and run: +Follow [Integrating with Native Apps](https://www.rockjs.dev/docs/brownfield/intro) steps in Rock docs and run: -- `rnef package:ios` for iOS -- `rnef package:aar` for Android +- `rock package:ios` for iOS +- `rock package:aar` for Android #### With custom scripts -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: +Instead of using Rock, 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: - [Example iOS script](https://github.com/callstackincubator/modern-brownfield-ref/blob/main/scripts/build-xcframework.sh) - [Example Android script](https://github.com/callstackincubator/modern-brownfield-ref/blob/main/scripts/build-aar.sh) @@ -121,17 +128,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import com.callstack.rnbrownfield.RNViewFactory // exposed by RN app framework class RNAppFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? = - this.context?.let { - RNViewFactory.createFrameLayout(it) - } + ): View? = ReactNativeBrownfield.shared.createView(activity, "BrownFieldTest") } ``` @@ -166,7 +169,9 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - ReactNativeHostManager.shared.initialize(this.application) + ReactNativeHostManager.shared.initialize(this.application) { + println("JS bundle loaded") + } showRNAppBtn = findViewById(R.id.show_rn_app_btn) showRNAppBtn.setOnClickListener { @@ -228,7 +233,7 @@ Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds | [Michał Chudziak
Michał Chudziak](https://twitter.com/michalchudziak)
[💻](https://github.com/callstack/react-native-brownfield/commits?author=michalchudziak "Code") [📖](https://github.com/callstack/react-native-brownfield/commits?author=michalchudziak "Documentation") [🤔](#ideas-michalchudziak "Ideas, Planning, & Feedback") | [Piotr Drapich
Piotr Drapich](https://twitter.com/dratwas)
[💻](https://github.com/callstack/react-native-brownfield/commits?author=dratwas "Code") [🤔](#ideas-dratwas "Ideas, Planning, & Feedback") | -| :---: | :---: | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | diff --git a/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt b/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt index 5b08f883..7f599393 100644 --- a/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +++ b/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt @@ -12,6 +12,7 @@ import com.facebook.react.ReactHost import com.facebook.react.ReactInstanceEventListener import com.facebook.react.ReactPackage import com.facebook.react.bridge.ReactContext +import com.facebook.react.common.build.ReactBuildConfig import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.soloader.OpenSourceMergedSoMapping @@ -75,6 +76,9 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) { context = application, packageList = (options["packages"] as? List<*> ?: emptyList()) .filterIsInstance(), + jsMainModulePath = options["mainModuleName"] as? String ?: "index", + useDevSupport = options["useDeveloperSupport"] as? Boolean + ?: ReactBuildConfig.DEBUG, jsRuntimeFactory = null ) } diff --git a/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt b/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt index 0942da92..5efe9fd9 100644 --- a/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +++ b/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt @@ -5,7 +5,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.callstack.reactnativebrownfield.constants.ReactNativeInitialPropsNames +import com.callstack.reactnativebrownfield.constants.ReactNativeFragmentArgNames import com.facebook.react.ReactFragment import com.facebook.react.ReactHost import com.facebook.react.bridge.Callback @@ -36,13 +36,13 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity { ) } - moduleName = arguments?.getString(ReactNativeInitialPropsNames.ARG_MODULE_NAME)!! + moduleName = arguments?.getString(ReactNativeFragmentArgNames.ARG_MODULE_NAME)!! this.reactDelegate = ReactDelegateWrapper( activity, this.reactHost, moduleName, - arguments?.getBundle(ReactNativeInitialPropsNames.ARG_LAUNCH_OPTIONS) + arguments?.getBundle(ReactNativeFragmentArgNames.ARG_LAUNCH_OPTIONS) ) } @@ -113,9 +113,9 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity { ): ReactNativeFragment { val fragment = ReactNativeFragment() val args = Bundle() - args.putString(ReactNativeInitialPropsNames.ARG_MODULE_NAME, moduleName) + args.putString(ReactNativeFragmentArgNames.ARG_MODULE_NAME, moduleName) if (initialProps != null) { - args.putBundle(ReactNativeInitialPropsNames.ARG_LAUNCH_OPTIONS, initialProps) + args.putBundle(ReactNativeFragmentArgNames.ARG_LAUNCH_OPTIONS, initialProps) } fragment.arguments = args return fragment diff --git a/android/src/main/java/com/callstack/reactnativebrownfield/constants/ReactNativeInitialPropsNames.kt b/android/src/main/java/com/callstack/reactnativebrownfield/constants/ReactNativeFragmentArgNames.kt similarity index 89% rename from android/src/main/java/com/callstack/reactnativebrownfield/constants/ReactNativeInitialPropsNames.kt rename to android/src/main/java/com/callstack/reactnativebrownfield/constants/ReactNativeFragmentArgNames.kt index e5c36a13..7a63bcc9 100644 --- a/android/src/main/java/com/callstack/reactnativebrownfield/constants/ReactNativeInitialPropsNames.kt +++ b/android/src/main/java/com/callstack/reactnativebrownfield/constants/ReactNativeFragmentArgNames.kt @@ -5,7 +5,7 @@ import com.facebook.react.ReactFragment /** * Convenience export of arguments that can be used */ -class ReactNativeInitialPropsNames private constructor() : +class ReactNativeFragmentArgNames private constructor() : ReactFragment() // subclass to gain access to protected constants { companion object { diff --git a/docs/JAVA.md b/docs/JAVA.md index 67367522..d0443893 100644 --- a/docs/JAVA.md +++ b/docs/JAVA.md @@ -25,13 +25,13 @@ With react-native >= 0.80.0, an auto-generated file was added which is responsib you will have this file `ReactNativeApplicationEntryPoint` available. If you're consuming this library in a RN android library which is backed by `com.callstack.react:brownfield-gradle-plugin`, then this file will also be available. -Below is the code you need to add before you call `RNBrownfield.initialize`: +Below is the code you need to add before you call `ReactNativeBrownfield.initialize`: ```java import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative loadReactNative(application); -RNBrownfield.initialize(application, packages); +ReactNativeBrownfield.initialize(application, packages); ```
@@ -62,15 +62,15 @@ A function used to initialize a React Native Brownfield singleton. Keep in mind Params: -| Param | Required | Type | Description | -| ----------------------- | -------- | -------------------- | --------------------------------------------------------- | -| application | Yes | `Application` | Main application. | -| rnHost | No* | `ReactNativeHost` | An instance of [ReactNativeHost](https://bit.ly/2ZnwgnA). | -| packages | No* | `List` | List of your React Native Native modules. | -| options | No* | `HashMap` | Map of initial options. __Options listed below.__ | -| onJSBundleLoaded | No* | `OnJSBundleLoaded` | Callback invoked after JS bundle is fully loaded. | +| Param | Required | Type | Description | +| ---------------- | ------------ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| application | Yes | `Application` | Main application. | +| reactHost | Exclusively* | `ReactHost` | An instance of [ReactHost](https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactHost.kt). | +| packages | Exclusively* | `List` | List of your React Native Native modules. | +| options | Exclusively* | `HashMap` | Map of initial options. __Options listed below.__ | +| onJSBundleLoaded | Exclusively* | `OnJSBundleLoaded` | Callback invoked after JS bundle is fully loaded. | -> * - Those fields aren't itself required, but at least one of them is. See examples below. +> * - From the marked fields, exactly one must be specified, excluding the others. See examples below. Available options: - `useDeveloperSupport`: `Boolean` - Flag to use dev support. @@ -103,7 +103,7 @@ private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { ReactNativeBrownfield.initialize(this, mReactNativeHost); -OR +// OR ReactNativeBrownfield.initialize(this, mReactNativeHost, initialized -> { // JS bundle loaded @@ -115,7 +115,7 @@ List packages = new PackageList(this).getPackages(); ReactNativeBrownfield.initialize(this, packages); -OR +// OR ReactNativeBrownfield.initialize(this, packages, initialized -> { // JS bundle loaded @@ -130,7 +130,7 @@ options.put("mainModuleName", "example/index"); ReactNativeBrownfield.initialize(this, options); -OR +// OR ReactNativeBrownfield.initialize(this, options, initialized -> { // JS bundle loaded @@ -153,9 +153,9 @@ ReactNativeBrownfield.getShared() **Properties:** -| Property | Type | Default | Description | -| --------------- | --------------- | -------------- | --------------------------------------------------------- | -| reactNativeHost | `ReactNativeHost` | null | An instance of [ReactNativeHost](https://bit.ly/2ZnwgnA). | +| Property | Type | Default | Description | +| --------- | ----------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| reactHost | `ReactHost` | null | An instance of [ReactHost](https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactHost.kt). | --- @@ -163,16 +163,16 @@ ReactNativeBrownfield.getShared() `createView` -Creates a React Native view with a given module name. It automatically uses an instance of React Native created in `startReactNative` method. This is useful when embedding React Native views directly in your native layouts. +Creates a React Native view with a given module name. It automatically uses an instance of React Native created in `initialize` method. This is useful when embedding React Native views directly in your native layouts. Params: -| Param | Required | Type | Description | -| -------------- | -------- | ------------------- | ----------------------------------------------------------- | -| context | Yes | `Context` | Android context to create the view | -| activity | No | `FragmentActivity` | Activity hosting the view, used for lifecycle management | -| moduleName | Yes | `String` | Name of React Native component registered to `AppRegistry` | -| launchOptions | No | `Bundle` | Initial properties to be passed to React Native component | +| Param | Required | Type | Description | +| ------------- | -------- | ------------------ | ---------------------------------------------------------- | +| context | Yes | `Context` | Android context to create the view | +| activity | No | `FragmentActivity` | Activity hosting the view, used for lifecycle management | +| moduleName | Yes | `String` | Name of React Native component registered to `AppRegistry` | +| launchOptions | No | `Bundle` | Initial properties to be passed to React Native component | Returns: `FrameLayout` - A view containing the React Native component. @@ -193,7 +193,7 @@ container.addView(reactView); #### `ReactNativeFragment` -An fragment rendering `ReactRootView` with a given module name. It automatically uses an instance of a React Native created in `startReactNative` method. It works well with exposed JavaScript module. All the lifecycles are proxied to `ReactInstanceManager`. It's the simplest way to embed React Native into your navigation stack. +An fragment rendering `ReactRootView` with a given module name. It automatically uses an instance of a React Native created in `initialize` method. It works well with exposed JavaScript module. All the lifecycles are proxied to `ReactInstanceManager`. It's the simplest way to embed React Native into your navigation stack. ```java import com.callstack.reactnativebrownfield.ReactNativeFragment; @@ -209,10 +209,10 @@ Creates a Fragment with `ReactNativeActivity`, you can use it as a parameter in Params: -| Param | Required | Type | Description | -| ----------------------- | -------- | ------------------------------------------- | ----------------------------------------------------------- | -| moduleName | Yes | `String` | Name of React Native component registered to `AppRegistry`. | -| initialProps | No | `Bundle` \|\| `HashMap` \|\| `ReadableMap` | Initial properties to be passed to React Native component. | +| Param | Required | Type | Description | +| ------------ | -------- | ----------------------------------------------------- | ----------------------------------------------------------- | +| moduleName | Yes | `String` | Name of React Native component registered to `AppRegistry`. | +| initialProps | No | `Bundle` \|\| `HashMap` \|\| `ReadableMap` | Initial properties to be passed to React Native component. | Examples: diff --git a/docs/KOTLIN.md b/docs/KOTLIN.md index d30eec9e..ca6e8dde 100644 --- a/docs/KOTLIN.md +++ b/docs/KOTLIN.md @@ -8,13 +8,13 @@ With react-native >= 0.80.0, an auto-generated file was added which is responsib you will have this file `ReactNativeApplicationEntryPoint` available. If you're consuming this library in a RN android library which is backed by `com.callstack.react:brownfield-gradle-plugin`, then this file will also be available. -Below is the code you need to add before you call `RNBrownfield.initialize`: +Below is the code you need to add before you call `ReactNativeBrownfield.initialize`: ```kt import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative loadReactNative(application) -RNBrownfield.initialize(application, packages) +ReactNativeBrownfield.initialize(application, packages) ```
@@ -45,15 +45,15 @@ A function used to initialize a React Native Brownfield singleton. Keep in mind Params: -| Param | Required | Type | Description | -| ----------------------- | -------- | -------------------- | --------------------------------------------------------- | -| application | Yes | `Application` | Main application. | -| rnHost | No* | `ReactNativeHost` | An instance of [ReactNativeHost](https://bit.ly/2ZnwgnA). | -| packages | No* | `List` | List of your React Native Native modules. | -| options | No* | `HashMap` | Map of initial options. __Options listed below.__ | -| onJSBundleLoaded | No* | `OnJSBundleLoaded` | Callback invoked after JS bundle is fully loaded. | +| Param | Required | Type | Description | +| ---------------- | ------------ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| application | Yes | `Application` | Main application. | +| reactHost | Exclusively* | `ReactHost` | An instance of [ReactHost](https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactHost.kt). | +| packages | Exclusively* | `List` | List of your React Native Native modules. | +| options | Exclusively* | `HashMap` | Map of initial options. __Options listed below.__ | +| onJSBundleLoaded | Exclusively* | `OnJSBundleLoaded` | Callback invoked after JS bundle is fully loaded. | -> * - Those fields aren't itself required, but at least one of them is. See examples below. +> `*` - From the marked fields, exactly one must be specified, excluding the others. See examples below. Available options: - `useDeveloperSupport`: `Boolean` - Flag to use dev support. @@ -81,7 +81,7 @@ val mReactNativeHost = object : ReactNativeHost(application) { ReactNativeBrownfield.initialize(this, mReactNativeHost) -OR +// OR ReactNativeBrownfield.initialize(this, mReactNativeHost) { // onJSBundleLoaded @@ -93,7 +93,7 @@ val packages = PackageList(this).getPackages() ReactNativeBrownfield.initialize(this, packages) -OR +// OR ReactNativeBrownfield.initialize(this, packages) { // onJSBundleLoaded @@ -109,7 +109,7 @@ val options = hashMapOf( ReactNativeBrownfield.initialize(this, options) -OR +// OR ReactNativeBrownfield.initialize(this, options) { // onJSBundleLoaded @@ -132,9 +132,9 @@ ReactNativeBrownfield.shared **Properties:** -| Property | Type | Default | Description | -| --------------- | --------------- | -------------- | --------------------------------------------------------- | -| reactNativeHost | `ReactNativeHost` | null | An instance of [ReactNativeHost](https://bit.ly/2ZnwgnA). | +| Property | Type | Default | Description | +| --------- | ----------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| reactHost | `ReactHost` | null | An instance of [ReactHost](https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactHost.kt). | --- @@ -142,16 +142,16 @@ ReactNativeBrownfield.shared `createView` -Creates a React Native view with a given module name. It automatically uses an instance of React Native created in `startReactNative` method. This is useful when embedding React Native views directly in your native layouts or Jetpack Compose UI. +Creates a React Native view with a given module name. It automatically uses an instance of React Native created in `initialize` method. This is useful when embedding React Native views directly in your native layouts or Jetpack Compose UI. Params: -| Param | Required | Type | Description | -| -------------- | -------- | ------------------- | ----------------------------------------------------------- | -| context | Yes | `Context` | Android context to create the view | -| activity | No | `FragmentActivity` | Activity hosting the view, used for lifecycle management | -| moduleName | Yes | `String` | Name of React Native component registered to `AppRegistry` | -| launchOptions | No | `Bundle` | Initial properties to be passed to React Native component | +| Param | Required | Type | Description | +| ------------- | -------- | ------------------ | ---------------------------------------------------------- | +| context | Yes | `Context` | Android context to create the view | +| activity | No | `FragmentActivity` | Activity hosting the view, used for lifecycle management | +| moduleName | Yes | `String` | Name of React Native component registered to `AppRegistry` | +| launchOptions | No | `Bundle` | Initial properties to be passed to React Native component | Returns: `FrameLayout` - A view containing the React Native component. @@ -182,7 +182,7 @@ AndroidView( #### `ReactNativeFragment` -An fragment rendering `ReactRootView` with a given module name. It automatically uses a instance of React Native created in `startReactNative` method. It works well with exposed JavaScript module. All the lifecycles are proxied to `ReactInstanceManager`. It's the simplest way to embed React Native into your navigation stack. +An fragment rendering `ReactRootView` with a given module name. It automatically uses a instance of React Native created in `initialize` method. It works well with exposed JavaScript module. All the lifecycles are proxied to `ReactInstanceManager`. It's the simplest way to embed React Native into your navigation stack. ```kotlin import com.callstack.reactnativebrownfield.ReactNativeFragment @@ -198,10 +198,10 @@ Creates a Fragment with `ReactNativeActivity`, you can use it as a parameter in Params: -| Param | Required | Type | Description | -| ----------------------- | -------- | ------------------------------------------- | ----------------------------------------------------------- | -| moduleName | Yes | `String` | Name of React Native component registered to `AppRegistry`. | -| initialProps | No | `Bundle` \|\| `HashMap` \|\| `ReadableMap` | Initial properties to be passed to React Native component. | +| Param | Required | Type | Description | +| ------------ | -------- | ----------------------------------------------------- | ----------------------------------------------------------- | +| moduleName | Yes | `String` | Name of React Native component registered to `AppRegistry`. | +| initialProps | No | `Bundle` \|\| `HashMap` \|\| `ReadableMap` | Initial properties to be passed to React Native component. | Examples: @@ -229,6 +229,25 @@ map.putInt("score", 12) ReactNativeFragment.createReactNativeFragment("ReactNative", map) ``` +### Usage in Jetpack Compose + +You can easily wrap the `ReactNativeFragment` inside a `AndroidFragment` composable to integrate React Native into your Jetpack Compose application. Since the AndroidFragment itself acts as a factory for the given Fragment class, you can pass the required arguments using a Bundle. + +The `arguments` passed to the `AndroidFragment` match the ones that can be passed to the `ReactNativeFragment.createReactNativeFragment` factory, yet need to be packed inside a Bundle, as follows: +- ***(required)*** the JS component name, a `String` under the `ReactNativeFragmentArgNames.ARG_MODULE_NAME` (equivalent to `"arg_module_name"`) constant +- *(optional)* the initial properties, a `Bundle` under the `ReactNativeFragmentArgNames.ARG_LAUNCH_OPTIONS` (equivalent to `"arg_launch_options"`) constant + +```kotlin +import androidx.fragment.compose.AndroidFragment + +import com.callstack.reactnativebrownfield.constants.ReactNativeFragmentArgNames + +AndroidFragment(arguments = Bundle().apply { + putString(ReactNativeFragmentArgNames.ARG_MODULE_NAME, "ReactNative") + putBundle(ReactNativeFragmentArgNames.ARG_LAUNCH_OPTIONS, initialProps) +}, modifier = Modifier.fillMaxSize()) +``` + --- ### Example diff --git a/example/index.tsx b/example/App.tsx similarity index 94% rename from example/index.tsx rename to example/App.tsx index c4388644..8c52685f 100644 --- a/example/index.tsx +++ b/example/App.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { AppRegistry, StyleSheet, Text, View, Button } from 'react-native'; +import { StyleSheet, Text, View, Button } from 'react-native'; import { createNativeStackNavigator, type NativeStackScreenProps, @@ -71,7 +71,7 @@ type RootStackParamList = { const Stack = createNativeStackNavigator(); -function App() { +export default function App() { return ( @@ -93,5 +93,3 @@ const styles = StyleSheet.create({ margin: 10, }, }); - -AppRegistry.registerComponent('ReactNative', () => App); diff --git a/example/index.js b/example/index.js new file mode 100644 index 00000000..64110690 --- /dev/null +++ b/example/index.js @@ -0,0 +1,5 @@ +import { AppRegistry } from 'react-native'; + +import App from './App'; + +AppRegistry.registerComponent('ReactNative', () => App);