0%

How to customize the navigation structure to greatly improve the performance of your ReactNative application

Where is the performance bottleneck of react-navigation

We can know that the best animation performance of ReactNative is LayoutAnimation UIView animation, The best gesture performance is to use NativeEvent. These two solutions point to one direction, that is, to solve the performance problem of ReactNative. We cannot let busy JS threads participate in these calls. However, react-navigation from onPress to navigation.push There are JS calls, so our task today is to customize a navigation architecture to greatly improve the performance of our ReactNative application

Encapsulate NavigationController

Starting from the animation, we need to use the UIView process animation that comes with Native. Then the iOS platform already has a UINavigationController library. We only need to create a UINavigationController on the Android platform.

  • We need to handle the hiding of the TabBar
  • We need to hide the Controller that has been covered by the top page to save memory
  • And we need to handle viewDidAppear and viewDidDisappear lifecycle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// NavigationController
public void popToViewController(final HTRouteController controller, Boolean animated) {
if (childControllerList. size() <= 1) {
return;
}
if (this. lock) {
return;
}
this. lock = true;
final int index = childControllerList. indexOf(controller);
final HTRouteController animatedController = childControllerList. get(childControllerList. size() - 1);
if (index == 0) {
HTRouteTabBarController tabBarController = HTRouteGlobal.nextController(getView(), HTRouteTabBarController.class);
if (tabBarController != null) {
tabBarController. reloadShowTabBar(true);
}
}
controller.getView().setVisibility(View.VISIBLE);
translateAnimation(animatedController, animatedController.getView(), false, animated, new Callback() {
@Override
public void invoke(Object... args) {
controller. viewDidAppear();
animatedController. viewDidDisappear();
int size = childControllerList. size();
List<HTRouteController> willRemoveControllerList = new ArrayList<>();
for (int i = index + 1; i < size; i ++) {
HTRouteController removeController = childControllerList. get(i);
willRemoveControllerList. add(removeController);
}
for (HTRouteController removeController: willRemoveControllerList) {
removeChildController(removeController);
}
lock = false;
}
});
}

Package TabBar

From gesture analysis, we need to avoid JS.onPress, so we use the package of UITabBarController and UITabBar on the iOS platform. We only need to create a UITabBar and UITabBarController on the Android platform.

  • Due to the diversity of TabBar, we just encapsulate a common example, and other projects can inherit and rewrite
  • For convenience, we use the code layout
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // TabBar
    public void reloadData() {
    this. removeAllViews();
    this.initSeparatorLine(this);
    LinearLayout linearLayout = new LinearLayout(HTRouteGlobal. activity);
    this.addView(linearLayout, HTRouteGlobal.matchParent);
    linearLayout.setOrientation(LinearLayout.HORIZONTAL);

    TypedValue typedValue = new TypedValue();
    int imageId = android.R.attr.selectableItemBackground;
    HTRouteGlobal.activity.getTheme().resolveAttribute(imageId, typedValue, true);
    int[] attribute = new int[]{imageId};
    TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(typedValue.resourceId, attribute);

    for (int index = 0; index < delegate. itemCount(); index ++) {
    ...
    linearLayout. addView(relativeLayout);
    reloadItemIndex(index);

    }
    }

Encapsulate TabBarController

  • For the Controller that has been covered by the top page, we need to move it out of the Fragment to save memory
  • And we need to handle viewDidAppear and viewDidDisappear lifecycle
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Override
    public void didItemSelected(int index) {
    HTRouteFragment fragment = modelList.get(index).fragment;

    if (fragmentContainer. getChildCount() > 0) {
    HTRouteFragment lastFragment = (HTRouteFragment) fragmentContainer.getChildAt(0).getTag();
    lastFragment. viewDidDisappear();
    }


    View view = fragment. getView();
    fragmentContainer. removeAllViews();

    RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);

    int bottom = 0;
    if (!TextUtils.isEmpty(HTRouteGlobal.getProp("ro.build.version.emui"))) {
    bottom = 1;
    }
    layoutParams.setMargins(0, 0, 0, bottom);
    fragmentContainer.addView(view, layoutParams);

    fragment. viewDidAppear();

    }

Encapsulate RouteView

The most important point is that we can bind the pages and parameters that need to be redirected to RouteView. When the original click event is received, we can directly jump to the native page without going through JS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//HTRouteViewManager
@ReactProp(name = "routeData")
public void setRouteData(HTRouteView routeView, ReadableMap routeDataMap) {
final Map<String, Object> routeData = routeDataMap == null ? null : routeDataMap.toHashMap();
if (routeData != null && routeData. size() > 0) {
routeView.setClickable(true);
final WeakReference<HTRouteView> weakRouteView = new WeakReference<>(routeView);
routeView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
touchRouteData(weakRouteView. get(), routeData);
}
});
} else {
routeView.setClickable(false);
routeView.setOnClickListener(null);
}
}

All page jump processing

Including push replace presentModal all common processing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//HTRouteViewManager
public static void handlerRouteDataWithController(HTRouteController controller, @Nullable Map<String, Object> routeData) {
...
if (action. equals("push") || action. equals("navigate")) {
...
navigationController.pushViewController(routeController, animated, null);
} else if (action. equals("replace")) {
...
navigationController. replaceViewController(routeController, animated);
} else if (action. equals("pop") || action. equals("back") || action. equals("goBack")) {
navigationController. popViewController(animated);
} else if (action. equals("popToTop") || action. equals("popToRoot")) {
navigationController.popToRootViewControllerAnimated(animated);
} else if (action. equals("present")) {
...
presentView.addView(presentBackgroundView, HTRouteGlobal.matchParent);
translatePresentAnimation(presentBackgroundView, presentNavigationController.getView(), presentEdgeTop, true, presentAnimatedDuration, new Callback() {
@Override
public void invoke(Object... args) {
presentNavigationController. viewDidAppear();
}
});
} else if (action. equals("dismiss")) {
final RelativeLayout presentView = rootPresentViewController();
for (int i = 0; i < presentView. getChildCount(); i ++) {
final ViewGroup presentBackgroundView = (ViewGroup) presentView. getChildAt(i);
View navigationControllerView = presentBackgroundView. getChildAt(0);
final HTRouteNavigationController presentNavigationController = HTRouteGlobal.nextController(navigationControllerView, HTRouteNavigationController.class);
HTRouteController routeController = presentNavigationController. childControllerList. get(0);
if (routeController. componentName. equals(componentName)) {
translatePresentAnimation(presentBackgroundView, presentNavigationController.getView(), presentEdgeTop, false, presentAnimatedDuration, new Callback() {
@Override
public void invoke(Object... args) {
presentView. removeView(presentBackgroundView);
presentNavigationController. viewDidDisappear();
presentNavigationController. dealloc();
}
});
}
}
}
}

Same interface as react-navigation

We need to leave the same interface as react-navigation on the JS side to facilitate switching at any time

JS parameter passing

If function parameters need to be passed between pages, we need to store these variables in global variables, and then retrieve them through the key on the second page, because functions cannot be serialized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// index.js
const encodeRouteData = (routeData, navigation) => {
let reloadRouteData = { ...routeData }
let componentName = reloadRouteData?.componentName
let componentPropList = reloadRouteData?. componentPropList ?? {}
let componentRouteOptionList = reloadRouteData?. componentRouteOptionList ?? {}
componentRouteOptionList = {
...HTRouteManager. defaultRouteOption({ ...routeData, navigation }),
...componentRouteOptionList,
}

if (typeof(componentName) == 'string' && !globalValue.registerList[componentName]) {
return null
}


let id = componentRouteOptionList['id']
if (id == null) {
globalValue.count += 1
id = `${globalValue.count}`
}
componentRouteOptionList['id'] = id

reloadRouteData['componentRouteOptionList'] = componentRouteOptionList

sureComponentItem(id, { componentPropList })

return reloadRouteData
}

const decodeRouteData = (props) => {
let reloadProps = { ... props }
let componentRouteOptionList = reloadProps?. componentRouteOptionList ?? {}
let id = componentRouteOptionList?.id
sureComponentItem(id)

let componentValueList = globalValue. componentList[id] ?? {}
let componentPropList = componentValueList['componentPropList'] ?? {}
reloadProps.componentPropList = componentPropList
return reloadProps
}

JS Lifecycle

We need to receive the viewDidAppear and viewDidDisappear life cycle events sent by the native side, and then call the method of the corresponding component through the id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// index.js
const routeListener = HTRouteEventManagerEmitter. addListener('onHTRouteEventChange', (value) => {
let id = value['id']
let actionName = value['actionName']
let componentRef = globalValue.componentList[id]?.ref
if (componentRef == null) {
return
}
DeviceEventEmitter. emit('onHTRouteEvent', { title: 'controller', value: value })

if (value?. actionName == 'dealloc') {
globalValue.componentList[id].ref = null
return
}


let componentFunction = componentRef[actionName]
if (componentFunction == null) {
return
}
componentFunction. call(componentRef, value)
})

Affiliated

The full code is here https://github.com/hellohublot/react-native-route
I’m hublot, sharing some of my thoughts, I’m too busy with work, it’s inevitable that there will be negligence, please forgive me