Skip to content

Commit a82bb37

Browse files
authored
Merge pull request #12 from okaryo/fix/preload-children
Fix issue where non-preloaded children were loaded when index changed
2 parents d7ac8cd + 280500c commit a82bb37

File tree

3 files changed

+208
-46
lines changed

3 files changed

+208
-46
lines changed

README.md

+9-9
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ A package that extends IndexedStack to allow for lazy loading and provides enhan
77

88
## Motivation
99

10-
If you use the IndexedStack with bottom navigation, all the widgets specified in the children of the IndexedStack will be built.
10+
If you use the IndexedStack with bottom navigation, all the widgets specified in the children of the IndexedStack will be built.
1111

12-
Moreover, if the widget requires API requests or database access, or has a complex UI, the IndexedStack build time will be significant.
12+
Moreover, if the widget requires API requests or database access, or has a complex UI, the IndexedStack build time will be significant.
1313

1414
Therefore, we created an extended IndexedStack that builds the required widget only when it is needed and returns the pre-built widget when it is needed again.
1515

@@ -19,7 +19,7 @@ Therefore, we created an extended IndexedStack that builds the required widget o
1919
* **Auto Disposal**: The `autoDisposeIndexes` parameter allows specific children to be automatically disposed of when they are no longer visible. When these children are accessed again, they will be rebuilt from scratch. This is useful for cases where widgets hold significant state or require resetting when revisited.
2020

2121
## Usage
22-
You can use `LazyLoadIndexedStack` in the same way as `IndexedStack`, with additional options for preloading and auto dispose.
22+
You can use `LazyLoadIndexedStack` in the same way as `IndexedStack`, with additional options for preloading and auto dispose. If an index is included in both `preloadIndexes` and `autoDisposeIndexes`, it will be preloaded initially but disposed when it becomes invisible and rebuilt when accessed again.
2323

2424
### Basic Example
2525
```dart
@@ -37,13 +37,13 @@ class _MainPageState extends State<MainPage> {
3737
home: Scaffold(
3838
body: LazyLoadIndexedStack(
3939
index: _index,
40-
preloadIndexes: const [3],
41-
autoDisposeIndexes: const [1, 2],
40+
preloadIndexes: [1, 2],
41+
autoDisposeIndexes: [2, 3],
4242
children: [
43-
Page1(),
44-
Page2(), // index 1 will be auto dispose
45-
Page3(), // index 2 will also auto dispose
46-
Page4(), // index 3 is preloaded
43+
Page1(), // Load by initial index
44+
Page2(), // Preloaded initially
45+
Page3(), // Preloaded initially but disposed when other index is selected
46+
Page4(), // Not preloaded and disposed when other index is selected
4747
],
4848
),
4949
bottomNavigationBar: BottomNavigationBar(

lib/lazy_load_indexed_stack.dart

+14-11
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ class LazyLoadIndexedStackState extends State<LazyLoadIndexedStack> {
5757
super.initState();
5858

5959
_children = _initialChildren();
60+
61+
final conflictingIndexes = widget.preloadIndexes.toSet().intersection(widget.autoDisposeIndexes.toSet());
62+
if (conflictingIndexes.isNotEmpty) {
63+
debugPrint('[LazyLoadIndexedStack] Warning: The same index is in both preloadIndexes and autoDisposeIndexes. '
64+
'It will be preloaded initially but disposed when not visible. Conflicting indexes: $conflictingIndexes');
65+
}
6066
}
6167

6268
@override
@@ -67,7 +73,7 @@ class LazyLoadIndexedStackState extends State<LazyLoadIndexedStack> {
6773
_children = _initialChildren();
6874
}
6975

70-
_children = _updateChildrenForAutoDispose();
76+
_children = _replaceAutoDisposedUnusedChildrenWithUnloadWidget();
7177

7278
_children[widget.index] = widget.children[widget.index];
7379
}
@@ -91,22 +97,19 @@ class LazyLoadIndexedStackState extends State<LazyLoadIndexedStack> {
9197

9298
if (index == widget.index || widget.preloadIndexes.contains(index)) {
9399
return childWidget;
94-
} else {
95-
return widget.unloadWidget;
96100
}
101+
102+
return widget.unloadWidget;
97103
}).toList();
98104
}
99105

100-
List<Widget> _updateChildrenForAutoDispose() {
101-
return widget.children.asMap().entries.map((entry) {
102-
final index = entry.key;
103-
final childWidget = entry.value;
104-
106+
List<Widget> _replaceAutoDisposedUnusedChildrenWithUnloadWidget() {
107+
return List.generate(_children.length, (index) {
105108
if (index != widget.index && widget.autoDisposeIndexes.contains(index)) {
106109
return widget.unloadWidget;
107-
} else {
108-
return childWidget;
109110
}
110-
}).toList();
111+
112+
return _children[index];
113+
});
111114
}
112115
}

test/lazy_load_indexed_stack_test.dart

+185-26
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,184 @@ import 'package:flutter_test/flutter_test.dart';
33
import 'package:lazy_load_indexed_stack/lazy_load_indexed_stack.dart';
44

55
void main() {
6-
testWidgets('LazyLoadIndexedStack', (final WidgetTester tester) async {
7-
const key = Key('key');
8-
final lazyLoadIndexedStack = LazyLoadIndexedStack(
9-
key: key,
10-
index: 0,
11-
preloadIndexes: const [1, 3],
12-
children: [
13-
_buildWidget(1),
14-
_buildWidget(2),
15-
_buildWidget(3),
16-
_buildWidget(4),
17-
_buildWidget(5),
18-
],
19-
);
20-
21-
await tester.pumpWidget(MaterialApp(home: lazyLoadIndexedStack));
22-
23-
final StatefulElement element = tester.element(find.byKey(key));
24-
final elementState = element.state as State<LazyLoadIndexedStack>;
25-
expect(elementState.widget, equals(lazyLoadIndexedStack));
26-
27-
expect(find.text('page1', skipOffstage: false), findsOneWidget);
28-
expect(find.text('page2', skipOffstage: false), findsOneWidget);
29-
expect(find.text('page3', skipOffstage: false), findsNothing);
30-
expect(find.text('page4', skipOffstage: false), findsOneWidget);
31-
expect(find.text('page5', skipOffstage: false), findsNothing);
6+
group('LazyLoadIndexedStack', () {
7+
group('default behavior', () {
8+
testWidgets('only the selected index is loaded', (tester) async {
9+
const key = Key('default_test');
10+
final lazyLoadIndexedStack = LazyLoadIndexedStack(
11+
key: key,
12+
index: 0,
13+
children: [
14+
_buildWidget(1),
15+
_buildWidget(2),
16+
_buildWidget(3),
17+
_buildWidget(4),
18+
_buildWidget(5),
19+
],
20+
);
21+
22+
// initial state index = 0
23+
await tester.pumpWidget(MaterialApp(home: lazyLoadIndexedStack));
24+
25+
expect(find.text('page1', skipOffstage: false), findsOneWidget);
26+
expect(find.text('page2', skipOffstage: false), findsNothing);
27+
expect(find.text('page3', skipOffstage: false), findsNothing);
28+
expect(find.text('page4', skipOffstage: false), findsNothing);
29+
expect(find.text('page5', skipOffstage: false), findsNothing);
30+
31+
// switch to index = 2
32+
await tester.pumpWidget(
33+
MaterialApp(
34+
home: LazyLoadIndexedStack(
35+
key: key,
36+
index: 2,
37+
children: [
38+
_buildWidget(1),
39+
_buildWidget(2),
40+
_buildWidget(3),
41+
_buildWidget(4),
42+
_buildWidget(5),
43+
],
44+
),
45+
),
46+
);
47+
await tester.pump();
48+
49+
expect(find.text('page1', skipOffstage: false), findsOneWidget);
50+
expect(find.text('page2', skipOffstage: false), findsNothing);
51+
expect(find.text('page3', skipOffstage: false), findsOneWidget);
52+
expect(find.text('page4', skipOffstage: false), findsNothing);
53+
expect(find.text('page5', skipOffstage: false), findsNothing);
54+
});
55+
});
56+
57+
group('#preloadIndexes', () {
58+
testWidgets('Only indexes in preloadIndexes should be preloaded', (tester) async {
59+
const key = Key('preload_test');
60+
final lazyLoadIndexedStack = LazyLoadIndexedStack(
61+
key: key,
62+
index: 0,
63+
preloadIndexes: const [1, 3],
64+
children: [
65+
_buildWidget(1),
66+
_buildWidget(2),
67+
_buildWidget(3),
68+
_buildWidget(4),
69+
_buildWidget(5),
70+
],
71+
);
72+
73+
await tester.pumpWidget(MaterialApp(home: lazyLoadIndexedStack));
74+
75+
expect(find.text('page1', skipOffstage: false), findsOneWidget);
76+
expect(find.text('page2', skipOffstage: false), findsOneWidget);
77+
expect(find.text('page3', skipOffstage: false), findsNothing);
78+
expect(find.text('page4', skipOffstage: false), findsOneWidget);
79+
expect(find.text('page5', skipOffstage: false), findsNothing);
80+
});
81+
});
82+
83+
group('#autoDisposeIndexes', () {
84+
testWidgets('Widgets in autoDisposeIndexes should be disposed when not visible', (tester) async {
85+
const key = Key('auto_dispose_test');
86+
final lazyLoadIndexedStack = LazyLoadIndexedStack(
87+
key: key,
88+
index: 0,
89+
autoDisposeIndexes: const [2, 4],
90+
children: [
91+
_buildWidget(1),
92+
_buildWidget(2),
93+
_buildWidgetWithKey(3),
94+
_buildWidget(4),
95+
_buildWidgetWithKey(5),
96+
],
97+
);
98+
99+
await tester.pumpWidget(MaterialApp(home: lazyLoadIndexedStack));
100+
101+
// initial state index = 0
102+
expect(find.text('page1', skipOffstage: false), findsOneWidget);
103+
expect(find.text('page2', skipOffstage: false), findsNothing);
104+
expect(find.text('page3', skipOffstage: false), findsNothing);
105+
expect(find.text('page4', skipOffstage: false), findsNothing);
106+
expect(find.text('page5', skipOffstage: false), findsNothing);
107+
108+
// switch to index = 2
109+
await tester.pumpWidget(
110+
MaterialApp(
111+
home: LazyLoadIndexedStack(
112+
key: key,
113+
index: 2,
114+
autoDisposeIndexes: const [2, 4],
115+
children: [
116+
_buildWidget(1),
117+
_buildWidget(2),
118+
_buildWidgetWithKey(3),
119+
_buildWidget(4),
120+
_buildWidgetWithKey(5),
121+
],
122+
),
123+
),
124+
);
125+
await tester.pump();
126+
127+
expect(find.text('page1', skipOffstage: false), findsOneWidget);
128+
expect(find.text('page2', skipOffstage: false), findsNothing);
129+
expect(find.text('page3', skipOffstage: false), findsOneWidget);
130+
expect(find.text('page4', skipOffstage: false), findsNothing);
131+
expect(find.text('page5', skipOffstage: false), findsNothing);
132+
133+
// back to index = 0 (3 should be disposed)
134+
await tester.pumpWidget(
135+
MaterialApp(
136+
home: LazyLoadIndexedStack(
137+
key: key,
138+
index: 0,
139+
autoDisposeIndexes: const [2, 4],
140+
children: [
141+
_buildWidget(1),
142+
_buildWidget(2),
143+
_buildWidgetWithKey(3),
144+
_buildWidget(4),
145+
_buildWidgetWithKey(5),
146+
],
147+
),
148+
),
149+
);
150+
await tester.pump();
151+
152+
expect(find.text('page1', skipOffstage: false), findsOneWidget);
153+
expect(find.text('page2', skipOffstage: false), findsNothing);
154+
expect(find.text('page3', skipOffstage: false), findsNothing);
155+
expect(find.text('page4', skipOffstage: false), findsNothing);
156+
expect(find.text('page5', skipOffstage: false), findsNothing);
157+
158+
// switch back to index = 2 (3 should be rebuilt)
159+
await tester.pumpWidget(
160+
MaterialApp(
161+
home: LazyLoadIndexedStack(
162+
key: key,
163+
index: 2,
164+
autoDisposeIndexes: const [2, 4],
165+
children: [
166+
_buildWidget(1),
167+
_buildWidget(2),
168+
_buildWidgetWithKey(3),
169+
_buildWidget(4),
170+
_buildWidgetWithKey(5),
171+
],
172+
),
173+
),
174+
);
175+
await tester.pump();
176+
177+
expect(find.text('page1', skipOffstage: false), findsOneWidget);
178+
expect(find.text('page2', skipOffstage: false), findsNothing);
179+
expect(find.text('page3', skipOffstage: false), findsOneWidget);
180+
expect(find.text('page4', skipOffstage: false), findsNothing);
181+
expect(find.text('page5', skipOffstage: false), findsNothing);
182+
});
183+
});
32184
});
33185
}
34186

@@ -37,3 +189,10 @@ Widget _buildWidget(final int num) {
37189
child: Text('page$num'),
38190
);
39191
}
192+
193+
Widget _buildWidgetWithKey(final int num) {
194+
return Center(
195+
key: ValueKey(num),
196+
child: Text('page$num'),
197+
);
198+
}

0 commit comments

Comments
 (0)