From 044889821399fe03d64f25b5cd7072979163576f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Guzm=C3=A1n?= Date: Tue, 10 Jun 2025 07:46:51 -0700 Subject: [PATCH] Migrate `ReactFragment` to Kotlin (#51756) Summary: Migrate com.facebook.react.ReactFragment to Kotlin. ## Changelog: [INTERNAL] - Migrate com.facebook.react.ReactFragment to Kotlin Pull Request resolved: https://github.com/facebook/react-native/pull/51756 Test Plan: ```bash yarn test-android yarn android ``` Reviewed By: javache Differential Revision: D76255248 Pulled By: cortinico fbshipit-source-id: 67d289f27f317da10cc68df568cfbf183978c0b4 --- .../ReactAndroid/api/ReactAndroid.api | 25 +- .../com/facebook/react/ReactDelegate.java | 10 +- .../com/facebook/react/ReactFragment.java | 270 ------------------ .../java/com/facebook/react/ReactFragment.kt | 232 +++++++++++++++ 4 files changed, 255 insertions(+), 282 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index e41d70d996b..7006bef17c2 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -186,11 +186,12 @@ public class com/facebook/react/ReactFragment : androidx/fragment/app/Fragment, protected static final field ARG_DISABLE_HOST_LIFECYCLE_EVENTS Ljava/lang/String; protected static final field ARG_FABRIC_ENABLED Ljava/lang/String; protected static final field ARG_LAUNCH_OPTIONS Ljava/lang/String; - protected field mReactDelegate Lcom/facebook/react/ReactDelegate; + public static final field Companion Lcom/facebook/react/ReactFragment$Companion; + protected field reactDelegate Lcom/facebook/react/ReactDelegate; public fun ()V public fun checkPermission (Ljava/lang/String;II)I public fun checkSelfPermission (Ljava/lang/String;)I - protected fun getReactDelegate ()Lcom/facebook/react/ReactDelegate; + protected final fun getReactDelegate ()Lcom/facebook/react/ReactDelegate; protected fun getReactHost ()Lcom/facebook/react/ReactHost; protected fun getReactNativeHost ()Lcom/facebook/react/ReactNativeHost; public fun onActivityResult (IILandroid/content/Intent;)V @@ -203,14 +204,24 @@ public class com/facebook/react/ReactFragment : androidx/fragment/app/Fragment, public fun onRequestPermissionsResult (I[Ljava/lang/String;[I)V public fun onResume ()V public fun requestPermissions ([Ljava/lang/String;ILcom/facebook/react/modules/core/PermissionListener;)V + protected final fun setReactDelegate (Lcom/facebook/react/ReactDelegate;)V } -public class com/facebook/react/ReactFragment$Builder { +public final class com/facebook/react/ReactFragment$Builder { public fun ()V - public fun build ()Lcom/facebook/react/ReactFragment; - public fun setComponentName (Ljava/lang/String;)Lcom/facebook/react/ReactFragment$Builder; - public fun setFabricEnabled (Z)Lcom/facebook/react/ReactFragment$Builder; - public fun setLaunchOptions (Landroid/os/Bundle;)Lcom/facebook/react/ReactFragment$Builder; + public final fun build ()Lcom/facebook/react/ReactFragment; + public final fun getComponentName ()Ljava/lang/String; + public final fun getFabricEnabled ()Z + public final fun getLaunchOptions ()Landroid/os/Bundle; + public final fun setComponentName (Ljava/lang/String;)Lcom/facebook/react/ReactFragment$Builder; + public final fun setComponentName (Ljava/lang/String;)V + public final fun setFabricEnabled (Z)Lcom/facebook/react/ReactFragment$Builder; + public final fun setFabricEnabled (Z)V + public final fun setLaunchOptions (Landroid/os/Bundle;)Lcom/facebook/react/ReactFragment$Builder; + public final fun setLaunchOptions (Landroid/os/Bundle;)V +} + +public final class com/facebook/react/ReactFragment$Companion { } public abstract interface class com/facebook/react/ReactHost { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java index 7d49d0acf39..45f51c809e4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java @@ -48,14 +48,13 @@ public class ReactDelegate { private boolean mFabricEnabled = ReactNativeNewArchitectureFeatureFlags.enableFabricRenderer(); /** - * Do not use this constructor as it's not accounting for New Architecture at all. You should - * either use {@link ReactDelegate#ReactDelegate(Activity, ReactHost, String, Bundle)} if you're - * on bridgeless mode or {@link ReactDelegate#ReactDelegate(Activity, ReactNativeHost, String, - * Bundle, boolean)} and use the last parameter to toggle paper/fabric. + * Do not use this constructor as it's not accounting for New Architecture at all. You should use + * {@link ReactDelegate#ReactDelegate(Activity, ReactNativeHost, String, Bundle, boolean)} as it's + * the constructor used for New Architecture. * * @deprecated Use one of the other constructors instead to account for New Architecture. */ - @Deprecated + @Deprecated(since = "0.75.0") public ReactDelegate( Activity activity, ReactNativeHost reactNativeHost, @@ -80,6 +79,7 @@ public class ReactDelegate { mReactHost = reactHost; } + @Deprecated(since = "0.81.0") public ReactDelegate( Activity activity, ReactNativeHost reactNativeHost, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java deleted file mode 100644 index 4d2204df6b7..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags; -import com.facebook.react.modules.core.PermissionAwareActivity; -import com.facebook.react.modules.core.PermissionListener; - -/** - * Fragment for creating a React View. This allows the developer to "embed" a React Application - * inside native components such as a Drawer, ViewPager, etc. - */ -public class ReactFragment extends Fragment implements PermissionAwareActivity { - - protected static final String ARG_COMPONENT_NAME = "arg_component_name"; - protected static final String ARG_LAUNCH_OPTIONS = "arg_launch_options"; - protected static final String ARG_FABRIC_ENABLED = "arg_fabric_enabled"; - - /** - * @deprecated We will remove this and use a different solution for handling Fragment lifecycle - * events - */ - @Deprecated - protected static final String ARG_DISABLE_HOST_LIFECYCLE_EVENTS = - "arg_disable_host_lifecycle_events"; - - protected ReactDelegate mReactDelegate; - - private boolean mDisableHostLifecycleEvents; - - @Nullable private PermissionListener mPermissionListener; - - public ReactFragment() { - // Required empty public constructor - } - - /** - * @param componentName The name of the react native component - * @param fabricEnabled Flag to enable Fabric for ReactFragment - * @return A new instance of fragment ReactFragment. - */ - private static ReactFragment newInstance( - String componentName, Bundle launchOptions, Boolean fabricEnabled) { - ReactFragment fragment = new ReactFragment(); - Bundle args = new Bundle(); - args.putString(ARG_COMPONENT_NAME, componentName); - args.putBundle(ARG_LAUNCH_OPTIONS, launchOptions); - args.putBoolean(ARG_FABRIC_ENABLED, fabricEnabled); - fragment.setArguments(args); - return fragment; - } - - // region Lifecycle - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - String mainComponentName = null; - Bundle launchOptions = null; - Boolean fabricEnabled = null; - if (getArguments() != null) { - mainComponentName = getArguments().getString(ARG_COMPONENT_NAME); - launchOptions = getArguments().getBundle(ARG_LAUNCH_OPTIONS); - fabricEnabled = getArguments().getBoolean(ARG_FABRIC_ENABLED); - mDisableHostLifecycleEvents = getArguments().getBoolean(ARG_DISABLE_HOST_LIFECYCLE_EVENTS); - } - if (mainComponentName == null) { - throw new IllegalStateException("Cannot loadApp if component name is null"); - } - if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) { - mReactDelegate = - new ReactDelegate(getActivity(), getReactHost(), mainComponentName, launchOptions); - } else { - mReactDelegate = - new ReactDelegate( - getActivity(), getReactNativeHost(), mainComponentName, launchOptions, fabricEnabled); - } - } - - /** - * Get the {@link ReactNativeHost} used by this app. By default, assumes {@link - * Activity#getApplication()} is an instance of {@link ReactApplication} and calls {@link - * ReactApplication#getReactNativeHost()}. Override this method if your application class does not - * implement {@code ReactApplication} or you simply have a different mechanism for storing a - * {@code ReactNativeHost}, e.g. as a static field somewhere. - */ - @Nullable - protected ReactNativeHost getReactNativeHost() { - ReactApplication application = ((ReactApplication) getActivity().getApplication()); - if (application != null) { - return application.getReactNativeHost(); - } else { - return null; - } - } - - /** - * Get the {@link ReactHost} used by this app. By default, assumes {@link - * Activity#getApplication()} is an instance of {@link ReactApplication} and calls {@link - * ReactApplication#getReactHost()}. Override this method if your application class does not - * implement {@code ReactApplication} or you simply have a different mechanism for storing a - * {@code ReactHost}, e.g. as a static field somewhere. - * - *

If you're using Old Architecture/Bridge Mode, this method should return null as {@link - * ReactHost} is a Bridgeless-only concept. - */ - @Nullable - protected ReactHost getReactHost() { - ReactApplication application = ((ReactApplication) getActivity().getApplication()); - if (application != null) { - return application.getReactHost(); - } else { - return null; - } - } - - protected ReactDelegate getReactDelegate() { - return mReactDelegate; - } - - @Override - public View onCreateView( - @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - mReactDelegate.loadApp(); - return mReactDelegate.getReactRootView(); - } - - @Override - public void onResume() { - super.onResume(); - if (!mDisableHostLifecycleEvents) { - mReactDelegate.onHostResume(); - } - } - - @Override - public void onPause() { - super.onPause(); - if (!mDisableHostLifecycleEvents) { - mReactDelegate.onHostPause(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (!mDisableHostLifecycleEvents) { - mReactDelegate.onHostDestroy(); - } else { - mReactDelegate.unloadApp(); - } - } - - // endregion - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - mReactDelegate.onActivityResult(requestCode, resultCode, data, false); - } - - /** - * Helper to forward hardware back presses to our React Native Host - * - *

This must be called via a forward from your host Activity - */ - public boolean onBackPressed() { - return mReactDelegate.onBackPressed(); - } - - /** - * Helper to forward onKeyUp commands from our host Activity. This allows ReactFragment to handle - * double tap reloads and dev menus - * - *

This must be called via a forward from your host Activity - * - * @param keyCode keyCode - * @param event event - * @return true if we handled onKeyUp - */ - public boolean onKeyUp(int keyCode, KeyEvent event) { - return mReactDelegate.shouldShowDevMenuOrReload(keyCode, event); - } - - @Override - public void onRequestPermissionsResult( - int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (mPermissionListener != null - && mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) { - mPermissionListener = null; - } - } - - @Override - public int checkPermission(String permission, int pid, int uid) { - return getActivity().checkPermission(permission, pid, uid); - } - - @Override - public int checkSelfPermission(String permission) { - return getActivity().checkSelfPermission(permission); - } - - @Override - public void requestPermissions( - String[] permissions, int requestCode, PermissionListener listener) { - mPermissionListener = listener; - requestPermissions(permissions, requestCode); - } - - /** Builder class to help instantiate a ReactFragment */ - public static class Builder { - - @Nullable String mComponentName; - @Nullable Bundle mLaunchOptions; - @Nullable Boolean mFabricEnabled; - - public Builder() { - mComponentName = null; - mLaunchOptions = null; - mFabricEnabled = false; - } - - /** - * Set the Component name for our React Native instance. - * - * @param componentName The name of the component - * @return Builder - */ - public Builder setComponentName(String componentName) { - mComponentName = componentName; - return this; - } - - /** - * Set the Launch Options for our React Native instance. - * - * @param launchOptions launchOptions - * @return Builder - */ - public Builder setLaunchOptions(Bundle launchOptions) { - mLaunchOptions = launchOptions; - return this; - } - - public ReactFragment build() { - return ReactFragment.newInstance(mComponentName, mLaunchOptions, mFabricEnabled); - } - - public Builder setFabricEnabled(boolean fabricEnabled) { - mFabricEnabled = fabricEnabled; - return this; - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.kt new file mode 100644 index 00000000000..16797871849 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.kt @@ -0,0 +1,232 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags +import com.facebook.react.modules.core.PermissionAwareActivity +import com.facebook.react.modules.core.PermissionListener + +/** + * Fragment for creating a React View. This allows the developer to "embed" a React Application + * inside native components such as a Drawer, ViewPager, etc. + */ +public open class ReactFragment : Fragment(), PermissionAwareActivity { + protected lateinit var reactDelegate: ReactDelegate + private var disableHostLifecycleEvents = false + private var permissionListener: PermissionListener? = null + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + var mainComponentName: String? = null + var launchOptions: Bundle? = null + var fabricEnabled = false + arguments?.let { args -> + mainComponentName = args.getString(ARG_COMPONENT_NAME) + launchOptions = args.getBundle(ARG_LAUNCH_OPTIONS) + fabricEnabled = args.getBoolean(ARG_FABRIC_ENABLED) + @Suppress("DEPRECATION") + disableHostLifecycleEvents = args.getBoolean(ARG_DISABLE_HOST_LIFECYCLE_EVENTS) + } + checkNotNull(mainComponentName) { "Cannot loadApp if component name is null" } + + reactDelegate = + if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) { + ReactDelegate(activity, reactHost, mainComponentName, launchOptions) + } else { + @Suppress("DEPRECATION") + ReactDelegate(activity, reactNativeHost, mainComponentName, launchOptions, fabricEnabled) + } + } + + /** + * Get the [ReactNativeHost] used by this app. By default, assumes [Activity.getApplication] is an + * instance of [ReactApplication] and calls [ReactApplication.getReactNativeHost]. Override this + * method if your application class does not implement `ReactApplication` or you simply have a + * different mechanism for storing a `ReactNativeHost`, e.g. as a static field somewhere. + */ + protected open val reactNativeHost: ReactNativeHost? + get() = (activity?.application as ReactApplication?)?.reactNativeHost + + /** + * Get the [ReactHost] used by this app. By default, assumes [Activity.getApplication] is an + * instance of [ReactApplication] and calls [ReactApplication.getReactHost]. Override this method + * if your application class does not implement `ReactApplication` or you simply have a different + * mechanism for storing a `ReactHost`, e.g. as a static field somewhere. + * + * If you're using Old Architecture/Bridge Mode, this method should return null as [ReactHost] is + * a Bridgeless-only concept. + */ + protected open val reactHost: ReactHost? + get() = (activity?.application as ReactApplication?)?.reactHost + + public override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + reactDelegate.loadApp() + return reactDelegate.reactRootView + } + + public override fun onResume() { + super.onResume() + if (!disableHostLifecycleEvents) { + reactDelegate.onHostResume() + } + } + + public override fun onPause() { + super.onPause() + if (!disableHostLifecycleEvents) { + reactDelegate.onHostPause() + } + } + + public override fun onDestroy() { + super.onDestroy() + if (!disableHostLifecycleEvents) { + reactDelegate.onHostDestroy() + } else { + reactDelegate.unloadApp() + } + } + + @Deprecated("Deprecated in Java") + public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + @Suppress("DEPRECATION") super.onActivityResult(requestCode, resultCode, data) + reactDelegate.onActivityResult(requestCode, resultCode, data, false) + } + + /** + * Helper to forward hardware back presses to our React Native Host. + * + * This must be called via a forward from your host Activity. + */ + public open fun onBackPressed(): Boolean = reactDelegate.onBackPressed() + + /** + * Helper to forward onKeyUp commands from our host Activity. This allows [ReactFragment] to + * handle double tap reloads and dev menus. + * + * This must be called via a forward from your host Activity. + * + * @param keyCode keyCode + * @param event event + * @return true if we handled onKeyUp + */ + public open fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean = + reactDelegate.shouldShowDevMenuOrReload(keyCode, event) + + @Deprecated("Deprecated in Java") + public override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + @Suppress("DEPRECATION") + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + permissionListener?.let { + if (it.onRequestPermissionsResult(requestCode, permissions, grantResults)) { + permissionListener = null + } + } + } + + override fun checkPermission(permission: String, pid: Int, uid: Int): Int = + getActivity()?.checkPermission(permission, pid, uid) ?: 0 + + override fun checkSelfPermission(permission: String): Int = + getActivity()?.checkSelfPermission(permission) ?: 0 + + @Suppress("DEPRECATION") + override fun requestPermissions( + permissions: Array, + requestCode: Int, + listener: PermissionListener? + ): Unit { + permissionListener = listener + requestPermissions(permissions, requestCode) + } + + /** Builder class to help instantiate a ReactFragment. */ + public class Builder { + public var componentName: String? = null + public var launchOptions: Bundle? = null + public var fabricEnabled: Boolean = false + + /** + * Set the Component name for our React Native instance. + * + * @param componentName The name of the component + * @return Builder + */ + public fun setComponentName(componentName: String): Builder { + this.componentName = componentName + return this + } + + /** + * Set the Launch Options for our React Native instance. + * + * @param launchOptions launchOptions + * @return Builder + */ + public fun setLaunchOptions(launchOptions: Bundle): Builder { + this.launchOptions = launchOptions + return this + } + + public fun build(): ReactFragment = newInstance(componentName, launchOptions, fabricEnabled) + + @Deprecated( + "You should not change call ReactFragment.setFabricEnabled. Instead enable the NewArchitecture for the whole application with newArchEnabled=true in your gradle.properties file") + public fun setFabricEnabled(fabricEnabled: Boolean): Builder { + this.fabricEnabled = fabricEnabled + return this + } + } + + public companion object { + protected const val ARG_COMPONENT_NAME: String = "arg_component_name" + protected const val ARG_LAUNCH_OPTIONS: String = "arg_launch_options" + protected const val ARG_FABRIC_ENABLED: String = "arg_fabric_enabled" + + @Deprecated( + "We will remove this and use a different solution for handling Fragment lifecycle events.") + protected const val ARG_DISABLE_HOST_LIFECYCLE_EVENTS: String = + "arg_disable_host_lifecycle_events" + + /** + * @param componentName The name of the react native component + * @param launchOptions The launch options for the react native component + * @param fabricEnabled Flag to enable Fabric for ReactFragment + * @return A new instance of fragment ReactFragment. + */ + private fun newInstance( + componentName: String?, + launchOptions: Bundle?, + fabricEnabled: Boolean + ): ReactFragment { + val args = + Bundle().apply { + putString(ARG_COMPONENT_NAME, componentName) + putBundle(ARG_LAUNCH_OPTIONS, launchOptions) + putBoolean(ARG_FABRIC_ENABLED, fabricEnabled) + } + return ReactFragment().apply { setArguments(args) } + } + } +}