Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue where non-preloaded children were loaded when index changed #12

Merged
merged 6 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ A package that extends IndexedStack to allow for lazy loading and provides enhan

## Motivation

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

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

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.

Expand All @@ -19,7 +19,7 @@ Therefore, we created an extended IndexedStack that builds the required widget o
* **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.

## Usage
You can use `LazyLoadIndexedStack` in the same way as `IndexedStack`, with additional options for preloading and auto dispose.
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.

### Basic Example
```dart
Expand All @@ -37,13 +37,13 @@ class _MainPageState extends State<MainPage> {
home: Scaffold(
body: LazyLoadIndexedStack(
index: _index,
preloadIndexes: const [3],
autoDisposeIndexes: const [1, 2],
preloadIndexes: [1, 2],
autoDisposeIndexes: [2, 3],
children: [
Page1(),
Page2(), // index 1 will be auto dispose
Page3(), // index 2 will also auto dispose
Page4(), // index 3 is preloaded
Page1(), // Load by initial index
Page2(), // Preloaded initially
Page3(), // Preloaded initially but disposed when other index is selected
Page4(), // Not preloaded and disposed when other index is selected
],
),
bottomNavigationBar: BottomNavigationBar(
Expand Down
25 changes: 14 additions & 11 deletions lib/lazy_load_indexed_stack.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ class LazyLoadIndexedStackState extends State<LazyLoadIndexedStack> {
super.initState();

_children = _initialChildren();

final conflictingIndexes = widget.preloadIndexes.toSet().intersection(widget.autoDisposeIndexes.toSet());
if (conflictingIndexes.isNotEmpty) {
debugPrint('[LazyLoadIndexedStack] Warning: The same index is in both preloadIndexes and autoDisposeIndexes. '
'It will be preloaded initially but disposed when not visible. Conflicting indexes: $conflictingIndexes');
}
}

@override
Expand All @@ -67,7 +73,7 @@ class LazyLoadIndexedStackState extends State<LazyLoadIndexedStack> {
_children = _initialChildren();
}

_children = _updateChildrenForAutoDispose();
_children = _replaceAutoDisposedUnusedChildrenWithUnloadWidget();

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

if (index == widget.index || widget.preloadIndexes.contains(index)) {
return childWidget;
} else {
return widget.unloadWidget;
}

return widget.unloadWidget;
}).toList();
}

List<Widget> _updateChildrenForAutoDispose() {
return widget.children.asMap().entries.map((entry) {
final index = entry.key;
final childWidget = entry.value;

List<Widget> _replaceAutoDisposedUnusedChildrenWithUnloadWidget() {
return List.generate(_children.length, (index) {
if (index != widget.index && widget.autoDisposeIndexes.contains(index)) {
return widget.unloadWidget;
} else {
return childWidget;
}
}).toList();

return _children[index];
});
}
}
211 changes: 185 additions & 26 deletions test/lazy_load_indexed_stack_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,184 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:lazy_load_indexed_stack/lazy_load_indexed_stack.dart';

void main() {
testWidgets('LazyLoadIndexedStack', (final WidgetTester tester) async {
const key = Key('key');
final lazyLoadIndexedStack = LazyLoadIndexedStack(
key: key,
index: 0,
preloadIndexes: const [1, 3],
children: [
_buildWidget(1),
_buildWidget(2),
_buildWidget(3),
_buildWidget(4),
_buildWidget(5),
],
);

await tester.pumpWidget(MaterialApp(home: lazyLoadIndexedStack));

final StatefulElement element = tester.element(find.byKey(key));
final elementState = element.state as State<LazyLoadIndexedStack>;
expect(elementState.widget, equals(lazyLoadIndexedStack));

expect(find.text('page1', skipOffstage: false), findsOneWidget);
expect(find.text('page2', skipOffstage: false), findsOneWidget);
expect(find.text('page3', skipOffstage: false), findsNothing);
expect(find.text('page4', skipOffstage: false), findsOneWidget);
expect(find.text('page5', skipOffstage: false), findsNothing);
group('LazyLoadIndexedStack', () {
group('default behavior', () {
testWidgets('only the selected index is loaded', (tester) async {
const key = Key('default_test');
final lazyLoadIndexedStack = LazyLoadIndexedStack(
key: key,
index: 0,
children: [
_buildWidget(1),
_buildWidget(2),
_buildWidget(3),
_buildWidget(4),
_buildWidget(5),
],
);

// initial state index = 0
await tester.pumpWidget(MaterialApp(home: lazyLoadIndexedStack));

expect(find.text('page1', skipOffstage: false), findsOneWidget);
expect(find.text('page2', skipOffstage: false), findsNothing);
expect(find.text('page3', skipOffstage: false), findsNothing);
expect(find.text('page4', skipOffstage: false), findsNothing);
expect(find.text('page5', skipOffstage: false), findsNothing);

// switch to index = 2
await tester.pumpWidget(
MaterialApp(
home: LazyLoadIndexedStack(
key: key,
index: 2,
children: [
_buildWidget(1),
_buildWidget(2),
_buildWidget(3),
_buildWidget(4),
_buildWidget(5),
],
),
),
);
await tester.pump();

expect(find.text('page1', skipOffstage: false), findsOneWidget);
expect(find.text('page2', skipOffstage: false), findsNothing);
expect(find.text('page3', skipOffstage: false), findsOneWidget);
expect(find.text('page4', skipOffstage: false), findsNothing);
expect(find.text('page5', skipOffstage: false), findsNothing);
});
});

group('#preloadIndexes', () {
testWidgets('Only indexes in preloadIndexes should be preloaded', (tester) async {
const key = Key('preload_test');
final lazyLoadIndexedStack = LazyLoadIndexedStack(
key: key,
index: 0,
preloadIndexes: const [1, 3],
children: [
_buildWidget(1),
_buildWidget(2),
_buildWidget(3),
_buildWidget(4),
_buildWidget(5),
],
);

await tester.pumpWidget(MaterialApp(home: lazyLoadIndexedStack));

expect(find.text('page1', skipOffstage: false), findsOneWidget);
expect(find.text('page2', skipOffstage: false), findsOneWidget);
expect(find.text('page3', skipOffstage: false), findsNothing);
expect(find.text('page4', skipOffstage: false), findsOneWidget);
expect(find.text('page5', skipOffstage: false), findsNothing);
});
});

group('#autoDisposeIndexes', () {
testWidgets('Widgets in autoDisposeIndexes should be disposed when not visible', (tester) async {
const key = Key('auto_dispose_test');
final lazyLoadIndexedStack = LazyLoadIndexedStack(
key: key,
index: 0,
autoDisposeIndexes: const [2, 4],
children: [
_buildWidget(1),
_buildWidget(2),
_buildWidgetWithKey(3),
_buildWidget(4),
_buildWidgetWithKey(5),
],
);

await tester.pumpWidget(MaterialApp(home: lazyLoadIndexedStack));

// initial state index = 0
expect(find.text('page1', skipOffstage: false), findsOneWidget);
expect(find.text('page2', skipOffstage: false), findsNothing);
expect(find.text('page3', skipOffstage: false), findsNothing);
expect(find.text('page4', skipOffstage: false), findsNothing);
expect(find.text('page5', skipOffstage: false), findsNothing);

// switch to index = 2
await tester.pumpWidget(
MaterialApp(
home: LazyLoadIndexedStack(
key: key,
index: 2,
autoDisposeIndexes: const [2, 4],
children: [
_buildWidget(1),
_buildWidget(2),
_buildWidgetWithKey(3),
_buildWidget(4),
_buildWidgetWithKey(5),
],
),
),
);
await tester.pump();

expect(find.text('page1', skipOffstage: false), findsOneWidget);
expect(find.text('page2', skipOffstage: false), findsNothing);
expect(find.text('page3', skipOffstage: false), findsOneWidget);
expect(find.text('page4', skipOffstage: false), findsNothing);
expect(find.text('page5', skipOffstage: false), findsNothing);

// back to index = 0 (3 should be disposed)
await tester.pumpWidget(
MaterialApp(
home: LazyLoadIndexedStack(
key: key,
index: 0,
autoDisposeIndexes: const [2, 4],
children: [
_buildWidget(1),
_buildWidget(2),
_buildWidgetWithKey(3),
_buildWidget(4),
_buildWidgetWithKey(5),
],
),
),
);
await tester.pump();

expect(find.text('page1', skipOffstage: false), findsOneWidget);
expect(find.text('page2', skipOffstage: false), findsNothing);
expect(find.text('page3', skipOffstage: false), findsNothing);
expect(find.text('page4', skipOffstage: false), findsNothing);
expect(find.text('page5', skipOffstage: false), findsNothing);

// switch back to index = 2 (3 should be rebuilt)
await tester.pumpWidget(
MaterialApp(
home: LazyLoadIndexedStack(
key: key,
index: 2,
autoDisposeIndexes: const [2, 4],
children: [
_buildWidget(1),
_buildWidget(2),
_buildWidgetWithKey(3),
_buildWidget(4),
_buildWidgetWithKey(5),
],
),
),
);
await tester.pump();

expect(find.text('page1', skipOffstage: false), findsOneWidget);
expect(find.text('page2', skipOffstage: false), findsNothing);
expect(find.text('page3', skipOffstage: false), findsOneWidget);
expect(find.text('page4', skipOffstage: false), findsNothing);
expect(find.text('page5', skipOffstage: false), findsNothing);
});
});
});
}

Expand All @@ -37,3 +189,10 @@ Widget _buildWidget(final int num) {
child: Text('page$num'),
);
}

Widget _buildWidgetWithKey(final int num) {
return Center(
key: ValueKey(num),
child: Text('page$num'),
);
}