Last active
February 14, 2022 20:49
-
-
Save HansMuller/0fdde5aac7a3ce6f5bf6c30978bea850 to your computer and use it in GitHub Desktop.
Bottom navigation example with a Navigator per destination
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2014 The Flutter Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
// Flutter code sample for NavigationBar with nested Navigator destinations. | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
class Destination { | |
const Destination(this.index, this.title, this.icon, this.color); | |
final int index; | |
final String title; | |
final IconData icon; | |
final MaterialColor color; | |
} | |
class RootPage extends StatelessWidget { | |
const RootPage({ Key? key, required this.destination }) : super(key: key); | |
final Destination destination; | |
Widget _buildDialog(BuildContext context) { | |
return AlertDialog( | |
title: Text('${destination.title} AlertDialog'), | |
actions: <Widget>[ | |
TextButton( | |
onPressed: () { Navigator.pop(context, 'OK'); }, | |
child: const Text('OK'), | |
), | |
], | |
); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final TextStyle headline5 = Theme.of(context).textTheme.headline5!; | |
final ButtonStyle buttonStyle = ElevatedButton.styleFrom( | |
primary: destination.color, | |
visualDensity: VisualDensity.comfortable, | |
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), | |
textStyle: headline5, | |
); | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('${destination.title} RootPage - /'), | |
backgroundColor: destination.color, | |
), | |
backgroundColor: destination.color[50], | |
body: Center( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
ElevatedButton( | |
style: buttonStyle, | |
onPressed: () { | |
Navigator.pushNamed(context, '/list'); | |
}, | |
child: const Text('Push /list'), | |
), | |
const SizedBox(height: 16), | |
ElevatedButton( | |
style: buttonStyle, | |
onPressed: () { | |
showDialog( | |
context: context, | |
useRootNavigator: false, | |
builder: _buildDialog, | |
); | |
}, | |
child: const Text('Local Dialog'), | |
), | |
const SizedBox(height: 16), | |
ElevatedButton( | |
style: buttonStyle, | |
onPressed: () { | |
showDialog( | |
context: context, | |
useRootNavigator: true, | |
builder: _buildDialog, | |
); | |
}, | |
child: const Text('Root Dialog'), | |
), | |
const SizedBox(height: 16), | |
Builder( | |
builder: (BuildContext context) { | |
return ElevatedButton( | |
style: buttonStyle, | |
onPressed: () { | |
showBottomSheet( | |
context: context, | |
builder: (BuildContext context) { | |
return Container( | |
padding: const EdgeInsets.all(16), | |
width: double.infinity, | |
child: Text( | |
'${destination.title} BottomSheet\n' | |
'Tap the back button to dismiss', | |
style: headline5, | |
softWrap: true, | |
textAlign: TextAlign.center, | |
), | |
); | |
}, | |
); | |
}, | |
child: const Text('Local BottomSheet'), | |
); | |
}, | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class ListPage extends StatelessWidget { | |
const ListPage({ Key? key, required this.destination }) : super(key: key); | |
final Destination destination; | |
@override | |
Widget build(BuildContext context) { | |
const int itemCount = 50; | |
final ButtonStyle buttonStyle = OutlinedButton.styleFrom( | |
primary: destination.color, | |
fixedSize: const Size.fromHeight(128), | |
textStyle: Theme.of(context).textTheme.headline5, | |
); | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('${destination.title} ListPage - /list'), | |
backgroundColor: destination.color, | |
), | |
backgroundColor: destination.color[50], | |
body: SizedBox.expand( | |
child: ListView.builder( | |
itemCount: itemCount, | |
itemBuilder: (BuildContext context, int index) { | |
return Padding( | |
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), | |
child: OutlinedButton( | |
style: buttonStyle.copyWith( | |
backgroundColor: MaterialStateProperty.all<Color>( | |
Color.lerp(destination.color[100], Colors.white, index / itemCount)! | |
), | |
), | |
onPressed: () { | |
Navigator.pushNamed(context, '/text'); | |
}, | |
child: Text('Push /text [$index]'), | |
), | |
); | |
}, | |
), | |
), | |
); | |
} | |
} | |
class TextPage extends StatefulWidget { | |
const TextPage({ Key? key, required this.destination }) : super(key: key); | |
final Destination destination; | |
@override | |
State<TextPage> createState() => _TextPageState(); | |
} | |
class _TextPageState extends State<TextPage> { | |
late final TextEditingController textController; | |
@override | |
void initState() { | |
super.initState(); | |
textController = TextEditingController(text: 'Sample Text'); | |
} | |
@override | |
void dispose() { | |
textController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final ThemeData theme = Theme.of(context); | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('${widget.destination.title} TextPage - /list/text'), | |
backgroundColor: widget.destination.color, | |
), | |
backgroundColor: widget.destination.color[50], | |
body: Container( | |
padding: const EdgeInsets.all(32.0), | |
alignment: Alignment.center, | |
child: TextField( | |
controller: textController, | |
style: theme.primaryTextTheme.headline4?.copyWith( | |
color: widget.destination.color, | |
), | |
decoration: InputDecoration( | |
focusedBorder: UnderlineInputBorder( | |
borderSide: BorderSide( | |
color: widget.destination.color, | |
width: 3.0, | |
), | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
class DestinationView extends StatefulWidget { | |
const DestinationView({ | |
Key? key, | |
required this.destination, | |
required this.navigatorKey, | |
}) : super(key: key); | |
final Destination destination; | |
final Key navigatorKey; | |
@override | |
State<DestinationView> createState() => _DestinationViewState(); | |
} | |
class _DestinationViewState extends State<DestinationView> { | |
@override | |
Widget build(BuildContext context) { | |
return Navigator( | |
key: widget.navigatorKey, | |
onGenerateRoute: (RouteSettings settings) { | |
return MaterialPageRoute<void>( | |
settings: settings, | |
builder: (BuildContext context) { | |
switch(settings.name) { | |
case '/': | |
return RootPage(destination: widget.destination); | |
case '/list': | |
return ListPage(destination: widget.destination); | |
case '/text': | |
return TextPage(destination: widget.destination); | |
} | |
assert(false); | |
return const SizedBox(); | |
}, | |
); | |
}, | |
); | |
} | |
} | |
class Home extends StatefulWidget { | |
const Home({ Key? key }) : super(key: key); | |
@override | |
State<Home> createState() => _HomeState(); | |
} | |
class _HomeState extends State<Home> with TickerProviderStateMixin<Home> { | |
static const List<Destination> allDestinations = <Destination>[ | |
Destination(0, 'Teal', Icons.home, Colors.teal), | |
Destination(1, 'Cyan', Icons.business, Colors.cyan), | |
Destination(2, 'Orange', Icons.school, Colors.orange), | |
Destination(3, 'Blue', Icons.flight, Colors.blue) | |
]; | |
late final List<GlobalKey<NavigatorState>> navigatorKeys; | |
late final List<GlobalKey> destinationKeys; | |
late final List<AnimationController> destinationFaders; | |
late final List<Widget> destinationViews; | |
int selectedIndex = 0; | |
AnimationController buildFaderController() { | |
final AnimationController controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 200)); | |
controller.addStatusListener((AnimationStatus status) { | |
if (status == AnimationStatus.dismissed) { | |
setState(() { }); // Rebuild unselected destinations offstage. | |
} | |
}); | |
return controller; | |
} | |
@override | |
void initState() { | |
super.initState(); | |
navigatorKeys = List<GlobalKey<NavigatorState>>.generate(allDestinations.length, (int index) => GlobalKey()).toList(); | |
destinationFaders = List<AnimationController>.generate(allDestinations.length, (int index) => buildFaderController()).toList(); | |
destinationFaders[selectedIndex].value = 1.0; | |
destinationViews = allDestinations.map((Destination destination) { | |
return FadeTransition( | |
opacity: destinationFaders[destination.index].drive(CurveTween(curve: Curves.fastOutSlowIn)), | |
child: KeyedSubtree( | |
key: GlobalKey(), | |
child: DestinationView( | |
destination: destination, | |
navigatorKey: navigatorKeys[destination.index], | |
), | |
) | |
); | |
}).toList(); | |
} | |
@override | |
void dispose() { | |
for (final AnimationController controller in destinationFaders) { | |
controller.dispose(); | |
} | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return WillPopScope( | |
onWillPop: () async { | |
final NavigatorState navigator = navigatorKeys[selectedIndex].currentState!; | |
if (!navigator.canPop()) { | |
return true; | |
} | |
navigator.pop(); | |
return false; | |
}, | |
child: Scaffold( | |
body: SafeArea( | |
top: false, | |
child: Stack( | |
fit: StackFit.expand, | |
children: allDestinations.map((Destination destination) { | |
final int index = destination.index; | |
final Widget view = destinationViews[index]; | |
if (index == selectedIndex) { | |
destinationFaders[index].forward(); | |
return view; | |
} else { | |
destinationFaders[index].reverse(); | |
if (destinationFaders[index].isAnimating) { | |
return IgnorePointer(child: view); | |
} | |
return Offstage(child: view); | |
} | |
}).toList(), | |
), | |
), | |
bottomNavigationBar: NavigationBar( | |
selectedIndex: selectedIndex, | |
onDestinationSelected: (int index) { | |
setState(() { | |
selectedIndex = index; | |
}); | |
}, | |
destinations: allDestinations.map((Destination destination) { | |
return NavigationDestination( | |
icon: Icon(destination.icon, color: destination.color), | |
label: destination.title, | |
); | |
}).toList(), | |
), | |
), | |
); | |
} | |
} | |
void main() { | |
runApp(const MaterialApp(home: Home())); | |
} |
Hi, I need to return a value from the "Text" page to the parent page but WillPopScope is not working for me from the "Text" page. Any idea how to fix this?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
When using a
TabBarView
as a child in a destination, the BottomNavigationBar disappears when swiping horizontally. You have an idea how we could fix that?