ReactNative inside Android Fragment

今天幫同事解決了一個在Fragment裏面使用RN頁面的需求,記錄一下.

ReactFragment

package com.xxx;

import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactRootView;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionListener;

public class MyFragment extends Fragment {

    public static final String ARG_COMPONENT_NAME = "arg_component_name";
    public static final String ARG_LAUNCH_OPTIONS = "arg_launch_options";

    private static MyFragment newInstance(@NonNull String componentName, Bundle launchOptions) {
        MyFragment fragment = new MyFragment();
        Bundle args = new Bundle();
        args.putString(ARG_COMPONENT_NAME, componentName);
        args.putBundle(ARG_LAUNCH_OPTIONS, launchOptions);
        fragment.setArguments(args);
        return fragment;
    }

    private String mComponentName;
    private Bundle mLaunchOptions;

    private ReactRootView mReactRootView;

    @Nullable
    private DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;

    @Nullable
    private PermissionListener mPermissionListener;

    // region Lifecycle

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mComponentName =getArguments().getString(ARG_COMPONENT_NAME);
            mLaunchOptions = getArguments().getBundle(ARG_LAUNCH_OPTIONS);
        }
        if (mComponentName == null) {
            throw new IllegalStateException("Cannot loadApp if component name is null");
        }
        mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        mReactRootView = new ReactRootView(getContext());
        mReactRootView.startReactApplication(
                getReactNativeHost().getReactInstanceManager(),
                mComponentName,
                mLaunchOptions);
        return mReactRootView;
    }

    @Override
    public void onResume() {
        super.onResume();
        if (getReactNativeHost().hasInstance()) {      getReactNativeHost().getReactInstanceManager().onHostResume(getActivity(), (DefaultHardwareBackBtnHandler) getActivity());
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (getReactNativeHost().hasInstance()) {
            getReactNativeHost().getReactInstanceManager().onHostPause(getActivity());
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mReactRootView != null) {
            mReactRootView.unmountReactApplication();
            mReactRootView = null;
        }
        if (getReactNativeHost().hasInstance()) {
            ReactInstanceManager reactInstanceMgr = getReactNativeHost().getReactInstanceManager();
            if (reactInstanceMgr.getLifecycleState() != LifecycleState.RESUMED) {
                reactInstanceMgr.onHostDestroy(getActivity());
                getReactNativeHost().clear();
            }
        }
    }

    // endregion

    @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;
        }
    }

    // endregion

    // region Helpers

    /**
     * Helper to forward hardware back presses to our React Native Host
     */
    public void onBackPressed() {
        if (getReactNativeHost().hasInstance()) {
            getReactNativeHost().getReactInstanceManager().onBackPressed();
        }
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (getReactNativeHost().hasInstance()) {
            getReactNativeHost().getReactInstanceManager().onActivityResult(getActivity(), requestCode, resultCode, data);
        }
    }

    public void onNewIntent(Intent intent) {
        if (getReactNativeHost().hasInstance()) {
            getReactNativeHost().getReactInstanceManager().onNewIntent(intent);
        }
    }

    /**
     * Helper to forward onKeyUp commands from our host Activity.
     * This allows ReactFragment to handle double tap reloads and dev menus
     *
     * @param keyCode keyCode
     * @param event   event
     * @return true if we handled onKeyUp
     */
    @SuppressWarnings("unused")
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        boolean handled = false;
        if (getReactNativeHost().getUseDeveloperSupport() && getReactNativeHost().hasInstance()) {
            if (keyCode == KeyEvent.KEYCODE_MENU) {
                getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
                handled = true;
            }
            boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer).didDoubleTapR(keyCode, getActivity().getCurrentFocus());
            if (didDoubleTapR) {
                getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
                handled = true;
            }
        }
        return handled;
    }

    // endregion

    /**
     * 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.
     */
    protected ReactNativeHost getReactNativeHost() {
        return ((MainApplication) getActivity().getApplication()).getReactNativeHost();
    }

    /**
     * Builder class to help instantiate a {@link MyFragment}
     */
    public static class Builder {

        private final String mComponentName;
        private Bundle mLaunchOptions;

        /**
         * Returns new Builder for creating a {@link MyFragment}
         *
         * @param componentName The name of your React Native component
         */
        public Builder(String componentName) {
            mComponentName = componentName;
        }

        /**
         * Set the Launch Options for our React Native instance.
         *
         * @param launchOptions launchOptions
         * @return Builder
         */
        public Builder setLaunchOptions(Bundle launchOptions) {
            mLaunchOptions = launchOptions;
            return this;
        }

        public MyFragment build() {
            return MyFragment.newInstance(mComponentName, mLaunchOptions);
        }

    }
}

Activity 你自己的任意FragmentActivity(你可能想繼承自己的BaseFragmentActivity,而不是去繼承ReactActivity or ReactFragmentActivity )

package com.xxx;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionListener;

public class MainActivity extends FragmentActivity implements DefaultHardwareBackBtnHandler{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getSupportFragmentManager().beginTransaction().add(R.id.f1,new MyFragment.Builder("DvaDemo").build()).commit();

    }
    
    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }

}

activity_main佈局

    <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/f1"
        android:layout_width="match_parent"
        android:layout_weight="1"
        android:layout_height="0dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_weight="1"
        android:layout_height="0dp"
        />
</LinearLayout>

效果(上半部分是RN頁面,下半部分是原生頁面)在這裏插入圖片描述

這裏只是證明了可行性,沒有深度使用,感覺實際使用過程中可能遇到一些問題,比如RN頁面有複雜跳轉邏輯,原生也有一些頁面跳轉、切換邏輯,那麼系統返回鍵在某些情況下可能需要特殊處理,又或者RN頁面中有requestPermission,那麼callback可能也要特殊處理(我們的Activity(or Fragment?)去繼承PermissionAwareActivity做一些特殊處理?)。因爲沒有深度使用,所以不確定會遇到哪些問題,但是所有問題應該都是可以解決的。

參考:
https://stackoverflow.com/questions/35221447/react-native-inside-a-fragment
https://medium.com/@pradeet/react-native-fragments-182d35459ca1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章