Skip to content

Commit

Permalink
Merge pull request #138 from danut007ro/controller_unique_history
Browse files Browse the repository at this point in the history
Add unique history for controller
  • Loading branch information
jb3rndt authored Apr 16, 2024
2 parents 9b0330b + c77e741 commit 8da642e
Show file tree
Hide file tree
Showing 3 changed files with 276 additions and 10 deletions.
10 changes: 6 additions & 4 deletions lib/components/persistent_tab_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,10 @@ class _PersistentTabViewState extends State<PersistentTabView> {
if (!widget.handleAndroidBackButtonPress && widget.onWillPop != null) {
return widget.onWillPop!(_contextList[_controller.index]!);
} else {
if (_controller.isOnInitialTab() &&
!_navigatorKeys.first.currentState!.canPop()) {
if (_controller.historyIsEmpty() &&
!_navigatorKeys[_controller.index].currentState!.canPop()) {
if (widget.handleAndroidBackButtonPress && widget.onWillPop != null) {
return widget.onWillPop!(_contextList.first!);
return widget.onWillPop!(_contextList[_controller.index]!);
}
// CanPop should be true in this case, so we dont return true because the pop already happened
return false;
Expand Down Expand Up @@ -429,7 +429,9 @@ class _PersistentTabViewState extends State<PersistentTabView> {
bool calcCanPop({bool? subtreeCantHandlePop}) =>
widget.handleAndroidBackButtonPress &&
widget.onWillPop == null &&
_controller.isOnInitialTab() &&
_controller.historyIsEmpty() &&
_navigatorKeys[_controller.index].currentState !=
null && // Required if historyLength == 0 because historyIsEmpty() is already true when switching to uninitialized tabs instead of only when going back.
(subtreeCantHandlePop ??
!_navigatorKeys[_controller.index].currentState!.canPop());
}
52 changes: 46 additions & 6 deletions lib/models/persistent_tab_controller.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
part of "../persistent_bottom_nav_bar_v2.dart";

/// Navigation bar controller for `PersistentTabView`.
///
/// [historyLength] is the number of tab switches that are kept in history.
/// Switching to another tab will add another entry in history, overwriting previous entry if the history gets too big.
/// Initial tab will always be on the first position in history.
/// Pressing the back button will switch to previous tab from history.
/// If [historyLength]=0 there will be no history, pressing back button will exit.
/// If [historyLength]=1 there will be one entry kept in history, pressing back button will switch to initial tab, pressing again will exit.
/// If [historyLength]=n there will be n entries kept in history, pressing back button will switch to previous tab.
///
/// [clearHistoryOnInitialIndex] specifies if history should be cleared when switching to initial tab.
/// Clearing history means that next back button press will exit.
class PersistentTabController extends ChangeNotifier {
PersistentTabController({int initialIndex = 0})
: _index = initialIndex,
assert(initialIndex >= 0, "Nav Bar item index cannot be less than 0");
PersistentTabController({
int initialIndex = 0,
int historyLength = 5,
bool clearHistoryOnInitialIndex = false,
}) : _initialIndex = initialIndex,
_historyLength = historyLength,
_clearHistoryOnInitialIndex = clearHistoryOnInitialIndex,
_index = initialIndex,
assert(initialIndex >= 0, "Nav Bar item index cannot be less than 0"),
assert(
historyLength >= 0, "Nav Bar history length cannot be less than 0");

final int _initialIndex;
final int _historyLength;
final bool _clearHistoryOnInitialIndex;
int get index => _index;
int _index;
GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
Expand All @@ -18,7 +40,25 @@ class PersistentTabController extends ChangeNotifier {
return;
}
if (!isUndo) {
_tabHistory.add(_index);
if (_clearHistoryOnInitialIndex && value == _initialIndex) {
_tabHistory.clear();
} else {
if (_historyLength == 1 &&
_tabHistory.length == 1 &&
_tabHistory[0] == value) {
// Clear history when switching to initial tab and it is the only entry in history.
_tabHistory.clear();
} else if (_historyLength > 0) {
_tabHistory.add(_index);
}

if (_tabHistory.length > _historyLength) {
_tabHistory.removeAt(1);
if (_tabHistory.length > 1 && _tabHistory[0] == _tabHistory[1]) {
_tabHistory.removeAt(1);
}
}
}
}
_index = value;
onIndexChanged?.call(value);
Expand All @@ -30,12 +70,12 @@ class PersistentTabController extends ChangeNotifier {
}

void jumpToPreviousTab() {
if (!isOnInitialTab()) {
if (!historyIsEmpty()) {
_updateIndex(_tabHistory.removeLast(), true);
}
}

bool isOnInitialTab() => _tabHistory.isEmpty;
bool historyIsEmpty() => _tabHistory.isEmpty;

void openDrawer() {
scaffoldKey.currentState?.openDrawer();
Expand Down
224 changes: 224 additions & 0 deletions test/persistent_tab_view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,35 @@ Future<void> tapAndroidBackButton(WidgetTester tester) async {
await tester.pumpAndSettle();
}

void expectScreen(int? id, {int screenCount = 3}) {
if (id == null) {
expect(find.text("MainScreen"), findsOne);
}

for (int i = 1; i <= screenCount; i++) {
expect(find.text("Screen$i").hitTestable(),
id == i ? findsOneWidget : findsNothing);
}
}

void main() {
Widget wrapTabView(WidgetBuilder builder) => MaterialApp(
home: Builder(
builder: (context) => builder(context),
),
);

Widget wrapTabViewWithMainScreen(WidgetBuilder builder) => wrapTabView(
(context) => ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => builder(context),
),
),
child: const Text("MainScreen"),
),
);

group("PersistentTabView", () {
testWidgets("builds a DecoratedNavBar", (tester) async {
await tester.pumpWidget(
Expand Down Expand Up @@ -319,6 +341,208 @@ void main() {
expect(find.text("Screen1"), findsOneWidget);
expect(find.text("Screen11"), findsNothing);
});

testWidgets("pops main screen when historyLength is 0", (tester) async {
await tester.pumpWidget(
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller:
PersistentTabController(initialIndex: 1, historyLength: 0),
tabs: [1, 2, 3]
.map((id) => tabConfig(id, defaultScreen(id)))
.toList(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
),
);

await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expectScreen(2);

await tester.tap(find.text("Item1"));
await tester.pumpAndSettle();
expectScreen(1);

await tapAndroidBackButton(tester);
expectScreen(null);
});

testWidgets("pops main screen when historyLength is 1", (tester) async {
await tester.pumpWidget(
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller:
PersistentTabController(initialIndex: 1, historyLength: 1),
tabs: [1, 2, 3]
.map((id) => tabConfig(id, defaultScreen(id)))
.toList(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
),
);

await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expectScreen(2);

await tester.tap(find.text("Item1"));
await tester.pumpAndSettle();
expectScreen(1);

await tapAndroidBackButton(tester);
expectScreen(2);

await tapAndroidBackButton(tester);
expectScreen(null);
});

testWidgets(
"pops main screen when historyLength is 1 and switched to initial tab",
(tester) async {
await tester.pumpWidget(
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller:
PersistentTabController(initialIndex: 1, historyLength: 1),
tabs: [1, 2, 3]
.map((id) => tabConfig(id, defaultScreen(id)))
.toList(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
),
);

await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expectScreen(2);

await tester.tap(find.text("Item1"));
await tester.pumpAndSettle();
expectScreen(1);

await tester.tap(find.text("Item2"));
await tester.pumpAndSettle();
expectScreen(2);

await tapAndroidBackButton(tester);
expectScreen(null);
});

testWidgets(
"pops main screen when historyLength is 1 and initial tab has subpage",
(tester) async {
await tester.pumpWidget(
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller:
PersistentTabController(initialIndex: 1, historyLength: 1),
tabs: [1, 2, 3]
.map((id) => tabConfig(
id, id == 2 ? screenWithSubPages(id) : defaultScreen(id)))
.toList(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
),
);

await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expectScreen(2);

await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.text("SubPage"), findsOne);

await tester.tap(find.text("Item1"));
await tester.pumpAndSettle();
expectScreen(1);

await tapAndroidBackButton(tester);
expect(find.text("SubPage"), findsOne);

await tapAndroidBackButton(tester);
expectScreen(2);

await tapAndroidBackButton(tester);
expectScreen(null);
});

testWidgets("pops main screen when historyLength is 2", (tester) async {
await tester.pumpWidget(
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller:
PersistentTabController(initialIndex: 1, historyLength: 2),
tabs: [1, 2, 3]
.map((id) => tabConfig(id, defaultScreen(id)))
.toList(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
),
);

await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expectScreen(2);

await tester.tap(find.text("Item1"));
await tester.pumpAndSettle();
expectScreen(1);

await tester.tap(find.text("Item3"));
await tester.pumpAndSettle();
expectScreen(3);

await tapAndroidBackButton(tester);
expectScreen(1);

await tapAndroidBackButton(tester);
expectScreen(2);

await tapAndroidBackButton(tester);
expectScreen(null);
});

testWidgets(
"pops main screen when historyLength is 2 and clearing history",
(tester) async {
await tester.pumpWidget(
wrapTabViewWithMainScreen(
(context) => PersistentTabView(
controller: PersistentTabController(
initialIndex: 1,
historyLength: 2,
clearHistoryOnInitialIndex: true),
tabs: [1, 2, 3]
.map((id) => tabConfig(id, defaultScreen(id)))
.toList(),
navBarBuilder: (config) =>
Style1BottomNavBar(navBarConfig: config),
),
),
);

await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expectScreen(2);

await tester.tap(find.text("Item1"));
await tester.pumpAndSettle();
expectScreen(1);

await tester.tap(find.text("Item2"));
await tester.pumpAndSettle();
expectScreen(2);

await tapAndroidBackButton(tester);
expectScreen(null);
});
});

group("should not handle Android back button press and thus", () {
Expand Down

0 comments on commit 8da642e

Please sign in to comment.