diff --git a/packages/flutter_adaptive_scaffold/CHANGELOG.md b/packages/flutter_adaptive_scaffold/CHANGELOG.md index cfa2ef5ba9a..9756cfe90b5 100644 --- a/packages/flutter_adaptive_scaffold/CHANGELOG.md +++ b/packages/flutter_adaptive_scaffold/CHANGELOG.md @@ -1,4 +1,8 @@ -## NEXT +## 0.1.1 + +* Fixes flutter/flutter#121135) `selectedIcon` parameter not displayed even if it is provided. + +## 0.1.0+1 * Aligns Dart and Flutter SDK constraints. diff --git a/packages/flutter_adaptive_scaffold/README.md b/packages/flutter_adaptive_scaffold/README.md index 9d07da101a9..65135be19d7 100644 --- a/packages/flutter_adaptive_scaffold/README.md +++ b/packages/flutter_adaptive_scaffold/README.md @@ -49,40 +49,60 @@ animation should use AdaptiveLayout. ) ]; - return BottomNavigationBarTheme( - data: const BottomNavigationBarThemeData( - unselectedItemColor: Colors.black, - selectedItemColor: Colors.black, - backgroundColor: Colors.white, + return AdaptiveScaffold( + // An option to override the default breakpoints used for small, medium, + // and large. + smallBreakpoint: const WidthPlatformBreakpoint(end: 700), + mediumBreakpoint: const WidthPlatformBreakpoint(begin: 700, end: 1000), + largeBreakpoint: const WidthPlatformBreakpoint(begin: 1000), + useDrawer: false, + selectedIndex: _selectedTab, + onSelectedIndexChange: (int index) { + setState(() { + _selectedTab = index; + }); + }, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.inbox_outlined), + selectedIcon: Icon(Icons.inbox), + label: 'Inbox', ), - child: AdaptiveScaffold( - // An option to override the default breakpoints used for small, medium, - // and large. - smallBreakpoint: const WidthPlatformBreakpoint(end: 700), - mediumBreakpoint: - const WidthPlatformBreakpoint(begin: 700, end: 1000), - largeBreakpoint: const WidthPlatformBreakpoint(begin: 1000), - useDrawer: false, - 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') - ], - body: (_) => GridView.count(crossAxisCount: 2, children: children), - smallBody: (_) => ListView.builder( - itemCount: children.length, - itemBuilder: (_, int idx) => children[idx], - ), - // Define a default secondaryBody. - secondaryBody: (_) => - Container(color: const Color.fromARGB(255, 234, 158, 192)), - // Override the default secondaryBody during the smallBreakpoint to be - // empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly - // overridden. - smallSecondaryBody: AdaptiveScaffold.emptyBuilder)); + NavigationDestination( + icon: Icon(Icons.article_outlined), + selectedIcon: Icon(Icons.article), + label: 'Articles', + ), + NavigationDestination( + icon: Icon(Icons.chat_outlined), + selectedIcon: Icon(Icons.chat), + label: 'Chat', + ), + NavigationDestination( + icon: Icon(Icons.video_call_outlined), + selectedIcon: Icon(Icons.video_call), + label: 'Video', + ), + NavigationDestination( + icon: Icon(Icons.home_outlined), + selectedIcon: Icon(Icons.home), + label: 'Inbox', + ), + ], + body: (_) => GridView.count(crossAxisCount: 2, children: children), + smallBody: (_) => ListView.builder( + itemCount: children.length, + itemBuilder: (_, int idx) => children[idx], + ), + // Define a default secondaryBody. + secondaryBody: (_) => Container( + color: const Color.fromARGB(255, 234, 158, 192), + ), + // Override the default secondaryBody during the smallBreakpoint to be + // empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly + // overridden. + smallSecondaryBody: AdaptiveScaffold.emptyBuilder, + ); } } ``` @@ -129,6 +149,12 @@ displayed and the entrance animation and exit animation. inAnimation: AdaptiveScaffold.leftOutIn, key: const Key('Primary Navigation Medium'), builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, leading: const Icon(Icons.menu), destinations: destinations .map((_) => AdaptiveScaffold.toRailDestination(_)) @@ -139,6 +165,12 @@ displayed and the entrance animation and exit animation. key: const Key('Primary Navigation Large'), inAnimation: AdaptiveScaffold.leftOutIn, builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, extended: true, leading: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -184,11 +216,14 @@ displayed and the entrance animation and exit animation. key: const Key('Bottom Navigation Small'), inAnimation: AdaptiveScaffold.bottomToTop, outAnimation: AdaptiveScaffold.topToBottom, - builder: (_) => BottomNavigationBarTheme( - data: const BottomNavigationBarThemeData( - selectedItemColor: Colors.black), - child: AdaptiveScaffold.standardBottomNavigationBar( - destinations: destinations), + builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( + destinations: destinations, + currentIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, ), ) }, diff --git a/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart b/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart index baa6f605026..1c753d7e467 100644 --- a/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart +++ b/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart @@ -27,10 +27,17 @@ class MyApp extends StatelessWidget { /// Creates a basic adaptive page with navigational elements and a body using /// [AdaptiveLayout]. -class MyHomePage extends StatelessWidget { +class MyHomePage extends StatefulWidget { /// Creates a const [MyHomePage]. const MyHomePage({super.key}); + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int selectedNavigation = 0; + @override Widget build(BuildContext context) { // Define the children to display within the body. @@ -113,19 +120,23 @@ class MyHomePage extends StatelessWidget { const List destinations = [ NavigationDestination( label: 'Inbox', - icon: Icon(Icons.inbox, color: Colors.black), + icon: Icon(Icons.inbox_outlined), + selectedIcon: Icon(Icons.inbox), ), NavigationDestination( label: 'Articles', - icon: Icon(Icons.article_outlined, color: Colors.black), + icon: Icon(Icons.article_outlined), + selectedIcon: Icon(Icons.article), ), NavigationDestination( label: 'Chat', - icon: Icon(Icons.chat_bubble_outline, color: Colors.black), + icon: Icon(Icons.chat_outlined), + selectedIcon: Icon(Icons.chat), ), NavigationDestination( label: 'Video', - icon: Icon(Icons.video_call_outlined, color: Colors.black), + icon: Icon(Icons.video_call_outlined), + selectedIcon: Icon(Icons.video_call), ), ]; @@ -142,6 +153,12 @@ class MyHomePage extends StatelessWidget { inAnimation: AdaptiveScaffold.leftOutIn, key: const Key('Primary Navigation Medium'), builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, leading: const Icon(Icons.menu), destinations: destinations .map((_) => AdaptiveScaffold.toRailDestination(_)) @@ -152,6 +169,12 @@ class MyHomePage extends StatelessWidget { key: const Key('Primary Navigation Large'), inAnimation: AdaptiveScaffold.leftOutIn, builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, extended: true, leading: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -197,11 +220,14 @@ class MyHomePage extends StatelessWidget { key: const Key('Bottom Navigation Small'), inAnimation: AdaptiveScaffold.bottomToTop, outAnimation: AdaptiveScaffold.topToBottom, - builder: (_) => BottomNavigationBarTheme( - data: const BottomNavigationBarThemeData( - selectedItemColor: Colors.black), - child: AdaptiveScaffold.standardBottomNavigationBar( - destinations: destinations), + builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( + destinations: destinations, + currentIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, ), ) }, diff --git a/packages/flutter_adaptive_scaffold/example/lib/adaptive_scaffold_demo.dart b/packages/flutter_adaptive_scaffold/example/lib/adaptive_scaffold_demo.dart index 9130b6be638..6ec09c71ae5 100644 --- a/packages/flutter_adaptive_scaffold/example/lib/adaptive_scaffold_demo.dart +++ b/packages/flutter_adaptive_scaffold/example/lib/adaptive_scaffold_demo.dart @@ -22,10 +22,17 @@ class MyApp extends StatelessWidget { /// Creates a basic adaptive page with navigational elements and a body using /// [AdaptiveScaffold]. -class MyHomePage extends StatelessWidget { +class MyHomePage extends StatefulWidget { /// Creates a const [MyHomePage]. const MyHomePage({super.key}); + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _selectedTab = 0; + // #docregion Example @override Widget build(BuildContext context) { @@ -41,40 +48,60 @@ class MyHomePage extends StatelessWidget { ) ]; - return BottomNavigationBarTheme( - data: const BottomNavigationBarThemeData( - unselectedItemColor: Colors.black, - selectedItemColor: Colors.black, - backgroundColor: Colors.white, + return AdaptiveScaffold( + // An option to override the default breakpoints used for small, medium, + // and large. + smallBreakpoint: const WidthPlatformBreakpoint(end: 700), + mediumBreakpoint: const WidthPlatformBreakpoint(begin: 700, end: 1000), + largeBreakpoint: const WidthPlatformBreakpoint(begin: 1000), + useDrawer: false, + selectedIndex: _selectedTab, + onSelectedIndexChange: (int index) { + setState(() { + _selectedTab = index; + }); + }, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.inbox_outlined), + selectedIcon: Icon(Icons.inbox), + label: 'Inbox', + ), + NavigationDestination( + icon: Icon(Icons.article_outlined), + selectedIcon: Icon(Icons.article), + label: 'Articles', + ), + NavigationDestination( + icon: Icon(Icons.chat_outlined), + selectedIcon: Icon(Icons.chat), + label: 'Chat', + ), + NavigationDestination( + icon: Icon(Icons.video_call_outlined), + selectedIcon: Icon(Icons.video_call), + label: 'Video', + ), + NavigationDestination( + icon: Icon(Icons.home_outlined), + selectedIcon: Icon(Icons.home), + label: 'Inbox', ), - child: AdaptiveScaffold( - // An option to override the default breakpoints used for small, medium, - // and large. - smallBreakpoint: const WidthPlatformBreakpoint(end: 700), - mediumBreakpoint: - const WidthPlatformBreakpoint(begin: 700, end: 1000), - largeBreakpoint: const WidthPlatformBreakpoint(begin: 1000), - useDrawer: false, - 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') - ], - body: (_) => GridView.count(crossAxisCount: 2, children: children), - smallBody: (_) => ListView.builder( - itemCount: children.length, - itemBuilder: (_, int idx) => children[idx], - ), - // Define a default secondaryBody. - secondaryBody: (_) => - Container(color: const Color.fromARGB(255, 234, 158, 192)), - // Override the default secondaryBody during the smallBreakpoint to be - // empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly - // overridden. - smallSecondaryBody: AdaptiveScaffold.emptyBuilder)); + ], + body: (_) => GridView.count(crossAxisCount: 2, children: children), + smallBody: (_) => ListView.builder( + itemCount: children.length, + itemBuilder: (_, int idx) => children[idx], + ), + // Define a default secondaryBody. + secondaryBody: (_) => Container( + color: const Color.fromARGB(255, 234, 158, 192), + ), + // Override the default secondaryBody during the smallBreakpoint to be + // empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly + // overridden. + smallSecondaryBody: AdaptiveScaffold.emptyBuilder, + ); } - // #enddocregion +// #enddocregion } diff --git a/packages/flutter_adaptive_scaffold/example/lib/main.dart b/packages/flutter_adaptive_scaffold/example/lib/main.dart index 60a863bb5be..994b9c8b835 100644 --- a/packages/flutter_adaptive_scaffold/example/lib/main.dart +++ b/packages/flutter_adaptive_scaffold/example/lib/main.dart @@ -54,6 +54,7 @@ class _MyHomePageState extends State // The index of the selected mail card. int? selected; + void selectCard(int? index) { setState(() { selected = index; @@ -68,6 +69,7 @@ class _MyHomePageState extends State late AnimationController _articleIconSlideController; late AnimationController _chatIconSlideController; late AnimationController _videoIconSlideController; + @override void initState() { showGridView.addListener(() { @@ -116,7 +118,6 @@ class _MyHomePageState extends State @override Widget build(BuildContext context) { - const Color iconColor = Color.fromARGB(255, 29, 25, 43); final Widget trailingNavRail = Column( children: [ const Divider(color: Colors.white, thickness: 1.5), @@ -187,16 +188,21 @@ class _MyHomePageState extends State // builders. const List destinations = [ NavigationDestination( - label: 'Inbox', icon: Icon(Icons.inbox, color: iconColor)), + label: 'Inbox', + icon: Icon(Icons.inbox), + ), NavigationDestination( - label: 'Articles', - icon: Icon(Icons.article_outlined, color: iconColor)), + label: 'Articles', + icon: Icon(Icons.article_outlined), + ), NavigationDestination( - label: 'Chat', - icon: Icon(Icons.chat_bubble_outline, color: iconColor)), + label: 'Chat', + icon: Icon(Icons.chat_bubble_outline), + ), NavigationDestination( - label: 'Video', - icon: Icon(Icons.video_call_outlined, color: iconColor)) + label: 'Video', + icon: Icon(Icons.video_call_outlined), + ) ]; // Updating the listener value. @@ -325,11 +331,8 @@ class _MyHomePageState extends State // You can define inAnimations or outAnimations to override the // default offset transition. outAnimation: AdaptiveScaffold.topToBottom, - builder: (_) => BottomNavigationBarTheme( - data: const BottomNavigationBarThemeData( - selectedItemColor: Colors.black), - child: AdaptiveScaffold.standardBottomNavigationBar( - destinations: destinations), + builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( + destinations: destinations, ), ) }, @@ -890,6 +893,7 @@ class _ScreenArguments { required this.item, required this.selectCard, }); + final _Item item; final _CardSelectedCallback selectCard; } diff --git a/packages/flutter_adaptive_scaffold/example/test/adaptive_layout_demo_test.dart b/packages/flutter_adaptive_scaffold/example/test/adaptive_layout_demo_test.dart index 603476f2d57..3e716ee633f 100644 --- a/packages/flutter_adaptive_scaffold/example/test/adaptive_layout_demo_test.dart +++ b/packages/flutter_adaptive_scaffold/example/test/adaptive_layout_demo_test.dart @@ -55,10 +55,8 @@ void main() { final WidgetBuilder? widgetBuilder = slotLayoutConfig.builder; final Widget Function(BuildContext) widgetFunction = (widgetBuilder ?? () => Container()) as Widget Function(BuildContext); - final BottomNavigationBarThemeData bottomNavigationBarThemeData = - (widgetFunction(context) as BottomNavigationBarTheme).data; - - expect(bottomNavigationBarThemeData.selectedItemColor, Colors.black); + final Builder? bottomNavBarBuilder = widgetFunction(context) as Builder?; + expect(bottomNavBarBuilder, isNotNull); }); testWidgets( diff --git a/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart b/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart index 8a7e37e8fa0..060ccaf148b 100644 --- a/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart +++ b/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart @@ -235,10 +235,12 @@ class AdaptiveScaffold extends StatefulWidget { /// Public helper method to be used for creating a [NavigationRailDestination] from /// a [NavigationDestination]. static NavigationRailDestination toRailDestination( - NavigationDestination destination) { + NavigationDestination destination, + ) { return NavigationRailDestination( label: Text(destination.label), icon: destination.icon, + selectedIcon: destination.selectedIcon, ); } @@ -309,7 +311,6 @@ class AdaptiveScaffold extends StatefulWidget { double iconSize = 24, ValueChanged? onDestinationSelected, }) { - currentIndex ??= 0; return Builder( builder: (_) { return BottomNavigationBar( @@ -628,6 +629,7 @@ BottomNavigationBarItem _toBottomNavItem(NavigationDestination destination) { return BottomNavigationBarItem( label: destination.label, icon: destination.icon, + activeIcon: destination.selectedIcon, ); } diff --git a/packages/flutter_adaptive_scaffold/pubspec.yaml b/packages/flutter_adaptive_scaffold/pubspec.yaml index b67fd5ee6d0..f94ff3356ed 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.1.0 +version: 0.1.1 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 7e3e46b44e4..e55220e0787 100644 --- a/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart +++ b/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart @@ -239,6 +239,174 @@ void main() { }); }, ); + + testWidgets( + 'when destinations passed with all data, it shall not be null', + (WidgetTester tester) async { + const List destinations = [ + NavigationDestination( + icon: Icon(Icons.inbox_outlined), + selectedIcon: Icon(Icons.inbox), + label: 'Inbox', + ), + NavigationDestination( + icon: Icon(Icons.video_call_outlined), + selectedIcon: Icon(Icons.video_call), + label: 'Video', + ), + ]; + + await tester.pumpWidget( + const MaterialApp( + home: MediaQuery( + data: MediaQueryData(size: Size(700, 900)), + child: AdaptiveScaffold( + destinations: destinations, + ), + ), + ), + ); + + final Finder fNavigationRail = find.descendant( + of: find.byType(AdaptiveScaffold), + matching: find.byType(NavigationRail), + ); + final NavigationRail navigationRail = tester.firstWidget(fNavigationRail); + expect( + navigationRail.destinations, + isA>(), + ); + expect( + navigationRail.destinations.length, + destinations.length, + ); + + for (final NavigationRailDestination destination + in navigationRail.destinations) { + expect(destination.label, isNotNull); + expect(destination.icon, isA()); + expect(destination.icon, isNotNull); + expect(destination.selectedIcon, isA()); + expect(destination.selectedIcon, isNotNull); + } + + final NavigationDestination firstDestinationFromListPassed = + destinations.first; + final NavigationRailDestination firstDestinationFromFinderView = + navigationRail.destinations.first; + + expect(firstDestinationFromListPassed, isNotNull); + expect(firstDestinationFromFinderView, isNotNull); + + expect( + firstDestinationFromListPassed.icon, + equals(firstDestinationFromFinderView.icon), + ); + expect( + firstDestinationFromListPassed.selectedIcon, + equals(firstDestinationFromFinderView.selectedIcon), + ); + }, + ); + + testWidgets( + 'when tap happens on any destination, its selected icon shall be visible', + (WidgetTester tester) async { + //region Keys + const ValueKey firstDestinationIconKey = ValueKey( + 'first-normal-icon', + ); + const ValueKey firstDestinationSelectedIconKey = ValueKey( + 'first-selected-icon', + ); + const ValueKey lastDestinationIconKey = ValueKey( + 'last-normal-icon', + ); + const ValueKey lastDestinationSelectedIconKey = ValueKey( + 'last-selected-icon', + ); + //endregion + + //region Finder for destinations as per its icon. + final Finder firstDestinationWithSelectedIcon = find.byKey( + firstDestinationSelectedIconKey, + ); + final Finder lastDestinationWithIcon = find.byKey( + lastDestinationIconKey, + ); + + final Finder firstDestinationWithIcon = find.byKey( + firstDestinationIconKey, + ); + final Finder lastDestinationWithSelectedIcon = find.byKey( + lastDestinationSelectedIconKey, + ); + //endregion + + int selectedDestination = 0; + const List destinations = [ + NavigationDestination( + icon: Icon( + Icons.inbox_outlined, + key: firstDestinationIconKey, + ), + selectedIcon: Icon( + Icons.inbox, + key: firstDestinationSelectedIconKey, + ), + label: 'Inbox', + ), + NavigationDestination( + icon: Icon( + Icons.video_call_outlined, + key: lastDestinationIconKey, + ), + selectedIcon: Icon( + Icons.video_call, + key: lastDestinationSelectedIconKey, + ), + label: 'Video', + ), + ]; + + await tester.pumpWidget( + MaterialApp( + home: MediaQuery( + data: const MediaQueryData(size: Size(700, 900)), + child: StatefulBuilder( + builder: (BuildContext context, Function(Function()) setState) { + return AdaptiveScaffold( + destinations: destinations, + selectedIndex: selectedDestination, + onSelectedIndexChange: (int value) { + setState(() { + selectedDestination = value; + }); + }, + ); + }, + ), + ), + ), + ); + + expect(selectedDestination, 0); + expect(firstDestinationWithSelectedIcon, findsOneWidget); + expect(lastDestinationWithIcon, findsOneWidget); + expect(firstDestinationWithIcon, findsNothing); + expect(lastDestinationWithSelectedIcon, findsNothing); + + await tester.ensureVisible(lastDestinationWithIcon); + await tester.tap(lastDestinationWithIcon); + await tester.pumpAndSettle(); + expect(selectedDestination, 1); + + expect(firstDestinationWithSelectedIcon, findsNothing); + expect(lastDestinationWithIcon, findsNothing); + expect(firstDestinationWithIcon, findsOneWidget); + expect(lastDestinationWithSelectedIcon, findsOneWidget); + }, + ); } /// An empty widget that implements [PreferredSizeWidget] to ensure that