How to implement and extend from a Base Page in Flutter - flutter

Let's say I have a base page in a Material App.
The basepage only has one widget, a scaffold.
The scaffold contains an appbar, that is to remain constant through the app, in every page.
The scaffold also contains a body, which should be overriden by the pages that extend the base to display their contents.
How can I go about to do this?
Thanks for the help!

You could create a globally accessible page like this:
base_page.dart
import 'package:flutter/material.dart';
class BasePage extends StatelessWidget {
/// Body of [BasePage]
final Widget body;
const BasePage({#required this.body, Key key})
: assert(body != null),
super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Your appbar content here
),
body: body,
);
}
}
And when you want to use it, just provide the body to the new class like this:
main.dart
import 'package:flutter/material.dart';
import 'base_page.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BasePage(
// This is where you give you custom widget it's data.
body: Center(child: Text('Hello, World')),
);
}
}

One way to do this is to create a variable holding the current route in your StatefulWidget.
Widget currentBody = your initial body ;
and then change that variable whenever you want to switch the body using setState:
SetState(() { currentBody = your new body widget }) ;
and in your scaffold after the appbar you put !
body : currentBody ;

You have many ways to do this, one is to use the Bloc package, but another way is to use a Bottom Navigation Bar](https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html)
The bottom navigation bar consists of multiple items in the form of text labels, icons, or both, laid out on top of a piece of material. It provides quick navigation between the top-level views of an app. For larger screens, side navigation may be a better fit.
A bottom navigation bar is usually used in conjunction with a Scaffold, where it is provided as the Scaffold.bottomNavigationBar argument.
I provided an example in my answer for transparent appbar, of course you do not need your appbar to be transparent.
class HomePageState extends State<Homepage> {
List<Widget> widgets = [Text("haha"), Placeholder(), Text("hoho")]; // as many widgets as you have buttons.
Widget currentWidget = widgets[0];
void _onItemTapped(int index) {
setState(() {
NavigationBar._selectedIndex = index;
currentWidget = widgets[index];
});
}
#override
Widget build(BuildContext context) {
return return MaterialApp(
home: Scaffold(
extendBody: true, // very important as noted
bottomNavigationBar: BottomNavigationBar(
currentIndex: NavigationBar._selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
backgroundColor: Color(0x00ffffff), // transparent
type: BottomNavigationBarType.fixed,
unselectedItemColor: Colors.blue,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.grade),
title: Text('Level'),
),
[...] // remaining in the link
),
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: ExactAssetImage("assets/background.png"), // because if you want a transparent navigation bar I assume that you have either a background image or a background color.
fit: BoxFit.fill
),
),
child: currentWidget
),
),
);
}
}
...
The Bloc architecture is harder to understand, you will need to read documentation and try tutorials, but it is also very interesting to implement.

Related

How to put two floating-like Slivers inside a CustomScrollView with SliverAppBar-like behavior?

Say we have the following, simple CustomScrollView:
A SliverAppBar
Widget 1
Widget 2
Widget 3
The previous example must be able to meet the following requirements:
The SliverAppBar must have floating: true, so it appears when we scroll up again. Easy enough;
Widget 1 and Widget 2 should behave something like the SliverAppBar. Let me elaborate this:
These two widgets aren't an AppBar, which means that no drawer should be painted on them, no insets or anything else should be reserved. They're plain widgets, I want to use my own implementations there;
When I scroll down, I expect those two widget to just scroll and disappear... Again, easy enough;
When I scroll up again, though, these two widgets should appear one after the other on the screen like so: SliverAppBar -> Widget 1 -> Widget 2, without needing to scroll to the top, i.e. behave just like the SliverAppBar with the floating:true option, but they must respect the aforementioned scroll order.
Widget 1 and Widget 2 are hideable: see example below;
Widget 3 is just the actual content, and behaves like a normal scrollable widget.
Here's the code of what I have right now. I tried to implement Widget 1 and Widget 2 with different approaches:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
var showGreen = false;
var showRed = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
drawer: const Drawer(),
body: Center(
child: CustomScrollView(
slivers: [
SliverAppBar(
title: const Text('My app bar'),
floating: true,
actions: [
IconButton(
icon: const Icon(Icons.grade_outlined),
tooltip: 'Add',
onPressed: () {
setState(() {
showGreen = !showGreen;
});
},
),
IconButton(
icon: const Icon(Icons.grade),
tooltip: 'Remove',
onPressed: () {
setState(() {
showRed = !showRed;
});
},
),
],
),
if (showGreen)
SliverAppBar(
toolbarHeight:
200, // I don't want hard-coded values in here!!
leadingWidth: 0,
titleSpacing: 0,
floating: true,
title: Container(
height: 200, // I don't want hard-coded values in here!!
color: Colors.green,
child: const Placeholder(),
),
),
SliverPersistentHeader(
floating: true,
delegate: MyDelegate(showRed),
),
SliverToBoxAdapter(
child: Container(
color: Colors.blue,
height: 2500,
child: const Placeholder(),
),
),
],
),
),
),
);
}
}
class MyDelegate extends SliverPersistentHeaderDelegate {
final bool showContents;
const MyDelegate(this.showContents) : super();
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return showContents
? Container(
color: Colors.red,
child: const Text(
"my contents",
style: TextStyle(fontSize: 36),
),
)
: const SizedBox.shrink();
}
#override
// But this is not what I want! I want the maxExtent to be as much as the build() method needs!
// I DO NOT want to hard code heights here!
double get maxExtent => 200;
#override
// For some reason... the content isn't disappearing right away and instead I get an overflow error?
double get minExtent => 0;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
Here's what I tried so far, with no luck:
I tried to implement Widget 1 and Widget 2 as two SliverAppBars, but:
Removing the Drawer is hacky (see example: you have to force the title to expand): is it actually there and just not shown, or did the framework remove that button entirely?
I want to leave these Widgets' height unbounded (no, I don't want anything fixed, I need their height to fit the content in a Flexible way); inserting anything besides a Placeholder breaks the AppBar as I'm not able to tell how much is needed in my contents beforehand!
This still feels like a hacky solution. I don't want to have three SliverAppBars semantically speaking (!)
This seems to be the "as-close-as-I-can-get" behavior, tho
I tried SliverLists, SliverGrids... the behavior is impossible to reproduce with the "out-of-the-box" Widgets;
I tried using a SliverPersistentHeader and its SliverPersistentHeaderDelegate, but I had no luck. I can't seem to understand how to consistently reproduce what I want with the maxExtent and minExtent parameters: I couldn't care less for fixed values (and as you see in the example, it just won't work well). Also, the floating behavior is just not there.
I am so lost with this one. Is anyone able to answer this?

Flutter change a "shared" widget as the route changes, like the navBar of facebook.com

I don't know if I used correct terms in the title. I meant share by being displayed in diffrent pages with the same state, so that even if I push a new page, the “shared” widget will stay the same.
I'm trying to share the same widget across several pages, like the navigation bar of facebook.com.
As I know, Navigator widget allows to build up a seperate route. I've attempted to use the widget here, and it works quite well.
...
Scaffold(
body: Stack(
children: [
Navigator(
key: navigatorKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) => MainPage());
},
// observers: <RouteObserver<ModalRoute<void>>>[ routeObserver ],
),
Positioned(
bottom: 0,
child: BottomBarWithRecord(),
)
],
));
...
To summarize the situation, there used to be only one root Navigator (I guess it's provided in MaterialApp, but anyway), and I added another Navigator in the route under a Stack (which always display BottomBarWithRecord).
This code works perfect as I expected, that BottomBarWithRecord stays the same even if I open a new page in that new Navigator. I can also open a new page without BottomBarWithRecord by pushing the page in the root Navigator: Navigator.of(context, rootNavigator: true).push(smthsmth)
However, I couldn't find a way to change BottomBarWithRecord() as the route changes, like the appbar of facebook.com.
What I've tried
Subscribe to route using navigator key
As I know, to define a navigator key, I have to write final navigatorKey = GlobalObjectKey<NavigatorState>(context);. This doesn't seem to have addListener thing, so I couldn't find a solution here
Subscribe to route using navigator observer
It was quite complicated. Normally, a super complicated solutions works quite well, but it didn't. By putting with RouteAware after class ClassName, I could use some functions like void didPush() {} didPop() didPushNext to subscribe to the route. However, it was not actually "subscribing" to the route change; it was just checking if user opened this page / opened a new page from this page / ... , which would be complicated to deal with in my situation.
React.js?
When I learned a bit of js with React, I remember that this was done quite easily; I just had to put something like
...
const [appBarIndex, setAppBarIndex] = useState(0);
//0 --> highlight Home icon, 1 --> highlight Chats icon, 2 --> highlight nothing
...
window.addEventListener("locationChange", () => {
//location is the thing like "/post/postID/..."
if (window.location == "/chats") {
setAppBarIndex(1);
} else if (window.location == "/") {
setAppBarIndex(0);
} else {
setAppBarIndex(2);
}
})
Obviously I cannot use React in flutter, so I was finding for a similar easy way to do it on flutter.
How can I make the shared BottomBarWithRecord widget change as the route changes?
Oh man it's already 2AM ahhhh
Thx for reading this till here, and I gotta go sleep rn
If I've mad e any typo, just ignore them
You can define a root widget from which you'll control what screen should be displayed and position the screen and the BottomBar accordingly. So instead of having a Navigator() and BottomBar() inside your Stack, you'll have YourScreen() and BottomBar().
Scaffold(
body: SafeArea(
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: _buildScreen(screenIndex),
),
Align(
alignment: Alignment.bottomCenter,
child: BottomBar(
screenIndex,
onChange: (newIndex) {
setState(() {
screenIndex = newIndex;
});
},
),
),
],
),
),
)
BotttomBar will use the screenIndex passed to it to do what you had in mind and highlight the selected item.
_buildScreen will display the corresponding screen based on screenIndex and you pass the onChange to your BottomBar so that it can update the screen if another item was selected. You won't be using Navigator.of(context).push() in this case unless you want to route to a screen without the BottomBar. Otherwise the onChange passed to BottomBar will be responsible for updating the index and building the new screen.
This is how you could go about it if you wanted to implement it yourself. This package can do what you want as well. Here is a simple example:
class Dashboard extends StatefulWidget {
const Dashboard({Key? key}) : super(key: key);
#override
State<Dashboard> createState() => _DashboardState();
}
class _DashboardState extends State<Dashboard> {
final PersistentTabController _controller = PersistentTabController(initialIndex: 0);
#override
Widget build(BuildContext context) {
return PersistentTabView(
context,
controller: _controller,
screens: _buildScreens(),
items: _navBarsItems(),
);
}
List<Widget> _buildScreens() {
return [
const FirstScreen(),
const SecondScreen(),
];
}
List<PersistentBottomNavBarItem> _navBarsItems() {
return [
PersistentBottomNavBarItem(
icon: const Icon(Icons.home),
title: ('First Screen'),
),
PersistentBottomNavBarItem(
icon: const Icon(Icons.edit),
title: ('Second Screen'),
),
];
}
}
class FirstScreen extends StatelessWidget {
const FirstScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Center(
child: Text('First Screen'),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Center(
child: Text('Second Screen'),
);
}
}

BottomNavigationBar and Provider?

I used the bottom navigation bar and added 3 buttons to the bottom.I was swapping pages with setsate. These buttons work fine. But if I call another page inside the pages called with those buttons, the BottomNavigationBar disappears. I've researched it and found this click me. Is it logical to use Provider to call pages? I think it keeps every page in memory, doesn't it consume too much ram?
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int selectedPage = 0;
final _pageOptions = [
HomeScreen(),
InboxScreen(),
SignInScreen()
];
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: _pageOptions[selectedPage],
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home, size: 30), title: Text('Home')),
BottomNavigationBarItem(icon: Icon(Icons.mail, size: 30), title: Text('Inbox')),
BottomNavigationBarItem(icon: Icon(Icons.account_circle, size: 30), title: Text('Account')),
],
selectedItemColor: Colors.green,
elevation: 5.0,
unselectedItemColor: Colors.green[900],
currentIndex: selectedPage,
backgroundColor: Colors.white,
onTap: (index){
setState(() {
selectedPage = index;
});
},
)
);
}
}
This is my home page.BottomNavigationBar disappears If I clicked on the text
class _HomePage extends State<HomePage> {
#override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(appBar: AppBar(title: Text('Page2')),body:GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => NewScreen()));
},
child:Text('clik')));
}
}
I want to like this
But my code run this
The code you posted looks perfectly fine !
Your problem must be elsewhere, probably in one of the Widgets
final _pageOptions = [
HomeScreen(),
InboxScreen(),
SignInScreen()
];
There can be many root causes to your problem, you could start by identifying which Widget is making the bottomNavigationBar disappear.
Once you found it out, be sure that you didn't use the method Navigator.of(context).push which could be one of the reasons why your bottomNavigationbar is disappearing.
If you're still stuck, feel free to update your question with the source code of the Widget making the bar disappear and I'll update my answer
EDIT
As I mentioned earlier, whenever you call the Navigator.push(context, UserProfilePage); method, you're replacing your previous widget with a new one.
Since your previous Widget was holding your bottomNavigationBar, that's why it's disappearing.
What you are looking for here is, how to have a persistent navigation bar.
Here are two useful links that'll help you and the other people having this need, a Video tutorial to understand how to implement it and a well written article to fully understand how it works

Flutter MaterialPageRoute as fullscreenDialog appears underneath BottomNavigationBar

There's a similar unanswered question here: FullscreenDialog screen not covering bottomnavigationbar
but I want to provide more context, to see if that helps find a solution.
We'll start at the top with my main.dart, I am building a MaterialApp that builds a custom DynamicApp. Here's the important stuff:
Widget build(BuildContext context) {
var _rootScreenSwitcher = RootScreenSwitcher(key: switcherKey);
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
builder: (context, child) {
return DynamicApp(
navigator: locator<NavigationService>().navigatorKey,
child: child,
switcher: _rootScreenSwitcher,
);
},
navigatorKey: locator<NavigationService>().navigatorKey,
onGenerateRoute: (routeSettings) {
switch (routeSettings.name) {
case SettingsNavigator.routeName:
return MaterialPageRoute(
builder: (context) => SettingsNavigator(),
fullscreenDialog: true);
default:
return MaterialPageRoute(builder: (context) => SettingsNavigator());
}
},
home: _rootScreenSwitcher,
);
}
My DynamicApp sets up the root Scaffold like so:
Widget build(BuildContext context) {
return Scaffold(
drawer: NavigationDrawer(
selectedIndex: _selectedIndex,
drawerItems: widget.drawerItems,
headerView: Container(
child: Text('Drawer Header'),
decoration: BoxDecoration(color: Colors.blue),
),
onNavigationItemSelect: (index) {
onTapped(index);
return true; // true means that drawer must close and false is Vice versa
},
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (index) {
onTapped(index);
},
currentIndex: _selectedIndex,
items: bottomNavBarItems,
showUnselectedLabels: false,
),
body: widget.child,
);
}
The child of the DynamicApp is a widget called RootScreenSwitcher which is an IndexedStack and controls the switching of screens from my BottomNavigationBar and also when items are selected in the Drawer. Here's the RootScreenSwitcher:
class RootScreenSwitcherState extends State<RootScreenSwitcher> {
int _currentIndex = 0;
int get currentIndex => _currentIndex;
set currentIndex(index) {
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false,
child: IndexedStack(
index: _currentIndex,
children: menuItems.map<Widget>((MenuItem item) {
return item.widget;
}).toList(),
),
),
);
}
void switchScreen(int index) {
setState(() {
_currentIndex = index;
});
}
}
Each of the main section of the app has its own Navigator and root screen. This all works fine, and I'm happy with the overall navigation structure. Each root screen has its own AppBar but the global Drawer and BottomNavigationBar are handled in the DynamicApp so I don't have to keep setting them in the other Scaffold screens.
So, then it came to start to introduce other sections of the app that are not serviced by the bottom tab bar, and can be presented from the Drawer or from other action buttons. Each of these new sections would have to be modal fullscreenDialog screens so they slide up from the bottom, but have their own navigation and scaffold.
My issue is that when I navigate to my SettingsNavigator screen it slides up from behind the BottomNavigationBar, and not on top of everything. Here's the onGenerateRoute method of the MaterialApp:
onGenerateRoute: (routeSettings) {
switch (routeSettings.name) {
case SettingsNavigator.routeName:
return MaterialPageRoute(
builder: (context) => SettingsNavigator(),
fullscreenDialog: true);
default:
return MaterialPageRoute(builder: (context) => SettingsNavigator());
}
}
I'm new to Flutter and don't quite understand how routing works with contexts, so I am wondering whether the context of the screen that calls the navigateTo method of the Navigator becomes the main build context, and is therefore not on top of the widget tree?
gif here: https://www.dropbox.com/s/nwgzo0q28cqk61p/FlutteRModalProb.gif?dl=0
Here's the tree structure that shows that the Scaffold for the Settings screen has been placed inside the DynamicApp Scaffold. The modal needs to sit above DynamicApp.
Can anyone shed some light on this?
UPDATE: I have tried creating and sharing a unique ScaffoldState key for the tab bar screens, and then the Settings page has a different key. It made no difference. I wonder now if it is the BuildContext having the same parent.
UPDATE UPDATE:
I had a breakthrough last night which has made me realise that it just isn't going to be possible to use embedded Scaffolds in the way I have them at the moment. The problem is that i have a root scaffold called DynamicApp which persists my Drawer and BottomNavigationBar, but loading in other Scaffold pages into the body means the modals are slotting into that body and behind the BottomNavigationBar. To solve the problem you have to subclass BottomNavigationBar and reference it in every Scaffold; which means encapsulating all of the business logic so it uses ChangeNotifier to change state when the nav is interacted with. Basically, Flutter forces a separation of concerns on your architecture, which I guess is a good thing. I'll compose a better answer when I've done all the extra work.
After many hours tearing my hair out trying to pass ScaffoldState keys around, and Navigators the answer was to build the BottomNavigationBar into every Scaffold. But to do this I've had to change how my architecture works ... for the better! I now have a BottomNavigationBar and RootScreenSwitcher that listens for updates from an AppState ChangeNotifier and rebuilds itself when the page index changes. So, I only have to change state in one place for the app to adapt automatically. This is the AppState class:
import 'package:flutter/material.dart';
class AppState extends ChangeNotifier {
int _pageIndex = 0;
int get pageIndex {
return _pageIndex;
}
set setpageIndex(int index) {
_pageIndex = index;
notifyListeners();
}
}
and this is my custom BottomNavigationBar called AppBottomNavigationBar:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AppBottomNavigationBar extends StatefulWidget {
AppBottomNavigationBar({Key key}) : super(key: key);
#override
_AppBottomNavigationBarState createState() => _AppBottomNavigationBarState();
}
class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
#override
Widget build(BuildContext context) {
var state = Provider.of<AppState>(
context,
);
int currentIndex = state.pageIndex;
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: currentIndex ?? 0,
items: bottomNavBarItems,
showUnselectedLabels: false,
onTap: (int index) {
setState(() {
state.setpageIndex = index;
});
},
);
}
}
So, now in my other Scaffold pages I only need to include this line to make sure the BottomNavigationBar is in the Scaffold`:
bottomNavigationBar: AppBottomNavigationBar(),
Which means absolute minimal boilerplate.
I changed the name of the DynamicApp class to AppRootScaffold and this now contains a Scaffold, Drawer, and then set the body of the Scaffold as the RootScreenSwitcher:
class RootScreenSwitcher extends StatelessWidget {
RootScreenSwitcher({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
var state = Provider.of<AppState>(
context,
);
int currentIndex = state.pageIndex;
return SafeArea(
top: false,
child: IndexedStack(
index: currentIndex ?? 0,
children: menuItems.map<Widget>((MenuItem item) {
return item.widget;
}).toList(),
),
);
}
}
I still have a lot to do to streamline this architecture, but it is definitely the better way to go.
UPDATE:
Can you spot the problem with the new Scaffold architecture?
This is still not great. Ironically, I need the BottomNavigationBar back in the root Scaffold for this to work as expected. But then the modals wont appear over the top of the bar again.

Fix last element of ListView to the bottom of screen

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: () => {})
],
),
);
}
}