From 78a66f81e3b8063066e6af661837c14d65b169b2 Mon Sep 17 00:00:00 2001 From: Alex Wallen Date: Mon, 3 Oct 2022 09:40:17 -0700 Subject: [PATCH] [flutter_adaptive_scaffold] Call onDestinationSelected in bottom nav. (#2668) --- .../flutter_adaptive_scaffold/CHANGELOG.md | 4 + .../lib/src/adaptive_scaffold.dart | 18 +- .../flutter_adaptive_scaffold/pubspec.yaml | 2 +- .../test/adaptive_scaffold_test.dart | 189 ++++++++++++++---- 4 files changed, 166 insertions(+), 47 deletions(-) diff --git a/packages/flutter_adaptive_scaffold/CHANGELOG.md b/packages/flutter_adaptive_scaffold/CHANGELOG.md index 734b39305e..70e4f6582a 100644 --- a/packages/flutter_adaptive_scaffold/CHANGELOG.md +++ b/packages/flutter_adaptive_scaffold/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.5 + +* Calls onDestinationChanged callback in bottom nav bar. + ## 0.0.4 * Fix static analyzer warnings using `core` lint. diff --git a/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart b/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart index 13ca20033a..0521634722 100644 --- a/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart +++ b/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart @@ -212,7 +212,7 @@ class AdaptiveScaffold extends StatefulWidget { /// Option to override the drawerBreakpoint for the usage of [Drawer] over the /// usual [BottomNavigationBar]. /// - /// Defaults to [Breakpoints.onlySmallDesktop]. + /// Defaults to [Breakpoints.smallDesktop]. final Breakpoint drawerBreakpoint; /// Option to override the default [AppBar] when using drawer in desktop @@ -303,10 +303,12 @@ class AdaptiveScaffold extends StatefulWidget { /// Public helper method to be used for creating a [BottomNavigationBar] from /// a list of [NavigationDestination]s. - static Builder standardBottomNavigationBar( - {required List destinations, - int currentIndex = 0, - double iconSize = 24}) { + static Builder standardBottomNavigationBar({ + required List destinations, + int currentIndex = 0, + double iconSize = 24, + ValueChanged? onDestinationSelected, + }) { return Builder( builder: (_) { return BottomNavigationBar( @@ -315,6 +317,7 @@ class AdaptiveScaffold extends StatefulWidget { items: destinations .map((NavigationDestination e) => _toBottomNavItem(e)) .toList(), + onTap: onDestinationSelected, ); }, ); @@ -524,7 +527,10 @@ class _AdaptiveScaffoldState extends State { key: const Key('bottomNavigation'), builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( - destinations: widget.destinations), + currentIndex: widget.selectedIndex, + destinations: widget.destinations, + onDestinationSelected: widget.onSelectedIndexChange, + ), ), }, ) diff --git a/packages/flutter_adaptive_scaffold/pubspec.yaml b/packages/flutter_adaptive_scaffold/pubspec.yaml index 9de8b2c1f8..46ab151ba5 100644 --- a/packages/flutter_adaptive_scaffold/pubspec.yaml +++ b/packages/flutter_adaptive_scaffold/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_adaptive_scaffold description: Widgets to easily build adaptive layouts, including navigation elements. -version: 0.0.4 +version: 0.0.5 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_adaptive_scaffold%22 repository: https://github.com/flutter/packages/tree/main/packages/flutter_adaptive_scaffold diff --git a/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart b/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart index 71b081cfbd..1b24b6b12e 100644 --- a/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart +++ b/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart @@ -20,7 +20,8 @@ void main() { final Finder primaryNav = find.byKey(const Key('primaryNavigation')); final Finder primaryNav1 = find.byKey(const Key('primaryNavigation1')); - await tester.pumpWidget(await scaffold(width: 300, tester: tester)); + await tester.binding.setSurfaceSize(SimulatedLayout.mobile.size); + await tester.pumpWidget(SimulatedLayout.mobile.app()); await tester.pumpAndSettle(); expect(smallBody, findsOneWidget); @@ -29,10 +30,11 @@ void main() { expect(primaryNav, findsNothing); expect(tester.getTopLeft(smallBody), Offset.zero); - expect(tester.getTopLeft(smallSBody), const Offset(150, 0)); + expect(tester.getTopLeft(smallSBody), const Offset(200, 0)); expect(tester.getTopLeft(bottomNav), const Offset(0, 744)); - await tester.pumpWidget(await scaffold(width: 900, tester: tester)); + await tester.binding.setSurfaceSize(SimulatedLayout.tablet.size); + await tester.pumpWidget(SimulatedLayout.tablet.app()); await tester.pumpAndSettle(); expect(smallBody, findsNothing); @@ -43,11 +45,12 @@ void main() { expect(primaryNav, findsOneWidget); expect(tester.getTopLeft(body), const Offset(88, 0)); - expect(tester.getTopLeft(sBody), const Offset(450, 0)); + expect(tester.getTopLeft(sBody), const Offset(400, 0)); expect(tester.getTopLeft(primaryNav), Offset.zero); expect(tester.getBottomRight(primaryNav), const Offset(88, 800)); - await tester.pumpWidget(await scaffold(width: 1100, tester: tester)); + await tester.binding.setSurfaceSize(SimulatedLayout.desktop.size); + await tester.pumpWidget(SimulatedLayout.desktop.app()); await tester.pumpAndSettle(); expect(body, findsNothing); @@ -68,8 +71,10 @@ void main() { final Finder b = find.byKey(const Key('body')); final Finder sBody = find.byKey(const Key('sBody')); - await tester.pumpWidget(await scaffold(width: 400, tester: tester)); - await tester.pumpWidget(await scaffold(width: 800, tester: tester)); + await tester.binding.setSurfaceSize(SimulatedLayout.mobile.size); + await tester.pumpWidget(SimulatedLayout.mobile.app()); + await tester.binding.setSurfaceSize(SimulatedLayout.tablet.size); + await tester.pumpWidget(SimulatedLayout.tablet.app()); await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); @@ -107,10 +112,11 @@ void main() { final Finder b = find.byKey(const Key('body')); final Finder sBody = find.byKey(const Key('sBody')); - await tester.pumpWidget( - await scaffold(width: 400, tester: tester, animations: false)); - await tester.pumpWidget( - await scaffold(width: 800, tester: tester, animations: false)); + await tester.binding.setSurfaceSize(SimulatedLayout.mobile.size); + await tester.pumpWidget(SimulatedLayout.mobile.app(animations: false)); + + await tester.binding.setSurfaceSize(SimulatedLayout.tablet.size); + await tester.pumpWidget(SimulatedLayout.tablet.app(animations: false)); await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); @@ -120,6 +126,48 @@ void main() { expect(tester.getTopLeft(sBody), const Offset(400, 0)); expect(tester.getBottomRight(sBody), const Offset(800, 800)); }); + + // The goal of this test is to run through each of the navigation elements + // and test whether tapping on that element will update the selected index + // globally + testWidgets('tapping navigation elements calls onSelectedIndexChange', + (WidgetTester tester) async { + // for each screen size there is a different navigational element that + // we want to test tapping to set the selected index + await Future.forEach(SimulatedLayout.values, + (SimulatedLayout region) async { + int selectedIndex = 0; + final MaterialApp app = region.app(initialIndex: selectedIndex); + await tester.binding.setSurfaceSize(region.size); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + // tap on the next icon + selectedIndex = (selectedIndex + 1) % TestScaffold.destinations.length; + + // Resolve the icon that should be found + final NavigationDestination destination = + TestScaffold.destinations[selectedIndex]; + expect(destination.icon, isA()); + final Icon icon = destination.icon as Icon; + expect(icon.icon, isNotNull); + + // Find the icon in the application to tap + final Widget navigationSlot = + tester.widget(find.byKey(Key(region.navSlotKey))); + final Finder target = + find.widgetWithIcon(navigationSlot.runtimeType, icon.icon!); + expect(target, findsOneWidget); + + await tester.tap(target); + await tester.pumpAndSettle(); + + // Check that the state was set appropriately + final Finder scaffold = find.byType(TestScaffold); + final TestScaffoldState state = tester.state(scaffold); + expect(selectedIndex, state.index); + }); + }); } class TestBreakpoint0 extends Breakpoint { @@ -152,34 +200,95 @@ class NeverOnBreakpoint extends Breakpoint { } } -Future scaffold({ - required double width, - required WidgetTester tester, - bool animations = true, -}) async { - await tester.binding.setSurfaceSize(Size(width, 800)); - return MaterialApp( - home: MediaQuery( - data: MediaQueryData(size: Size(width, 800)), - child: AdaptiveScaffold( - drawerBreakpoint: NeverOnBreakpoint(), - internalAnimations: animations, - smallBreakpoint: TestBreakpoint0(), - mediumBreakpoint: TestBreakpoint800(), - largeBreakpoint: TestBreakpoint1000(), - destinations: const [ - NavigationDestination(icon: Icon(Icons.inbox), label: 'Inbox'), - NavigationDestination(icon: Icon(Icons.article), label: 'Articles'), - NavigationDestination(icon: Icon(Icons.chat), label: 'Chat'), - NavigationDestination(icon: Icon(Icons.video_call), label: 'Video'), - ], - smallBody: (_) => Container(color: Colors.red), - body: (_) => Container(color: Colors.green), - largeBody: (_) => Container(color: Colors.blue), - smallSecondaryBody: (_) => Container(color: Colors.red), - secondaryBody: (_) => Container(color: Colors.green), - largeSecondaryBody: (_) => Container(color: Colors.blue), - ), +class TestScaffold extends StatefulWidget { + const TestScaffold({ + super.key, + this.initialIndex = 0, + this.isAnimated = true, + }); + + final int initialIndex; + final bool isAnimated; + + static const List destinations = + [ + NavigationDestination( + key: Key('Inbox'), + icon: Icon(Icons.inbox), + label: 'Inbox', ), - ); + NavigationDestination( + key: Key('Articles'), + icon: Icon(Icons.article), + label: 'Articles', + ), + NavigationDestination( + key: Key('Chat'), + icon: Icon(Icons.chat), + label: 'Chat', + ), + ]; + + @override + State createState() => TestScaffoldState(); +} + +class TestScaffoldState extends State { + late int index = widget.initialIndex; + + @override + Widget build(BuildContext context) { + return AdaptiveScaffold( + selectedIndex: index, + onSelectedIndexChange: (int index) { + setState(() { + this.index = index; + }); + }, + drawerBreakpoint: NeverOnBreakpoint(), + internalAnimations: widget.isAnimated, + smallBreakpoint: TestBreakpoint0(), + mediumBreakpoint: TestBreakpoint800(), + largeBreakpoint: TestBreakpoint1000(), + destinations: TestScaffold.destinations, + smallBody: (_) => Container(color: Colors.red), + body: (_) => Container(color: Colors.green), + largeBody: (_) => Container(color: Colors.blue), + smallSecondaryBody: (_) => Container(color: Colors.red), + secondaryBody: (_) => Container(color: Colors.green), + largeSecondaryBody: (_) => Container(color: Colors.blue), + ); + } +} + +enum SimulatedLayout { + mobile(width: 400, navSlotKey: 'bottomNavigation'), + tablet(width: 800, navSlotKey: 'primaryNavigation'), + desktop(width: 1100, navSlotKey: 'primaryNavigation1'); + + const SimulatedLayout({ + required double width, + required this.navSlotKey, + }) : _width = width; + + final double _width; + final double _height = 800; + final String navSlotKey; + + Size get size => Size(_width, _height); + + MaterialApp app({ + int initialIndex = 0, + bool animations = true, + }) { + return MaterialApp( + home: MediaQuery( + data: MediaQueryData(size: size), + child: TestScaffold( + initialIndex: initialIndex, + isAnimated: animations, + ), + ), + ); + } }