Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Last active February 14, 2022 20:49
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save HansMuller/0fdde5aac7a3ce6f5bf6c30978bea850 to your computer and use it in GitHub Desktop.
Save HansMuller/0fdde5aac7a3ce6f5bf6c30978bea850 to your computer and use it in GitHub Desktop.
Bottom navigation example with a Navigator per destination
// 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()));
}
@philipgiuliani
Copy link

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?

@dotnata
Copy link

dotnata commented Jan 28, 2021

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