Browse Source

Create bottom tabs once (#4478)

Create BottomTabs once when options are applied

The BottomTabs library we use recreates its children each time a style property changes.
This can hinder performance quite a bit as the view creation logic is quite costly.
This commit introduces a simple fix to this issue - we delay view creation until all options are applied.

BottomTabs refactor
* Prevent view recreation in `onSizeChanged`
* Cleanup `createTabs` method
Guy Carmeli 5 years ago
parent
commit
b84a3e5fad
No account linked to committer's email address

+ 1
- 1
lib/android/app/build.gradle View File

@@ -87,7 +87,7 @@ dependencies {
87 87
     implementation 'com.android.support:appcompat-v7:26.1.0'
88 88
     implementation 'com.android.support:support-v4:26.1.0'
89 89
 
90
-    implementation 'com.github.wix-playground:ahbottomnavigation:2.4.6'
90
+    implementation 'com.github.wix-playground:ahbottomnavigation:2.4.8'
91 91
     implementation 'com.github.wix-playground:reflow-animator:1.0.4'
92 92
     implementation 'com.github.clans:fab:1.6.4'
93 93
 

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/presentation/BottomTabPresenter.java View File

@@ -45,7 +45,7 @@ public class BottomTabPresenter {
45 45
         this.bottomTabs = bottomTabs;
46 46
     }
47 47
 
48
-    public void present() {
48
+    public void applyOptions() {
49 49
         for (int i = 0; i < tabs.size(); i++) {
50 50
             BottomTabOptions tab = tabs.get(i).options.copy().withDefaultOptions(defaultOptions).bottomTabOptions;
51 51
             bottomTabs.setBadge(i, tab.badge.get(""));

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/presentation/BottomTabsPresenter.java View File

@@ -52,7 +52,7 @@ public class BottomTabsPresenter {
52 52
         mergeBottomTabsOptions(options.bottomTabsOptions, options.animations);
53 53
     }
54 54
 
55
-    public void present(Options options) {
55
+    public void applyOptions(Options options) {
56 56
         Options withDefaultOptions = options.copy().withDefaultOptions(defaultOptions);
57 57
         applyBottomTabsOptions(withDefaultOptions.bottomTabsOptions, withDefaultOptions.animations);
58 58
     }

+ 10
- 0
lib/android/app/src/main/java/com/reactnativenavigation/utils/ImageLoader.java View File

@@ -33,6 +33,16 @@ public class ImageLoader {
33 33
 
34 34
     private static final String FILE_SCHEME = "file";
35 35
 
36
+    @Nullable
37
+    public Drawable loadIcon(Context context, String uri) {
38
+        try {
39
+            return getDrawable(context, uri);
40
+        } catch (IOException e) {
41
+            e.printStackTrace();
42
+        }
43
+        return null;
44
+    }
45
+
36 46
     public void loadIcon(Context context, String uri, ImagesLoadingListener listener) {
37 47
         try {
38 48
             listener.onComplete(getDrawable(context, uri));

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/utils/Time.java View File

@@ -29,7 +29,7 @@ public class Time {
29 29
 
30 30
     public static void log(String tag) {
31 31
         if (tagsToStartTime.containsKey(tag)) {
32
-            Log.i(tag, "Elapsed: " + (now() - time(tag)));
32
+            Log.i(tag, "Elapsed: " + (now() - time(tag)) + "ms");
33 33
         } else {
34 34
             Log.v(tag, "logging start");
35 35
         }

+ 25
- 45
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java View File

@@ -1,7 +1,6 @@
1 1
 package com.reactnativenavigation.viewcontrollers.bottomtabs;
2 2
 
3 3
 import android.app.Activity;
4
-import android.graphics.drawable.Drawable;
5 4
 import android.support.annotation.NonNull;
6 5
 import android.support.annotation.RestrictTo;
7 6
 import android.view.View;
@@ -18,20 +17,20 @@ import com.reactnativenavigation.presentation.Presenter;
18 17
 import com.reactnativenavigation.react.EventEmitter;
19 18
 import com.reactnativenavigation.utils.CommandListener;
20 19
 import com.reactnativenavigation.utils.ImageLoader;
21
-import com.reactnativenavigation.utils.ImageLoadingListenerAdapter;
22 20
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
23 21
 import com.reactnativenavigation.viewcontrollers.ParentController;
24 22
 import com.reactnativenavigation.viewcontrollers.ViewController;
25 23
 import com.reactnativenavigation.views.BottomTabs;
26 24
 import com.reactnativenavigation.views.Component;
27 25
 
28
-import java.util.ArrayList;
29 26
 import java.util.Collection;
30 27
 import java.util.List;
31 28
 
32 29
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
33 30
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
34 31
 import static android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM;
32
+import static com.reactnativenavigation.utils.CollectionUtils.forEach;
33
+import static com.reactnativenavigation.utils.CollectionUtils.map;
35 34
 
36 35
 public class BottomTabsController extends ParentController implements AHBottomNavigation.OnTabSelectedListener, TabSelector {
37 36
 
@@ -49,6 +48,7 @@ public class BottomTabsController extends ParentController implements AHBottomNa
49 48
         this.imageLoader = imageLoader;
50 49
         this.presenter = bottomTabsPresenter;
51 50
         this.tabPresenter = bottomTabPresenter;
51
+        forEach(tabs, (tab) -> tab.setParentController(this));
52 52
     }
53 53
 
54 54
     @Override
@@ -62,22 +62,30 @@ public class BottomTabsController extends ParentController implements AHBottomNa
62 62
 	@Override
63 63
 	protected ViewGroup createView() {
64 64
 		RelativeLayout root = new RelativeLayout(getActivity());
65
-		bottomTabs = new BottomTabs(getActivity());
65
+		bottomTabs = createBottomTabs();
66 66
         presenter.bindView(bottomTabs, this);
67 67
         tabPresenter.bindView(bottomTabs);
68 68
         bottomTabs.setOnTabSelectedListener(this);
69 69
 		RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
70 70
 		lp.addRule(ALIGN_PARENT_BOTTOM);
71 71
 		root.addView(bottomTabs, lp);
72
-		createTabs(root);
73
-		return root;
72
+		bottomTabs.addItems(createTabs());
73
+        attachTabs(root);
74
+        return root;
74 75
 	}
75 76
 
77
+    @NonNull
78
+    protected BottomTabs createBottomTabs() {
79
+        return new BottomTabs(getActivity());
80
+    }
81
+
76 82
     @Override
77 83
     public void applyOptions(Options options) {
78 84
         super.applyOptions(options);
79
-        presenter.present(options);
80
-        tabPresenter.present();
85
+        bottomTabs.disableItemsCreation();
86
+        presenter.applyOptions(options);
87
+        tabPresenter.applyOptions();
88
+        bottomTabs.enableItemsCreation();
81 89
         this.options.bottomTabsOptions.clearOneTimeOptions();
82 90
         this.initialOptions.bottomTabsOptions.clearOneTimeOptions();
83 91
     }
@@ -135,43 +143,15 @@ public class BottomTabsController extends ParentController implements AHBottomNa
135 143
         return false;
136 144
 	}
137 145
 
138
-	private void createTabs(RelativeLayout root) {
139
-		if (tabs.size() > 5) {
140
-			throw new RuntimeException("Too many tabs!");
141
-		}
142
-        List<String> icons = new ArrayList<>();
143
-        List<BottomTabOptions> bottomTabOptionsList = new ArrayList<>();
144
-        for (int i = 0; i < tabs.size(); i++) {
145
-            tabs.get(i).setParentController(this);
146
-            BottomTabOptions tabOptions = tabs.get(i).resolveCurrentOptions().bottomTabOptions;
147
-            if (!tabOptions.icon.hasValue()) {
148
-                throw new RuntimeException("BottomTab must have an icon");
149
-            }
150
-            bottomTabOptionsList.add(tabOptions);
151
-            icons.add(tabOptions.icon.get());
152
-        }
153
-
154
-        imageLoader.loadIcons(getActivity(), icons, new ImageLoadingListenerAdapter() {
155
-
156
-            @Override
157
-            public void onComplete(@NonNull List<Drawable> drawables) {
158
-                List<AHBottomNavigationItem> tabs = new ArrayList<>();
159
-                for (int i = 0; i < drawables.size(); i++) {
160
-                    tabs.add(new AHBottomNavigationItem(bottomTabOptionsList.get(i).text.get(""), drawables.get(i)));
161
-                }
162
-                bottomTabs.addItems(tabs);
163
-                bottomTabs.post(() -> {
164
-                    for (int i = 0; i < bottomTabOptionsList.size(); i++) {
165
-                        bottomTabs.setTabTestId(i, bottomTabOptionsList.get(i).testId);
166
-                    }
167
-                });
168
-                attachTabs(root);
169
-            }
170
-
171
-            @Override
172
-            public void onError(Throwable error) {
173
-                error.printStackTrace();
174
-            }
146
+	private List<AHBottomNavigationItem> createTabs() {
147
+		if (tabs.size() > 5) throw new RuntimeException("Too many tabs!");
148
+        return map(tabs, tab -> {
149
+            BottomTabOptions options = tab.resolveCurrentOptions().bottomTabOptions;
150
+            return new AHBottomNavigationItem(
151
+                    options.text.get(""),
152
+                    imageLoader.loadIcon(getActivity(), options.icon.get()),
153
+                    options.testId.get("")
154
+            );
175 155
         });
176 156
 	}
177 157
 

+ 28
- 10
lib/android/app/src/main/java/com/reactnativenavigation/views/BottomTabs.java View File

@@ -8,26 +8,44 @@ import android.support.annotation.IntRange;
8 8
 
9 9
 import com.aurelhubert.ahbottomnavigation.AHBottomNavigation;
10 10
 import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem;
11
-import com.reactnativenavigation.BuildConfig;
12
-import com.reactnativenavigation.parse.params.Text;
13 11
 import com.reactnativenavigation.utils.CompatUtils;
14 12
 
15
-import static com.reactnativenavigation.utils.ObjectUtils.perform;
16
-
17 13
 @SuppressLint("ViewConstructor")
18 14
 public class BottomTabs extends AHBottomNavigation {
15
+    private boolean itemsCreationEnabled = true;
16
+    private boolean shouldCreateItems = true;
17
+
18
+    public void disableItemsCreation() {
19
+        itemsCreationEnabled = false;
20
+    }
21
+
22
+    public void enableItemsCreation() {
23
+        itemsCreationEnabled = true;
24
+        if (shouldCreateItems) createItems();
25
+    }
26
+
19 27
     public BottomTabs(Context context) {
20 28
         super(context);
21 29
         setId(CompatUtils.generateViewId());
22 30
         setContentDescription("BottomTabs");
23 31
     }
24 32
 
25
-    public void setTabTestId(int index, Text testId) {
26
-        if (!testId.hasValue() ) return;
27
-        perform(getViewAtPosition(index), view -> {
28
-            view.setTag(testId.get());
29
-            if (BuildConfig.DEBUG) view.setContentDescription(testId.get());
30
-        });
33
+    @Override
34
+    protected void createItems() {
35
+        if (itemsCreationEnabled) {
36
+            superCreateItems();
37
+        } else {
38
+            shouldCreateItems = true;
39
+        }
40
+    }
41
+
42
+    @Override
43
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
44
+
45
+    }
46
+
47
+    public void superCreateItems() {
48
+        super.createItems();
31 49
     }
32 50
 
33 51
     public void setBadge(int bottomTabIndex, String badge) {

+ 1
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabPresenterTest.java View File

@@ -51,7 +51,7 @@ public class BottomTabPresenterTest extends BaseTest {
51 51
 
52 52
     @Test
53 53
     public void present() {
54
-        uut.present();
54
+        uut.applyOptions();
55 55
         for (int i = 0; i < tabs.size(); i++) {
56 56
             verify(bottomTabs, times(1)).setBadge(i, tabs.get(i).options.bottomTabOptions.badge.get(""));
57 57
             verify(bottomTabs, times(1)).setTitleInactiveColor(i, tabs.get(i).options.bottomTabOptions.textColor.get(null));

+ 38
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsControllerTest.java View File

@@ -51,6 +51,7 @@ import static org.mockito.Mockito.when;
51 51
 public class BottomTabsControllerTest extends BaseTest {
52 52
 
53 53
     private Activity activity;
54
+    private BottomTabs bottomTabs;
54 55
     private BottomTabsController uut;
55 56
     private Options initialOptions = new Options();
56 57
     private ViewController child1;
@@ -69,6 +70,12 @@ public class BottomTabsControllerTest extends BaseTest {
69 70
     @Override
70 71
     public void beforeEach() {
71 72
         activity = newActivity();
73
+        bottomTabs = spy(new BottomTabs(activity) {
74
+            @Override
75
+            public void superCreateItems() {
76
+
77
+            }
78
+        });
72 79
         childRegistry = new ChildControllersRegistry();
73 80
         eventEmitter = Mockito.mock(EventEmitter.class);
74 81
 
@@ -97,6 +104,14 @@ public class BottomTabsControllerTest extends BaseTest {
97 104
         createBottomTabs();
98 105
     }
99 106
 
107
+    @Test
108
+    public void parentControllerIsSet() {
109
+        uut = createBottomTabs();
110
+        for (ViewController tab : tabs) {
111
+            assertThat(tab.getParentController()).isEqualTo(uut);
112
+        }
113
+    }
114
+
100 115
     @Test
101 116
     public void setTabs_allChildViewsAreAttachedToHierarchy() {
102 117
         uut.onViewAppeared();
@@ -180,6 +195,12 @@ public class BottomTabsControllerTest extends BaseTest {
180 195
         assertThat(optionsCaptor.getValue().bottomTabsOptions.backgroundColor.hasValue()).isFalse();
181 196
     }
182 197
 
198
+    @Test
199
+    public void applyOptions_bottomTabsCreateViewOnlyOnce() {
200
+        verify(presenter).applyOptions(any());
201
+        verify(bottomTabs, times(2)).superCreateItems(); // first time when view is created, second time when options are applied
202
+    }
203
+
183 204
     @Test
184 205
     public void mergeOptions_currentTabIndex() {
185 206
         uut.ensureViewIsCreated();
@@ -236,6 +257,17 @@ public class BottomTabsControllerTest extends BaseTest {
236 257
             public Options resolveCurrentOptions() {
237 258
                 return resolvedOptions;
238 259
             }
260
+
261
+            @NonNull
262
+            @Override
263
+            protected BottomTabs createBottomTabs() {
264
+                return new BottomTabs(activity) {
265
+                    @Override
266
+                    protected void createItems() {
267
+
268
+                    }
269
+                };
270
+            }
239 271
         };
240 272
 
241 273
         activity.setContentView(uut.getView());
@@ -347,6 +379,12 @@ public class BottomTabsControllerTest extends BaseTest {
347 379
                 uut.getView().layout(0, 0, 1000, 1000);
348 380
                 uut.getBottomTabs().layout(0, 0, 1000, 100);
349 381
             }
382
+
383
+            @NonNull
384
+            @Override
385
+            protected BottomTabs createBottomTabs() {
386
+                return bottomTabs;
387
+            }
350 388
         };
351 389
     }
352 390
 }

+ 13
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java View File

@@ -31,6 +31,7 @@ import com.reactnativenavigation.viewcontrollers.ViewController;
31 31
 import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
32 32
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
33 33
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
34
+import com.reactnativenavigation.views.BottomTabs;
34 35
 
35 36
 import org.junit.Test;
36 37
 import org.mockito.Mockito;
@@ -347,7 +348,18 @@ public class NavigatorTest extends BaseTest {
347 348
 
348 349
     @NonNull
349 350
     private BottomTabsController newTabs(List<ViewController> tabs) {
350
-        return new BottomTabsController(activity, tabs, childRegistry, eventEmitter, imageLoaderMock, "tabsController", new Options(), new Presenter(activity, new Options()), new BottomTabsPresenter(tabs, new Options()), new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new Options()));
351
+        return new BottomTabsController(activity, tabs, childRegistry, eventEmitter, imageLoaderMock, "tabsController", new Options(), new Presenter(activity, new Options()), new BottomTabsPresenter(tabs, new Options()), new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new Options())) {
352
+            @NonNull
353
+            @Override
354
+            protected BottomTabs createBottomTabs() {
355
+                return new BottomTabs(activity) {
356
+                    @Override
357
+                    public void superCreateItems() {
358
+
359
+                    }
360
+                };
361
+            }
362
+        };
351 363
     }
352 364
 
353 365
     @Test