diff --git a/README.md b/README.md index f8e7fb1..26768e5 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -37,13 +37,13 @@ class _MainPageState extends State { 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( diff --git a/lib/lazy_load_indexed_stack.dart b/lib/lazy_load_indexed_stack.dart index 8ca06bc..d4e6b60 100644 --- a/lib/lazy_load_indexed_stack.dart +++ b/lib/lazy_load_indexed_stack.dart @@ -57,6 +57,12 @@ class LazyLoadIndexedStackState extends State { 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 @@ -67,7 +73,7 @@ class LazyLoadIndexedStackState extends State { _children = _initialChildren(); } - _children = _updateChildrenForAutoDispose(); + _children = _replaceAutoDisposedUnusedChildrenWithUnloadWidget(); _children[widget.index] = widget.children[widget.index]; } @@ -91,22 +97,19 @@ class LazyLoadIndexedStackState extends State { if (index == widget.index || widget.preloadIndexes.contains(index)) { return childWidget; - } else { - return widget.unloadWidget; } + + return widget.unloadWidget; }).toList(); } - List _updateChildrenForAutoDispose() { - return widget.children.asMap().entries.map((entry) { - final index = entry.key; - final childWidget = entry.value; - + List _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]; + }); } } diff --git a/test/lazy_load_indexed_stack_test.dart b/test/lazy_load_indexed_stack_test.dart index 7584717..c805887 100644 --- a/test/lazy_load_indexed_stack_test.dart +++ b/test/lazy_load_indexed_stack_test.dart @@ -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; - 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); + }); + }); }); } @@ -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'), + ); +}