I am trying to implement a custom navigation drawer using Flutter. I would like to attach log out option to the bottom of the drawer. The problem is that number of elements above log out option is unknow (from 3 to 17).
So if these widgets take half of the space of a drawer, then log out option will be on the bottom, and if there is too much of them and you have to scroll to see them all, then the log out option will be simply the last.
I am also trying to give the first two options a green background color. Which widget tree would you recommend me? I had a thought about the ListView widget, it takes List of widgets as an argument in constructor.
Therefore I can solve the different background color for the first two items. But I still can't figure out how to attach the log out option to the bottom. In this case it's at the bottom of drawer, but it can happen, that other options will be bigger than screen size and in that case, it should be placed at the bottom of whole list.
EDIT: I've add a design to the question. The logout option is the one called Odhlášení. In this case it's at the bottom of drawer, but it can happen, that other options will be bigger than the screen size and in that case, it should be placed at the bottom of whole list.
Design:
You can simply use ListView to manage the "17" navigation options. Wrap this ListView inside an Column. The ListView will be the first child of the Column the second child, therefore placing at the bottom, will be your logout action.
If you are using transparent widgets (like ListTile) inside your ListView to display the navigation options, you can simply wrap it inside a Container. The Container, besides many other widgets, allows you to set a new background color with its color attribute.
Using this approach the widget tree would look like the following:
- Column // Column to place your LogutButton always below the ListView
- ListView // ListView to wrap all your navigation scrollable
- Container // Container for setting the color to green
- GreenNavigation
- Container
- GreenNavigation
- Navigation
- Navigation
- ...
- LogOutButton
Update 1 - Sticky LogOutButton :
To achieve the LogOutButton sticking to the end of the ListView you'll neeed to do two things:
Replace the Expanded with an Flexible
Set shrinkWrap: true inside the ListView
Update 2 - Spaced LogOutButton until large List:
Achieving the described behavior is a more difficult step. You'll have to check if the ListView exceeds the screen and is scrollable.
To do this I wrote this short snippet:
bool isListLarge() {
return controller.positions.isNotEmpty && physics.shouldAcceptUserOffset(controller.position);
}
It will return true if the ListView exceeds its limitations. Now we can refresh the state of the view, depending on the result of isListViewLarge. Below again a full code example.
Standalone code example (Update 2: Spaced LogOutButton until large List):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
drawer: MyDrawer(),
),
);
}
}
class MyDrawer extends StatefulWidget {
#override
_MyDrawerState createState() => _MyDrawerState();
}
class _MyDrawerState extends State<MyDrawer> {
ScrollController controller = ScrollController();
ScrollPhysics physics = ScrollPhysics();
int entries = 4;
#override
Widget build(BuildContext context) {
Widget logout = IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: () => {setState(() => entries += 4)});
List<Widget> navigationEntries = List<int>.generate(entries, (i) => i)
.map<Widget>((i) => ListTile(
title: Text(i.toString()),
))
.toList();
if (this.isListLarge()) { // if the List is large, add the logout to the scrollable list
navigationEntries.add(logout);
}
return Drawer(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // place the logout at the end of the drawer
children: <Widget>[
Flexible(
child: ListView(
controller: controller,
physics: physics,
shrinkWrap: true,
children: navigationEntries,
),
),
this.isListLarge() ? Container() : logout // if the List is small, add the logout at the end of the drawer
],
),
);
}
bool isListLarge() {
return controller.positions.isNotEmpty && physics.shouldAcceptUserOffset(controller.position);
}
}
Standalone code example (Update 1: Sticky LogOutButton):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
drawer: MyDrawer(),
),
);
}
}
class MyDrawer extends StatefulWidget {
#override
_MyDrawerState createState() => _MyDrawerState();
}
class _MyDrawerState extends State<MyDrawer> {
int entries = 4;
#override
Widget build(BuildContext context) {
return Drawer(
child: Column(
children: <Widget>[
Flexible(
child: ListView(
shrinkWrap: true,
children: List<int>.generate(entries, (i) => i)
.map((i) => ListTile(
title: Text(i.toString()),
))
.toList(),
),
),
IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: () => {setState(() => entries += 4)})
],
),
);
}
}
Standalone code example (Old: Sticking to bottom):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
drawer: MyDrawer(),
),
);
}
}
class MyDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Drawer(
child: Column(
children: <Widget>[
Expanded(
child: ListView(
children: List<int>.generate(40, (i) => i + 1)
.map((i) => ListTile(
title: Text(i.toString()),
))
.toList(),
),
),
IconButton(icon: Icon(Icons.exit_to_app), onPressed: () => {})
],
),
);
}
}
Related
let's say I have an app with the following setup:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: MainLoginScreen()),
],
),
));
}
}
I would like to know how can I navigate only the MainLoginScreen widget from the MainMenu with any .push() method.
(I found a way to navigate from a context inside the mainloginscreen,by wrapping it with a MaterialApp widget, but what if I want to use the MainMenu widget instead, which has another context)
There is a general agreement that a 'screen' is a topmost widget in the route. An instance of 'screen' is what you pass to Navigator.of(context).push(MaterialPageRoute(builder: (context) => HereGoesTheScreen()). So if it is under Scaffold, it is not a screen. That said, here are the options:
1. If you want to use navigation with 'back' button
Use different screens. To avoid code duplication, create MenuAndContentScreen class:
class MenuAndContentScreen extends StatelessWidget {
final Widget child;
MenuAndContentScreen({
required this.child,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: child),
],
),
),
);
}
}
Then for each screen create a pair of a screen and a nested widget:
class MainLoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MenuAndContentScreen(
child: MainLoginWidget(),
);
}
}
class MainLoginWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Here goes the screen content.
}
}
2. If you do not need navigation with 'back' button
You may use IndexedStack widget. It can contain multiple widgets with only one visible at a time.
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState(
initialContentIndex: 0,
);
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
int _index;
_MainMenuAndContentScreenState({
required int initialContentIndex,
}) : _contentIndex = initialContentIndex;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
// A callback that will be triggered somewhere down the menu
// when an item is tapped.
setContentIndex: _setContentIndex,
),
Expanded(
child: IndexedStack(
index: _contentIndex,
children: [
MainLoginWidget(),
SomeOtherContentWidget(),
],
),
),
],
),
),
);
}
void _setContentIndex(int index) {
setState(() {
_contentIndex = index;
});
}
}
The first way is generally preferred as it is declrative which is a major idea in Flutter. When you have the entire widget tree statically declared, less things can go wrong and need to be tracked. Once you feel it, it really is a pleasure. And if you want to avoid back navigation, use replacement as ahmetakil has suggested in a comment: Navigator.of(context).pushReplacement(...)
The second way is mostly used when MainMenu needs to hold some state that needs to be preserved between views so we choose to have one screen with interchangeable content.
3. Using a nested Navigator widget
As you specifically asked about a nested Navigator widget, you may use it instead of IndexedStack:
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _navigatorKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
navigatorKey: _navigatorKey,
),
Expanded(
child: Navigator(
key: _navigatorKey,
onGenerateRoute: ...
),
),
],
),
),
);
}
}
// Then somewhere in MainMenu:
final anotherContext = navigatorKey.currentContext;
Navigator.of(anotherContext).push(...);
This should do the trick, however it is a bad practice because:
MainMenu knows that a particular Navigator exists and it should interact with it. It is better to either abstract this knowledge with a callback as in (2) or do not use a specific navigator as in (1). Flutter is really about passing information down the tree and not up.
At some point you would like to highlight the active item in MainMenu, but it is hard for MainMenu to know which widget is currently in the Navigator. This would add yet another non-down interaction.
For such interaction there is BLoC pattern
In Flutter, BLoC stands for Business Logic Component. In its simpliest form it is a plain object that is created in the parent widget and then passed down to MainMenu and Navigator, these widgets may then send events through it and listen on it.
class CurrentPageBloc {
// int is an example. You may use String, enum or whatever
// to identify pages.
final _outCurrentPageController = BehaviorSubject<int>();
Stream<int> _outCurrentPage => _outCurrentPageController.stream;
void setCurrentPage(int page) {
_outCurrentPageController.sink.add(page);
}
void dispose() {
_outCurrentPageController.close();
}
}
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _currentPageBloc = CurrentPageBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
currentPageBloc: _currentPageBloc,
),
Expanded(
child: ContentWidget(
currentPageBloc: _currentPageBloc,
onGenerateRoute: ...
),
),
],
),
),
);
}
#override
void dispose() {
_currentPageBloc.dispose();
}
}
// Then in MainMenu:
currentPageBloc.setCurrentPage(1);
// Then in ContentWidget's state:
final _navigatorKey = GlobalKey();
late final StreamSubscription _subscription;
#override
void initState() {
super.initState();
_subscription = widget.currentPageBloc.outCurrentPage.listen(_setCurrentPage);
}
#override
Widget build(BuildContext context) {
return Navigator(
key: _navigatorKey,
// Everything else.
);
}
void _setCurrentPage(int currentPage) {
// Can't use this.context, because the Navigator's context is down the tree.
final anotherContext = navigatorKey?.currentContext;
if (anotherContext != null) { // null if the event is emitted before the first build.
Navigator.of(anotherContext).push(...); // Use currentPage
}
}
#override
void dispose() {
_subscription.cancel();
}
This has advantages:
MainMenu does not know who will receive the event, if anybody.
Any number of listeners may listen on such events.
However, there is still a fundamental flaw with Navigator. It can be navigated without MainMenu knowledge using 'back' button or by its internal widgets. So there is no single variable that knows which page is showing now. To highlight the active menu item, you would query the Navigator's stack which eliminates the benefits of BLoC.
For all these reasons I still suggest one of the first two solutions.
I have created a ListView with container boxes as widgets. I want a specific container to expand onTap upto a specific screen height and width. I need help in implementing this in flutter. I have made a prototype on AdobeXD.
AdobeXD Prototype GIF
I am new to flutter, any kind of help is appreciated.
A flutter plugin called flutter swiper might help you achieve what you want to achieve.
Visit this pub dev and you can read documentation.
Here you go brother, Although its not blurring the background but I think it will get you going.
It's working something like this:
Below the code which you can copy paste. I have added comments in the code for understanding it in better way. Cheers :)
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeApp(),
);
}
}
class HomeApp extends StatefulWidget {
#override
_HomeAppState createState() => _HomeAppState();
}
class _HomeAppState extends State<HomeApp> {
// Items in the list --> Custom Widgets
List<Widget> arr = [
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
];
Widget getListWidget(List<Widget> items) {
List<Widget> list = new List<Widget>();
for (var i = 0; i <= items.length; i++) {
list.add(new ListContainerHere(
index: i,
));
}
return Row(children: list);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter App :)"),
),
body: Center(
// Using a 'Row' as Horizontal ListView
child: SingleChildScrollView(
scrollDirection: Axis.horizontal, child: getListWidget(arr)),
),
);
}
}
// Widgets that will be rendered in the Horizontal Row
class ListContainerHere extends StatefulWidget {
final int index;
ListContainerHere({this.index});
#override
_ListContainerHereState createState() => _ListContainerHereState();
}
class _ListContainerHereState extends State<ListContainerHere> {
// Varibale to change the height and width accordingly
// Initally no item will be expanded
bool isExpanded = false;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
// Changing the value of 'isExpanded' when an item is tapped in the List
setState(() {
isExpanded = !isExpanded;
});
},
// AnimatedContainer for slowing down the changing
child: AnimatedContainer(
duration: Duration(milliseconds: 150),
// Changing the width and height
height: isExpanded ? 250 : 150,
width: isExpanded ? 250 : 150,
// Decoration Portion of the Container
decoration: BoxDecoration(
color: Colors.blue, borderRadius: BorderRadius.circular(15.0)),
),
),
);
}
}
I have a small flutter application with several stateful and stateless widgets.
My drawer widget looks like follows:
class AppDrawer extends StatelessWidget{
#override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
_createDrawerHeader(),
ListTile(
title: Text('Scenario'),
onTap: () {
Navigator.pop(context);
Navigator.pushReplacementNamed(context, Routes.scenario);
},
),
...
This works fine for switche between my widgets. I now have a simple counter widget that stores a counter variable in its state.
class Counter extends StatefulWidget {
static const String routeName = '/Counter';
int _counter = 0;
#override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
void _increment() {
setState(() {
widget._counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: ${widget._counter}'),
],
),
drawer: AppDrawer(),
);
}
}
If I switch between this and my other widgets with the drawer and go back to the counter widgets the counter is always 0. Looks like the state is initialized everytime.
I am a beginner using flutter and I thougt I can save a variable within this state. I think I am wrong. As my search didn't get my some usable results maybe you can give me an idea how to solve this or just provide some links with information.
Thanks for your help :)
I suggest learning some sort of State Management practice's. You will eventually need to learn this because this is an essential step.Try provider its easy and simple to use.
Hello I am new to Flutter and I am trying to implement a bottom tab bar with multiple navigation screen for each tab.
Here is my initial set up
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return CupertinoApp(home: HomeScreen(),
routes: {
Screen1.id: (context) => Screen1(),
Screen2.id: (context) => Screen1(),
DetailScreen3.id: (context) => DetailScreen3(),
DetailScreen4.id: (context) => DetailScreen4(),
});
}
}
Here is my HomeScreen
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: [
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.book_solid),
title: Text('Articles'),
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.eye_solid),
title: Text('Views'),
),
],
),
tabBuilder: (context, index) {
if (index == 0) {
return Screen1();
} else {
return Screen2();
}
},
);
}
}
Here is my screen1
class Screen1 extends StatelessWidget {
static const String id = 'screen1';
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: GestureDetector(
onTap: () {
Navigator.pushNamed(context, DetailScreen3.id);
},
child: Center(
child: Text('Screen 1',),
),
),
);
}
}
and here is my screen3
class DetailScreen3 extends StatelessWidget {
static const String id = 'screen3';
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: Center(
child: Text('terzo schermo',),
),
);
}
}
The tabBar work ok and I am able to swap between the 2 tabs but I am not able to navigate from screen 1 to screen 3. When I tap on screen1 Center widget, the screen start to navigate but half way it stops and then the screen become all black...
Here is the error
There are multiple heroes that share the same tag within a subtree.
Within each subtree for which heroes are to be animated (i.e. a
PageRoute subtree), each Hero must have a unique non-null tag. In this
case, multiple heroes had the following tag: Default Hero tag for
Cupertino navigation bars with navigator NavigatorState#05492(tickers:
tracking 2 tickers)
I understand the problem is related to the hero tag of the navigation bar which must have a unique identifier. How should I fix this problem? should I assign an heroTag to all navigation bar???
Many thanks in advance for the help
I resolved by setting the following properties for each CupertinoNavigationBar
heroTag: 'screen1', // a different string for each navigationBar
transitionBetweenRoutes: false,
As an iOS developer, I tried flutter for the first time, this thing caused a black screen after jumping the page, and also troubled me for two days
heroTag: 'screen1', // a different string for each navigationBar
transitionBetweenRoutes: false,
I am very new to Flutter but coming from an Angular background here. Say I want to build a BaseFrame for my app, and depending on the routing, I want to change the INSIDE of that BaseFrame. I don't understand how this would work?
In Angular it would be something like:
'/page-one': PageOne(),
'/page-two': PageTwo(), children: [
'/part-1': Part1(),
'/part-2': Part2(),
];
And in this case, if you navigate to /page-two/part-1 then it would load PageTwo(), and it would load Part1() wherever you specified <app-route></app-route> on PageTwo(). I don't understand how one would do this in Flutter because in Flutter it seems like you can only ever have a single flat route in your main.dart (in your MaterialApp builder)
The best thing I can think of is to have a variable on PageTwo(), and have a type of switch statement:
switch (subPage) {
case '/part-1':
return Part1();
case '/part-2':
return Part2();
}
But this seems like a crappy solution. You also now have the problem of fixing animations etc yourself (because the MaterialApp won't help you here automatically).
Here is something about this but this seems super complicated for a noob. Is this really the right/only way to do this:
Nesting routes with flutter
You can do something like that using this package qlevar_router
A full example of your case
import 'package:flutter/material.dart';
import 'package:qlevar_router/qlevar_router.dart';
void main() {
runApp(MyApp());
}
class AppRoutes {
final routes = <QRouteBase>[
QRoute(
path: '/page-one',
page: (c) => PageOne(),
),
QRoute(
path: '/page-two',
page: (c) => PageTwo(c),
initRoute: '/part-1',
children: [
QRoute(
path: '/part-1',
page: (c) => Part1(),
),
QRoute(
path: '/part-2',
page: (c) => Part2(),
)
])
];
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: QR.router(AppRoutes().routes, initRoute: '/page-one'),
routeInformationParser: QR.routeParser(),
);
}
}
class PageOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(children: [
Text('This is page one'),
TextButton(
onPressed: () => QR.to('/page-two/part-1'),
child: Text('To Part 1')),
TextButton(
onPressed: () => QR.to('/page-two/part-2'),
child: Text('To Part 2')),
]),
),
);
}
}
class PageTwo extends StatelessWidget {
final QRouteChild child;
PageTwo(this.child);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(children: [
Text('This is page one'),
TextButton(onPressed: () => QR.back(), child: Text('Back')),
TextButton(
onPressed: () => QR.to('/page-two/part-1'),
child: Text('To Part 1')),
TextButton(
onPressed: () => QR.to('/page-two/part-2'),
child: Text('To Part 2')),
SizedBox(
width: 500,
height: 500,
child: child.childRouter,
)
]),
),
);
}
}
class Part1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(child: Text('This is part 1'));
}
}
class Part2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(child: Text('This is part 2'));
}
}
Note that page-tow doesn't change at all when you navigate between the children part-1 and part-2
You could just use something like a TabBar/TabBarView to have other pages show up within the pages you're on. Which then you could give an integer as a parameter of your PageTwo class which you could use that integer to select which page you initially navigate to. This is a fairly straightforward way to do what I think you want but more limited since you won't have different navigators within each of those initial pages. Unless the Parts pages are meant to have their own navigation. If you want to have separate navigators for screens you can do this. That's how the CupertinoTabbedScaffold works (though with CupertinoApps). Which may be something to look into.