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
     implementation 'com.android.support:appcompat-v7:26.1.0'
87
     implementation 'com.android.support:appcompat-v7:26.1.0'
88
     implementation 'com.android.support:support-v4:26.1.0'
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
     implementation 'com.github.wix-playground:reflow-animator:1.0.4'
91
     implementation 'com.github.wix-playground:reflow-animator:1.0.4'
92
     implementation 'com.github.clans:fab:1.6.4'
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
         this.bottomTabs = bottomTabs;
45
         this.bottomTabs = bottomTabs;
46
     }
46
     }
47
 
47
 
48
-    public void present() {
48
+    public void applyOptions() {
49
         for (int i = 0; i < tabs.size(); i++) {
49
         for (int i = 0; i < tabs.size(); i++) {
50
             BottomTabOptions tab = tabs.get(i).options.copy().withDefaultOptions(defaultOptions).bottomTabOptions;
50
             BottomTabOptions tab = tabs.get(i).options.copy().withDefaultOptions(defaultOptions).bottomTabOptions;
51
             bottomTabs.setBadge(i, tab.badge.get(""));
51
             bottomTabs.setBadge(i, tab.badge.get(""));

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

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

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

33
 
33
 
34
     private static final String FILE_SCHEME = "file";
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
     public void loadIcon(Context context, String uri, ImagesLoadingListener listener) {
46
     public void loadIcon(Context context, String uri, ImagesLoadingListener listener) {
37
         try {
47
         try {
38
             listener.onComplete(getDrawable(context, uri));
48
             listener.onComplete(getDrawable(context, uri));

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

29
 
29
 
30
     public static void log(String tag) {
30
     public static void log(String tag) {
31
         if (tagsToStartTime.containsKey(tag)) {
31
         if (tagsToStartTime.containsKey(tag)) {
32
-            Log.i(tag, "Elapsed: " + (now() - time(tag)));
32
+            Log.i(tag, "Elapsed: " + (now() - time(tag)) + "ms");
33
         } else {
33
         } else {
34
             Log.v(tag, "logging start");
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
 package com.reactnativenavigation.viewcontrollers.bottomtabs;
1
 package com.reactnativenavigation.viewcontrollers.bottomtabs;
2
 
2
 
3
 import android.app.Activity;
3
 import android.app.Activity;
4
-import android.graphics.drawable.Drawable;
5
 import android.support.annotation.NonNull;
4
 import android.support.annotation.NonNull;
6
 import android.support.annotation.RestrictTo;
5
 import android.support.annotation.RestrictTo;
7
 import android.view.View;
6
 import android.view.View;
18
 import com.reactnativenavigation.react.EventEmitter;
17
 import com.reactnativenavigation.react.EventEmitter;
19
 import com.reactnativenavigation.utils.CommandListener;
18
 import com.reactnativenavigation.utils.CommandListener;
20
 import com.reactnativenavigation.utils.ImageLoader;
19
 import com.reactnativenavigation.utils.ImageLoader;
21
-import com.reactnativenavigation.utils.ImageLoadingListenerAdapter;
22
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
20
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
23
 import com.reactnativenavigation.viewcontrollers.ParentController;
21
 import com.reactnativenavigation.viewcontrollers.ParentController;
24
 import com.reactnativenavigation.viewcontrollers.ViewController;
22
 import com.reactnativenavigation.viewcontrollers.ViewController;
25
 import com.reactnativenavigation.views.BottomTabs;
23
 import com.reactnativenavigation.views.BottomTabs;
26
 import com.reactnativenavigation.views.Component;
24
 import com.reactnativenavigation.views.Component;
27
 
25
 
28
-import java.util.ArrayList;
29
 import java.util.Collection;
26
 import java.util.Collection;
30
 import java.util.List;
27
 import java.util.List;
31
 
28
 
32
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
29
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
33
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
30
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
34
 import static android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM;
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
 public class BottomTabsController extends ParentController implements AHBottomNavigation.OnTabSelectedListener, TabSelector {
35
 public class BottomTabsController extends ParentController implements AHBottomNavigation.OnTabSelectedListener, TabSelector {
37
 
36
 
49
         this.imageLoader = imageLoader;
48
         this.imageLoader = imageLoader;
50
         this.presenter = bottomTabsPresenter;
49
         this.presenter = bottomTabsPresenter;
51
         this.tabPresenter = bottomTabPresenter;
50
         this.tabPresenter = bottomTabPresenter;
51
+        forEach(tabs, (tab) -> tab.setParentController(this));
52
     }
52
     }
53
 
53
 
54
     @Override
54
     @Override
62
 	@Override
62
 	@Override
63
 	protected ViewGroup createView() {
63
 	protected ViewGroup createView() {
64
 		RelativeLayout root = new RelativeLayout(getActivity());
64
 		RelativeLayout root = new RelativeLayout(getActivity());
65
-		bottomTabs = new BottomTabs(getActivity());
65
+		bottomTabs = createBottomTabs();
66
         presenter.bindView(bottomTabs, this);
66
         presenter.bindView(bottomTabs, this);
67
         tabPresenter.bindView(bottomTabs);
67
         tabPresenter.bindView(bottomTabs);
68
         bottomTabs.setOnTabSelectedListener(this);
68
         bottomTabs.setOnTabSelectedListener(this);
69
 		RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
69
 		RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
70
 		lp.addRule(ALIGN_PARENT_BOTTOM);
70
 		lp.addRule(ALIGN_PARENT_BOTTOM);
71
 		root.addView(bottomTabs, lp);
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
     @Override
82
     @Override
77
     public void applyOptions(Options options) {
83
     public void applyOptions(Options options) {
78
         super.applyOptions(options);
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
         this.options.bottomTabsOptions.clearOneTimeOptions();
89
         this.options.bottomTabsOptions.clearOneTimeOptions();
82
         this.initialOptions.bottomTabsOptions.clearOneTimeOptions();
90
         this.initialOptions.bottomTabsOptions.clearOneTimeOptions();
83
     }
91
     }
135
         return false;
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
 
8
 
9
 import com.aurelhubert.ahbottomnavigation.AHBottomNavigation;
9
 import com.aurelhubert.ahbottomnavigation.AHBottomNavigation;
10
 import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem;
10
 import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem;
11
-import com.reactnativenavigation.BuildConfig;
12
-import com.reactnativenavigation.parse.params.Text;
13
 import com.reactnativenavigation.utils.CompatUtils;
11
 import com.reactnativenavigation.utils.CompatUtils;
14
 
12
 
15
-import static com.reactnativenavigation.utils.ObjectUtils.perform;
16
-
17
 @SuppressLint("ViewConstructor")
13
 @SuppressLint("ViewConstructor")
18
 public class BottomTabs extends AHBottomNavigation {
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
     public BottomTabs(Context context) {
27
     public BottomTabs(Context context) {
20
         super(context);
28
         super(context);
21
         setId(CompatUtils.generateViewId());
29
         setId(CompatUtils.generateViewId());
22
         setContentDescription("BottomTabs");
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
     public void setBadge(int bottomTabIndex, String badge) {
51
     public void setBadge(int bottomTabIndex, String badge) {

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

51
 
51
 
52
     @Test
52
     @Test
53
     public void present() {
53
     public void present() {
54
-        uut.present();
54
+        uut.applyOptions();
55
         for (int i = 0; i < tabs.size(); i++) {
55
         for (int i = 0; i < tabs.size(); i++) {
56
             verify(bottomTabs, times(1)).setBadge(i, tabs.get(i).options.bottomTabOptions.badge.get(""));
56
             verify(bottomTabs, times(1)).setBadge(i, tabs.get(i).options.bottomTabOptions.badge.get(""));
57
             verify(bottomTabs, times(1)).setTitleInactiveColor(i, tabs.get(i).options.bottomTabOptions.textColor.get(null));
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
 public class BottomTabsControllerTest extends BaseTest {
51
 public class BottomTabsControllerTest extends BaseTest {
52
 
52
 
53
     private Activity activity;
53
     private Activity activity;
54
+    private BottomTabs bottomTabs;
54
     private BottomTabsController uut;
55
     private BottomTabsController uut;
55
     private Options initialOptions = new Options();
56
     private Options initialOptions = new Options();
56
     private ViewController child1;
57
     private ViewController child1;
69
     @Override
70
     @Override
70
     public void beforeEach() {
71
     public void beforeEach() {
71
         activity = newActivity();
72
         activity = newActivity();
73
+        bottomTabs = spy(new BottomTabs(activity) {
74
+            @Override
75
+            public void superCreateItems() {
76
+
77
+            }
78
+        });
72
         childRegistry = new ChildControllersRegistry();
79
         childRegistry = new ChildControllersRegistry();
73
         eventEmitter = Mockito.mock(EventEmitter.class);
80
         eventEmitter = Mockito.mock(EventEmitter.class);
74
 
81
 
97
         createBottomTabs();
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
     @Test
115
     @Test
101
     public void setTabs_allChildViewsAreAttachedToHierarchy() {
116
     public void setTabs_allChildViewsAreAttachedToHierarchy() {
102
         uut.onViewAppeared();
117
         uut.onViewAppeared();
180
         assertThat(optionsCaptor.getValue().bottomTabsOptions.backgroundColor.hasValue()).isFalse();
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
     @Test
204
     @Test
184
     public void mergeOptions_currentTabIndex() {
205
     public void mergeOptions_currentTabIndex() {
185
         uut.ensureViewIsCreated();
206
         uut.ensureViewIsCreated();
236
             public Options resolveCurrentOptions() {
257
             public Options resolveCurrentOptions() {
237
                 return resolvedOptions;
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
         activity.setContentView(uut.getView());
273
         activity.setContentView(uut.getView());
347
                 uut.getView().layout(0, 0, 1000, 1000);
379
                 uut.getView().layout(0, 0, 1000, 1000);
348
                 uut.getBottomTabs().layout(0, 0, 1000, 100);
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
 import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
31
 import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
32
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
32
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
33
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
33
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
34
+import com.reactnativenavigation.views.BottomTabs;
34
 
35
 
35
 import org.junit.Test;
36
 import org.junit.Test;
36
 import org.mockito.Mockito;
37
 import org.mockito.Mockito;
347
 
348
 
348
     @NonNull
349
     @NonNull
349
     private BottomTabsController newTabs(List<ViewController> tabs) {
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
     @Test
365
     @Test