Browse Source

Unmount screens before handling bundle reload

When ever bundle was reloaded, all currently registered screens
where recreated since they were still attached to the react context.
This commit adds, in a very hacky way, means to respond to bundle
reload events.
Guy Carmeli 8 years ago
parent
commit
34f09ff4dd

+ 13
- 0
android/app/src/main/java/com/reactnativenavigation/activities/BaseReactActivity.java View File

@@ -1,6 +1,9 @@
1 1
 package com.reactnativenavigation.activities;
2 2
 
3
+import android.content.BroadcastReceiver;
4
+import android.content.Context;
3 5
 import android.content.Intent;
6
+import android.content.IntentFilter;
4 7
 import android.content.res.Configuration;
5 8
 import android.os.Build;
6 9
 import android.os.Bundle;
@@ -26,6 +29,7 @@ import com.facebook.react.bridge.Arguments;
26 29
 import com.facebook.react.bridge.ReadableMap;
27 30
 import com.facebook.react.bridge.WritableMap;
28 31
 import com.facebook.react.common.ReactConstants;
32
+import com.facebook.react.devsupport.DevServerHelper;
29 33
 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
30 34
 import com.facebook.react.shell.MainReactPackage;
31 35
 import com.reactnativenavigation.BuildConfig;
@@ -367,6 +371,15 @@ public abstract class BaseReactActivity extends AppCompatActivity implements Def
367 371
         return super.onKeyUp(keyCode, event);
368 372
     }
369 373
 
374
+    /**
375
+     * Called after bundle was reloaded. This is a good chance to clean up previously connected react views.
376
+     */
377
+    public void onJSBundleReloaded() {
378
+        removeAllReactViews();
379
+    }
380
+
381
+    protected abstract void removeAllReactViews();
382
+
370 383
     @Override
371 384
     public void onBackPressed() {
372 385
         if (getScreenStackSize() > 1) {

+ 7
- 0
android/app/src/main/java/com/reactnativenavigation/activities/BottomTabActivity.java View File

@@ -267,4 +267,11 @@ public class BottomTabActivity extends BaseReactActivity implements AHBottomNavi
267 267
         }
268 268
         this.onTabSelected(0, false);
269 269
     }
270
+
271
+    @Override
272
+    protected void removeAllReactViews() {
273
+        for (ScreenStack screenStack : mScreenStacks) {
274
+            screenStack.removeAllReactViews();
275
+        }
276
+    }
270 277
 }

+ 4
- 0
android/app/src/main/java/com/reactnativenavigation/activities/RootActivity.java View File

@@ -45,4 +45,8 @@ public class RootActivity extends BaseReactActivity {
45 45
         return 0;
46 46
     }
47 47
 
48
+    @Override
49
+    public void removeAllReactViews() {
50
+
51
+    }
48 52
 }

+ 5
- 0
android/app/src/main/java/com/reactnativenavigation/activities/SingleScreenActivity.java View File

@@ -103,4 +103,9 @@ public class SingleScreenActivity extends BaseReactActivity {
103 103
     public int getScreenStackSize() {
104 104
         return mScreenStack.getStackSize();
105 105
     }
106
+
107
+    @Override
108
+    protected void removeAllReactViews() {
109
+        mScreenStack.removeAllReactViews();
110
+    }
106 111
 }

+ 5
- 1
android/app/src/main/java/com/reactnativenavigation/activities/TabActivity.java View File

@@ -1,6 +1,5 @@
1 1
 package com.reactnativenavigation.activities;
2 2
 
3
-import android.support.design.widget.CoordinatorLayout;
4 3
 import android.support.v4.view.ViewPager;
5 4
 import android.view.Menu;
6 5
 import android.view.ViewGroup;
@@ -69,6 +68,11 @@ public class TabActivity extends BaseReactActivity {
69 68
         return ret;
70 69
     }
71 70
 
71
+    @Override
72
+    public void removeAllReactViews() {
73
+
74
+    }
75
+
72 76
     @Override
73 77
     public void push(Screen screen) {
74 78
         super.push(screen);

+ 71
- 0
android/app/src/main/java/com/reactnativenavigation/core/RctManager.java View File

@@ -1,15 +1,21 @@
1 1
 package com.reactnativenavigation.core;
2 2
 
3 3
 import android.app.Application;
4
+import android.util.Log;
4 5
 
5 6
 import com.facebook.react.LifecycleState;
6 7
 import com.facebook.react.ReactInstanceManager;
7 8
 import com.facebook.react.ReactPackage;
9
+import com.facebook.react.bridge.JavaJSExecutor;
8 10
 import com.facebook.react.bridge.ReactContext;
9 11
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
10 12
 import com.facebook.react.bridge.WritableMap;
13
+import com.facebook.react.devsupport.DevSupportManager;
14
+import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler;
11 15
 import com.reactnativenavigation.activities.BaseReactActivity;
12 16
 import com.reactnativenavigation.core.objects.Screen;
17
+import com.reactnativenavigation.utils.ContextProvider;
18
+import com.reactnativenavigation.utils.ReflectionUtils;
13 19
 
14 20
 import static com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
15 21
 
@@ -17,6 +23,7 @@ import static com.facebook.react.modules.core.DeviceEventManagerModule.RCTDevice
17 23
  * Created by guyc on 22/02/16.
18 24
  */
19 25
 public class RctManager {
26
+    private static final String TAG = "RctManager";
20 27
     private static final String KEY_EVENT_ID = "id";
21 28
     private static RctManager sInstance;
22 29
 
@@ -68,9 +75,34 @@ public class RctManager {
68 75
         }
69 76
 
70 77
         mReactManager = builder.build();
78
+        setupDevSupportHandler(mReactManager);
79
+
71 80
         return mReactManager;
72 81
     }
73 82
 
83
+    /**
84
+     * Inject our CustomDevCommandsHandler to {@code reactInstanceManager} so we can detect when the bundle was
85
+     * parsed and is about to load.
86
+     * @param reactInstanceManager
87
+     */
88
+    private void setupDevSupportHandler(ReactInstanceManager reactInstanceManager) {
89
+        final ReactInstanceDevCommandsHandler devInterface = (ReactInstanceDevCommandsHandler)
90
+                ReflectionUtils.getDeclaredField(reactInstanceManager, "mDevInterface");
91
+        if (devInterface == null) {
92
+            Log.e(TAG, "Could not get field: mDevInterface");
93
+            return;
94
+        }
95
+
96
+        // Create customDevCommandsHandler
97
+        CustomDevCommandsHandler customDevCommandsHandler = new CustomDevCommandsHandler(devInterface);
98
+        ReflectionUtils.setField(reactInstanceManager, "mDevInterface", customDevCommandsHandler);
99
+
100
+        // Set customDevCommandsHandler in devSupportManager. Fun =).
101
+        DevSupportManager devSupportManager = (DevSupportManager)
102
+                ReflectionUtils.getDeclaredField(reactInstanceManager, "mDevSupportManager");
103
+        ReflectionUtils.setField(devSupportManager, "mReactInstanceCommandsHandler", customDevCommandsHandler);
104
+    }
105
+
74 106
     public <T extends ReactContextBaseJavaModule> T getNativeModule(Class<T> nativeModuleClass) {
75 107
         if (mReactManager == null || mReactManager.getCurrentReactContext() == null) {
76 108
             return null;
@@ -113,5 +145,44 @@ public class RctManager {
113 145
         mReactManager = null;
114 146
         sInstance = null;
115 147
     }
148
+
149
+    /**
150
+     * Proxy for {@link ReactInstanceDevCommandsHandler} used by {@link DevSupportManager} for requesting React
151
+     * instance recreation. Used to notify {@link BaseReactActivity} that the bundle has been reloaded.
152
+     */
153
+    private static class CustomDevCommandsHandler implements ReactInstanceDevCommandsHandler {
154
+        private ReactInstanceDevCommandsHandler mCommandsHandler;
155
+
156
+        public CustomDevCommandsHandler(ReactInstanceDevCommandsHandler commandsHandler) {
157
+            mCommandsHandler = commandsHandler;
158
+        }
159
+
160
+        @Override
161
+        public void onReloadWithJSDebugger(JavaJSExecutor.Factory proxyExecutorFactory) {
162
+            onJSBundleReloaded();
163
+            mCommandsHandler.onReloadWithJSDebugger(proxyExecutorFactory);
164
+        }
165
+
166
+        @Override
167
+        public void onJSBundleLoadedFromServer() {
168
+            onJSBundleReloaded();
169
+            mCommandsHandler.onJSBundleLoadedFromServer();
170
+        }
171
+
172
+        /**
173
+         * Detach previously added ReactRootViews before handling bundle.
174
+         */
175
+        private void onJSBundleReloaded() {
176
+            BaseReactActivity context = ContextProvider.getActivityContext();
177
+            if (context != null) {
178
+                context.onJSBundleReloaded();
179
+            }
180
+        }
181
+
182
+        @Override
183
+        public void toggleElementInspector() {
184
+            mCommandsHandler.toggleElementInspector();
185
+        }
186
+    }
116 187
 }
117 188
 

+ 0
- 2
android/app/src/main/java/com/reactnativenavigation/utils/IconUtils.java View File

@@ -1,13 +1,11 @@
1 1
 package com.reactnativenavigation.utils;
2 2
 
3 3
 import android.content.Context;
4
-import android.content.res.Resources;
5 4
 import android.graphics.Bitmap;
6 5
 import android.graphics.BitmapFactory;
7 6
 import android.graphics.drawable.BitmapDrawable;
8 7
 import android.graphics.drawable.Drawable;
9 8
 import android.net.Uri;
10
-import android.util.DisplayMetrics;
11 9
 
12 10
 import com.reactnativenavigation.BuildConfig;
13 11
 

+ 25
- 4
android/app/src/main/java/com/reactnativenavigation/utils/ReflectionUtils.java View File

@@ -1,24 +1,45 @@
1 1
 package com.reactnativenavigation.utils;
2 2
 
3 3
 import java.lang.reflect.Field;
4
+import java.lang.reflect.Method;
4 5
 
5 6
 /**
6 7
  * Created by guyc on 14/04/16.
7 8
  */
8 9
 public class ReflectionUtils {
9 10
 
10
-    public static boolean setBooleanField(Object obj, String name, Boolean value) {
11
+    public static boolean setField(Object obj, String name, Object value) {
11 12
         Field field;
12 13
         try {
13 14
             field = obj.getClass().getDeclaredField(name);
14 15
             field.setAccessible(true);
15 16
             field.set(obj, value);
16 17
             return true;
17
-        } catch (NoSuchFieldException e) {
18
-            e.printStackTrace();
19
-        } catch (IllegalAccessException e) {
18
+        } catch (Exception e) {
20 19
             e.printStackTrace();
21 20
         }
22 21
         return false;
23 22
     }
23
+
24
+    public static Object getDeclaredField(Object obj, String fieldName) {
25
+        try {
26
+            Field f = obj.getClass().getDeclaredField(fieldName);
27
+            f.setAccessible(true);
28
+            return f.get(obj); //IllegalAccessException
29
+        } catch (Exception e) {
30
+            e.printStackTrace();
31
+        }
32
+        return null;
33
+    }
34
+
35
+    public static Object invoke(Object object, String methodName) {
36
+        try {
37
+            Method method = object.getClass().getDeclaredMethod(methodName);
38
+            method.setAccessible(true);
39
+            return method.invoke(object);
40
+        } catch (Exception e) {
41
+            e.printStackTrace();
42
+        }
43
+        return null;
44
+    }
24 45
 }

+ 7
- 3
android/app/src/main/java/com/reactnativenavigation/views/RctView.java View File

@@ -67,7 +67,7 @@ public class RctView extends FrameLayout {
67 67
      */
68 68
     public void onTemporallyRemovedFromScreen() {
69 69
         // Hack in order to prevent the react view from getting unmounted
70
-        ReflectionUtils.setBooleanField(mReactRootView, "mAttachScheduled", true);
70
+        ReflectionUtils.setField(mReactRootView, "mAttachScheduled", true);
71 71
     }
72 72
 
73 73
     /**
@@ -75,7 +75,7 @@ public class RctView extends FrameLayout {
75 75
      * executed and componentWillUnmount is called
76 76
      */
77 77
     public void onRemoveFromScreen() {
78
-        ReflectionUtils.setBooleanField(mReactRootView, "mAttachScheduled", false);
78
+        ReflectionUtils.setField(mReactRootView, "mAttachScheduled", false);
79 79
     }
80 80
 
81 81
     /**
@@ -83,7 +83,11 @@ public class RctView extends FrameLayout {
83 83
      * executed and componentWillUnmount is called
84 84
      */
85 85
     public void onReAddToScreen() {
86
-        ReflectionUtils.setBooleanField(mReactRootView, "mAttachScheduled", false);
86
+        ReflectionUtils.setField(mReactRootView, "mAttachScheduled", false);
87
+    }
88
+
89
+    public void detachFromScreen() {
90
+        ReflectionUtils.invoke(mReactRootView, "onDetachedFromWindow");
87 91
     }
88 92
 }
89 93
 

+ 11
- 0
android/app/src/main/java/com/reactnativenavigation/views/ScreenStack.java View File

@@ -165,4 +165,15 @@ public class ScreenStack extends FrameLayout {
165 165
 
166 166
         parent.addView(this, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
167 167
     }
168
+
169
+    public void removeAllReactViews() {
170
+        while (!mStack.empty()) {
171
+            RctView view = mStack.pop().view;
172
+            // Ensure view will be properly detached and unmounted
173
+            view.onRemoveFromScreen();
174
+            // Unmount the view
175
+            view.detachFromScreen();
176
+        }
177
+        removeAllViews();
178
+    }
168 179
 }