Quellcode durchsuchen

android: fixing app lifecycle

Daniel Zlotin vor 7 Jahren
Ursprung
Commit
44d783add0

+ 0
- 69
android/app/src/main/java/com/reactnativenavigation/NavigationActivity.java Datei anzeigen

@@ -5,51 +5,16 @@ import android.support.annotation.Nullable;
5 5
 import android.support.v7.app.AppCompatActivity;
6 6
 import android.view.View;
7 7
 
8
-import com.facebook.react.ReactInstanceManager;
9
-import com.facebook.react.ReactNativeHost;
10
-import com.facebook.react.ReactPackage;
11
-import com.facebook.react.bridge.ReactContext;
12 8
 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
13
-import com.facebook.react.shell.MainReactPackage;
14
-import com.reactnativenavigation.react.NavigationEventEmitter;
15
-import com.reactnativenavigation.react.NavigationPackage;
16
-import com.reactnativenavigation.react.ReactDevPermission;
17 9
 import com.reactnativenavigation.views.NavigationSplashView;
18 10
 
19
-import java.util.Arrays;
20
-import java.util.List;
21
-
22 11
 public class NavigationActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {
23
-    private ReactNativeHost host;
24 12
     private View contentView;
25 13
 
26 14
     @Override
27 15
     public void onCreate(Bundle savedInstanceState) {
28 16
         super.onCreate(savedInstanceState);
29 17
         setContentView(new NavigationSplashView(this));
30
-
31
-        host = new ReactNativeHost(getApplication()) {
32
-            @Override
33
-            protected boolean getUseDeveloperSupport() {
34
-                return NavigationApplication.instance.isDebug();
35
-            }
36
-
37
-            @Override
38
-            protected List<ReactPackage> getPackages() {
39
-                return Arrays.asList(
40
-                        new MainReactPackage(),
41
-                        new NavigationPackage(NavigationActivity.this)
42
-                );
43
-            }
44
-        };
45
-
46
-        host.getReactInstanceManager().addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
47
-            @Override
48
-            public void onReactContextInitialized(ReactContext context) {
49
-                // TODO should we implement this line also if context already exists onCreate?
50
-                new NavigationEventEmitter(context).emitAppLaunched();
51
-            }
52
-        });
53 18
     }
54 19
 
55 20
     @Override
@@ -63,42 +28,8 @@ public class NavigationActivity extends AppCompatActivity implements DefaultHard
63 28
         return contentView;
64 29
     }
65 30
 
66
-    @Override
67
-    protected void onResume() {
68
-        super.onResume();
69
-        if (ReactDevPermission.shouldAskPermission()) {
70
-            ReactDevPermission.askPermission(this);
71
-        } else {
72
-            if (!host.getReactInstanceManager().hasStartedCreatingInitialContext()) {
73
-                host.getReactInstanceManager().createReactContextInBackground();
74
-            }
75
-            host.getReactInstanceManager().onHostResume(this, this);
76
-        }
77
-    }
78
-
79
-    @Override
80
-    protected void onPause() {
81
-        super.onPause();
82
-        if (host.getReactInstanceManager().hasStartedCreatingInitialContext()) {
83
-            host.getReactInstanceManager().onHostPause(this);
84
-        }
85
-    }
86
-
87
-    @Override
88
-    protected void onDestroy() {
89
-        super.onDestroy();
90
-        if (host.getReactInstanceManager().hasStartedCreatingInitialContext()) {
91
-            host.getReactInstanceManager().onHostDestroy(this);
92
-        }
93
-        host.clear();
94
-    }
95
-
96 31
     @Override
97 32
     public void invokeDefaultOnBackPressed() {
98 33
         onBackPressed();
99 34
     }
100
-
101
-    public ReactNativeHost getHost() {
102
-        return host;
103
-    }
104 35
 }

+ 104
- 10
android/app/src/main/java/com/reactnativenavigation/NavigationApplication.java Datei anzeigen

@@ -1,26 +1,120 @@
1 1
 package com.reactnativenavigation;
2 2
 
3
+import android.app.Activity;
3 4
 import android.app.Application;
4
-import android.os.Handler;
5
-import android.os.Looper;
5
+import android.os.Bundle;
6 6
 
7
-public abstract class NavigationApplication extends Application {
7
+import com.facebook.react.ReactApplication;
8
+import com.facebook.react.ReactInstanceManager;
9
+import com.facebook.react.ReactNativeHost;
10
+import com.facebook.react.ReactPackage;
11
+import com.facebook.react.bridge.ReactContext;
12
+import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
13
+import com.facebook.react.shell.MainReactPackage;
14
+import com.reactnativenavigation.react.NavigationEventEmitter;
15
+import com.reactnativenavigation.react.NavigationPackage;
16
+import com.reactnativenavigation.react.ReactDevPermission;
17
+
18
+import java.util.Arrays;
19
+import java.util.List;
20
+import java.util.concurrent.atomic.AtomicBoolean;
21
+
22
+public abstract class NavigationApplication extends Application implements ReactApplication {
8 23
     public static NavigationApplication instance;
9
-    private Handler handler;
24
+    private ReactNativeHost host;
10 25
 
11 26
     @Override
12 27
     public void onCreate() {
13 28
         super.onCreate();
14 29
         instance = this;
15
-        handler = new Handler(Looper.getMainLooper());
16
-    }
30
+        host = new ReactNativeHost(this) {
31
+            @Override
32
+            protected boolean getUseDeveloperSupport() {
33
+                return isDebug();
34
+            }
35
+
36
+            @Override
37
+            protected List<ReactPackage> getPackages() {
38
+                return Arrays.asList(
39
+                        new MainReactPackage(),
40
+                        new NavigationPackage()
41
+                );
42
+            }
43
+        };
44
+
45
+        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
46
+            private AtomicBoolean creating = new AtomicBoolean(true);
47
+
48
+            @Override
49
+            public void onActivityCreated(final Activity activity, Bundle bundle) {
50
+                if (!(activity instanceof NavigationActivity)) return;
51
+                creating.set(true);
52
+            }
53
+
54
+            @Override
55
+            public void onActivityStarted(Activity activity) {
56
+            }
57
+
58
+            @Override
59
+            public void onActivityResumed(Activity activity) {
60
+                if (!(activity instanceof NavigationActivity)) return;
61
+
62
+                if (ReactDevPermission.shouldAskPermission()) {
63
+                    ReactDevPermission.askPermission(activity);
64
+                    return;
65
+                }
17 66
 
18
-    public void runOnUiThread(Runnable runnable) {
19
-        handler.post(runnable);
67
+                if (!host.getReactInstanceManager().hasStartedCreatingInitialContext()) {
68
+                    host.getReactInstanceManager().addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
69
+                        @Override
70
+                        public void onReactContextInitialized(ReactContext context) {
71
+                            host.getReactInstanceManager().removeReactInstanceEventListener(this);
72
+                            new NavigationEventEmitter(context).emitAppLaunched();
73
+                        }
74
+                    });
75
+                    host.getReactInstanceManager().createReactContextInBackground();
76
+                    host.getReactInstanceManager().onHostResume(activity, (DefaultHardwareBackBtnHandler) activity);
77
+                    return;
78
+                }
79
+
80
+                host.getReactInstanceManager().onHostResume(activity, (DefaultHardwareBackBtnHandler) activity);
81
+
82
+                if (creating.compareAndSet(true, false)) {
83
+                    new NavigationEventEmitter(host.getReactInstanceManager().getCurrentReactContext()).emitAppLaunched();
84
+                }
85
+            }
86
+
87
+            @Override
88
+            public void onActivityPaused(Activity activity) {
89
+                if (!(activity instanceof NavigationActivity)) return;
90
+
91
+                if (host.getReactInstanceManager().hasStartedCreatingInitialContext()) {
92
+                    host.getReactInstanceManager().onHostPause(activity);
93
+                }
94
+            }
95
+
96
+            @Override
97
+            public void onActivityStopped(Activity activity) {
98
+            }
99
+
100
+            @Override
101
+            public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
102
+            }
103
+
104
+            @Override
105
+            public void onActivityDestroyed(Activity activity) {
106
+                if (!(activity instanceof NavigationActivity)) return;
107
+
108
+                if (host.getReactInstanceManager().hasStartedCreatingInitialContext()) {
109
+                    host.getReactInstanceManager().onHostDestroy(activity);
110
+                }
111
+            }
112
+        });
20 113
     }
21 114
 
22
-    public void runOnUiThreadDelayed(Runnable runnable, long delayMillis) {
23
-        handler.postDelayed(runnable, delayMillis);
115
+    @Override
116
+    public ReactNativeHost getReactNativeHost() {
117
+        return host;
24 118
     }
25 119
 
26 120
     public abstract boolean isDebug();

+ 32
- 37
android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java Datei anzeigen

@@ -16,6 +16,7 @@ import com.reactnativenavigation.layout.LayoutFactory;
16 16
 import com.reactnativenavigation.layout.LayoutNode;
17 17
 import com.reactnativenavigation.layout.StackLayout;
18 18
 import com.reactnativenavigation.layout.bottomtabs.BottomTabsCreator;
19
+import com.reactnativenavigation.utils.UiThread;
19 20
 
20 21
 import java.util.ArrayList;
21 22
 import java.util.HashMap;
@@ -23,11 +24,9 @@ import java.util.Map;
23 24
 
24 25
 public class NavigationModule extends ReactContextBaseJavaModule {
25 26
     private static final String NAME = "RNNBridgeModule";
26
-    private final NavigationActivity activity;
27 27
 
28
-    public NavigationModule(ReactApplicationContext context, NavigationActivity activity) {
29
-        super(context);
30
-        this.activity = activity;
28
+    public NavigationModule(ReactApplicationContext reactContext) {
29
+        super(reactContext);
31 30
     }
32 31
 
33 32
     @Override
@@ -37,66 +36,63 @@ public class NavigationModule extends ReactContextBaseJavaModule {
37 36
 
38 37
     @ReactMethod
39 38
     public void setRoot(final ReadableMap layoutTree) {
40
-        NavigationApplication.instance.runOnUiThread(new Runnable() {
39
+        if (getCurrentActivity() == null) return;
40
+
41
+        UiThread.post(new Runnable() {
41 42
             @Override
42 43
             public void run() {
43
-                getActivity().runOnUiThread(new Runnable() {
44
-                    @Override
45
-                    public void run() {
46
-                        LayoutFactory factory =
47
-                                new LayoutFactory(getActivity(), new LayoutFactory.ReactRootViewCreator() {
48
-                                    @Override
49
-                                    public View create(String id, String name) {
50
-                                        ReactRootView rootView = new ReactRootView(getActivity());
51
-                                        Bundle opts = new Bundle();
52
-                                        opts.putString("id", id);
53
-                                        rootView.startReactApplication(getActivity().getHost().getReactInstanceManager(), name, opts);
54
-                                        return rootView;
55
-                                    }
56
-                                }, new BottomTabsCreator());
57
-
58
-                        final LayoutNode layoutTreeRoot = readableMapToLayoutNode(layoutTree);
59
-                        final View rootView = factory.create(layoutTreeRoot);
60
-                        getActivity().setContentView(rootView);
61
-                    }
62
-                });
44
+                LayoutFactory factory =
45
+                        new LayoutFactory(getCurrentActivity(), new LayoutFactory.ReactRootViewCreator() {
46
+                            @Override
47
+                            public View create(String id, String name) {
48
+                                ReactRootView rootView = new ReactRootView(getCurrentActivity());
49
+                                Bundle opts = new Bundle();
50
+                                opts.putString("id", id);
51
+                                rootView.startReactApplication(NavigationApplication.instance.getReactNativeHost().getReactInstanceManager(), name, opts);
52
+                                return rootView;
53
+                            }
54
+                        }, new BottomTabsCreator());
55
+
56
+                final LayoutNode layoutTreeRoot = readableMapToLayoutNode(layoutTree);
57
+                final View rootView = factory.create(layoutTreeRoot);
58
+                getCurrentActivity().setContentView(rootView);
63 59
             }
64 60
         });
65 61
     }
66 62
 
67
-    private NavigationActivity getActivity() {
68
-        return activity;
69
-    }
70
-
71 63
     @ReactMethod
72 64
     public void push(String onContainerId, final ReadableMap layout) {
73
-        getActivity().runOnUiThread(new Runnable() {
65
+        if (getCurrentActivity() == null) return;
66
+
67
+        UiThread.post(new Runnable() {
74 68
             @Override
75 69
             public void run() {
76 70
                 LayoutFactory factory =
77
-                        new LayoutFactory(getActivity(), new LayoutFactory.ReactRootViewCreator() {
71
+                        new LayoutFactory(getCurrentActivity(), new LayoutFactory.ReactRootViewCreator() {
78 72
                             @Override
79 73
                             public View create(String id, String name) {
80
-                                ReactRootView rootView = new ReactRootView(getActivity());
74
+                                ReactRootView rootView = new ReactRootView(getCurrentActivity());
81 75
                                 Bundle opts = new Bundle();
82 76
                                 opts.putString("id", id);
83
-                                rootView.startReactApplication(getActivity().getHost().getReactInstanceManager(), name, opts);
77
+                                rootView.startReactApplication(NavigationApplication.instance.getReactNativeHost().getReactInstanceManager(), name, opts);
84 78
                                 return rootView;
85 79
                             }
86 80
                         }, new BottomTabsCreator());
87 81
                 final LayoutNode layoutNode = readableMapToLayoutNode(layout);
88 82
                 final View rootView = factory.create(layoutNode);
89
-                ((StackLayout) getActivity().getContentView()).push(rootView);
83
+                ((StackLayout) ((NavigationActivity) getCurrentActivity()).getContentView()).push(rootView);
90 84
             }
91 85
         });
92 86
     }
93 87
 
94 88
     @ReactMethod
95 89
     public void pop(String onContainerId) {
96
-        getActivity().runOnUiThread(new Runnable() {
90
+        if (getCurrentActivity() == null) return;
91
+
92
+        UiThread.post(new Runnable() {
97 93
             @Override
98 94
             public void run() {
99
-                ((StackLayout) getActivity().getContentView()).pop();
95
+                ((StackLayout) ((NavigationActivity) getCurrentActivity()).getContentView()).pop();
100 96
             }
101 97
         });
102 98
     }
@@ -132,5 +128,4 @@ public class NavigationModule extends ReactContextBaseJavaModule {
132 128
         }
133 129
         return map;
134 130
     }
135
-
136 131
 }

+ 1
- 8
android/app/src/main/java/com/reactnativenavigation/react/NavigationPackage.java Datei anzeigen

@@ -5,7 +5,6 @@ import com.facebook.react.bridge.JavaScriptModule;
5 5
 import com.facebook.react.bridge.NativeModule;
6 6
 import com.facebook.react.bridge.ReactApplicationContext;
7 7
 import com.facebook.react.uimanager.ViewManager;
8
-import com.reactnativenavigation.NavigationActivity;
9 8
 
10 9
 import java.util.Arrays;
11 10
 import java.util.Collections;
@@ -13,16 +12,10 @@ import java.util.List;
13 12
 
14 13
 public class NavigationPackage implements ReactPackage {
15 14
 
16
-    private final NavigationActivity activity;
17
-
18
-    public NavigationPackage(NavigationActivity activity) {
19
-        this.activity = activity;
20
-    }
21
-
22 15
     @Override
23 16
     public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
24 17
         return Arrays.<NativeModule>asList(
25
-                new NavigationModule(reactContext, activity)
18
+                new NavigationModule(reactContext)
26 19
         );
27 20
     }
28 21
 

+ 16
- 0
android/app/src/main/java/com/reactnativenavigation/utils/UiThread.java Datei anzeigen

@@ -0,0 +1,16 @@
1
+package com.reactnativenavigation.utils;
2
+
3
+import android.os.Handler;
4
+import android.os.Looper;
5
+
6
+public class UiThread {
7
+    private static final Handler handler = new Handler(Looper.getMainLooper());
8
+
9
+    public static void post(Runnable runnable) {
10
+        handler.post(runnable);
11
+    }
12
+
13
+    public static void postDelayed(Runnable runnable, long millis) {
14
+        handler.postDelayed(runnable, millis);
15
+    }
16
+}

+ 24
- 28
playground/android/app/src/androidTest/java/com/reactnativenavigation/playground/ApplicationLifecycleTest.java Datei anzeigen

@@ -11,6 +11,8 @@ import android.support.test.uiautomator.UiSelector;
11 11
 
12 12
 import com.reactnativenavigation.views.NavigationSplashView;
13 13
 
14
+import org.junit.After;
15
+import org.junit.Before;
14 16
 import org.junit.FixMethodOrder;
15 17
 import org.junit.Rule;
16 18
 import org.junit.Test;
@@ -30,26 +32,19 @@ import static org.assertj.core.api.Java6Assertions.assertThat;
30 32
 @FixMethodOrder(value = MethodSorters.NAME_ASCENDING)
31 33
 public class ApplicationLifecycleTest {
32 34
 
33
-    private ReactIdlingResource reactIdlingResource;
35
+    private ReactIdlingResource reactIdlingResource = new ReactIdlingResource();
34 36
 
35 37
     @Rule
36
-    public ActivityTestRule<MainActivity> rule = new ActivityTestRule<MainActivity>(MainActivity.class, false, false) {
37
-        @Override
38
-        protected void afterActivityLaunched() {
39
-            super.afterActivityLaunched();
40
-            reactIdlingResource = new ReactIdlingResource(getActivity());
41
-            Espresso.registerIdlingResources(reactIdlingResource);
42
-        }
38
+    public ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class, false, false);
43 39
 
44
-        @Override
45
-        protected void afterActivityFinished() {
46
-            super.afterActivityFinished();
47
-            Espresso.unregisterIdlingResources(reactIdlingResource);
48
-        }
49
-    };
40
+    @Before
41
+    public void beforeEach() {
42
+        Espresso.registerIdlingResources(reactIdlingResource);
43
+    }
50 44
 
51
-    private void launchActivity() {
52
-        rule.launchActivity(null);
45
+    @After
46
+    public void afterEach() {
47
+        Espresso.unregisterIdlingResources(reactIdlingResource);
53 48
     }
54 49
 
55 50
     private UiDevice uiDevice() {
@@ -67,23 +62,16 @@ public class ApplicationLifecycleTest {
67 62
     }
68 63
 
69 64
     @Test
70
-    public void _1_acceptsOverlayPermissions_ShowsWelcomeScreen() throws Exception {
71
-        launchActivity();
72
-        acceptOverlayPermissionIfNeeded();
73
-        onView(withText("React Native Navigation!")).check(matches(isDisplayed()));
74
-    }
75
-
76
-    @Test
77
-    public void _2_showsSplashOnStartup() throws Exception {
78
-        launchActivity();
65
+    public void _1_ShowSplash_AcceptsOverlayPermissions_ShowsWelcomeScreen() throws Exception {
66
+        rule.launchActivity(null);
79 67
         assertThat(rule.getActivity().getContentView()).isNotNull().isInstanceOf(NavigationSplashView.class);
80 68
         acceptOverlayPermissionIfNeeded();
69
+        onView(withText("React Native Navigation!")).check(matches(isDisplayed()));
81 70
     }
82 71
 
83 72
     @Test
84
-    public void _3_relaunchFromBackground() throws Exception {
85
-        launchActivity();
86
-        acceptOverlayPermissionIfNeeded();
73
+    public void _2_relaunchFromBackground() throws Exception {
74
+        rule.launchActivity(null);
87 75
         onView(withText("React Native Navigation!")).check(matches(isDisplayed()));
88 76
 
89 77
         uiDevice().pressHome();
@@ -92,6 +80,14 @@ public class ApplicationLifecycleTest {
92 80
 
93 81
         onView(withText("React Native Navigation!")).check(matches(isDisplayed()));
94 82
     }
83
+
84
+//    @Test
85
+//    public void _3_relaunchAfterClose() throws Exception {
86
+//        launchActivity();
87
+//        uiDevice().pressBack();
88
+//        launchActivity();
89
+//        onView(withText("React Native Navigation!")).check(matches(isDisplayed()));
90
+//    }
95 91
 }
96 92
 //    xdescribe('android application lifecycle', () => {
97 93
 ////launch, pause, and resume

+ 6
- 8
playground/android/app/src/androidTest/java/com/reactnativenavigation/playground/ReactIdlingResource.java Datei anzeigen

@@ -4,24 +4,22 @@ import android.support.test.espresso.IdlingResource;
4 4
 
5 5
 import com.facebook.react.ReactNativeHost;
6 6
 import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
7
-import com.reactnativenavigation.NavigationActivity;
8 7
 import com.reactnativenavigation.NavigationApplication;
8
+import com.reactnativenavigation.utils.UiThread;
9 9
 
10 10
 import java.util.concurrent.atomic.AtomicBoolean;
11 11
 
12 12
 class ReactIdlingResource implements IdlingResource, NotThreadSafeBridgeIdleDebugListener {
13
-    private final NavigationActivity activity;
14 13
     private ResourceCallback callback;
15 14
     private AtomicBoolean registered = new AtomicBoolean(false);
16 15
     private AtomicBoolean bridgeIdle = new AtomicBoolean(false);
17 16
 
18
-    ReactIdlingResource(NavigationActivity activity) {
19
-        this.activity = activity;
20
-        NavigationApplication.instance.runOnUiThreadDelayed(new Runnable() {
17
+    ReactIdlingResource() {
18
+        UiThread.postDelayed(new Runnable() {
21 19
             @Override
22 20
             public void run() {
23 21
                 if (!isIdleNow()) {
24
-                    NavigationApplication.instance.runOnUiThreadDelayed(this, 10);
22
+                    UiThread.postDelayed(this, 10);
25 23
                 }
26 24
             }
27 25
         }, 10);
@@ -35,7 +33,7 @@ class ReactIdlingResource implements IdlingResource, NotThreadSafeBridgeIdleDebu
35 33
 
36 34
     @Override
37 35
     public boolean isIdleNow() {
38
-        ReactNativeHost host = activity.getHost();
36
+        ReactNativeHost host = NavigationApplication.instance.getReactNativeHost();
39 37
         boolean hasContext = host != null
40 38
                 && host.getReactInstanceManager() != null
41 39
                 && host.getReactInstanceManager().getCurrentReactContext() != null;
@@ -48,7 +46,7 @@ class ReactIdlingResource implements IdlingResource, NotThreadSafeBridgeIdleDebu
48 46
         }
49 47
 
50 48
         boolean idle = bridgeIdle.get();
51
-        if (idle) {
49
+        if (idle && callback != null) {
52 50
             callback.onTransitionToIdle();
53 51
         }
54 52
         return idle;