Navigation Drawer with Dynamic Menu and Submenu using Flutter’s Pageview
Complete Source Code : https://github.com/nickyrabit/NavigationDrawer-with-Dynamic-Menu-and-Submenu-using-Flutter-Pageview
The challenge I faced: Creating a dynamic navigation drawer that loads menu items and their submenus dynamically depending on the roles the user has been granted when logging in. When the user clicks the submenu it should display a corresponding screen. Doing all this while maintaining performance and low memory usage by not re-rendering the navigation drawer and its menus and it also with the option of saving the state of the previous screen.
Solution: I chose to employ PageView because it has inbuilt functionalities which satisfy my needs including switching pages, maintaining the previous scree’s state as well as performance. We also get complementary animations options which are awesome!. a
The catch: It requires you to use the array index as a primary reference for the pages(screens) so you need to know which index opens a certain screen.
Code Implementation:
class _MyHomePageState extends State<MyHomePage> {
Future<http.Response> _responseFuture;
@override
void initState() {
super.initState();
_responseFuture = http.get('https://jsonkeeper.com/b/LD4N');
_selectedPageIndex = 0;// Below is how you define screens which are basically widgets
_pages = [
// 0
Dashboard(),
// 1
Receipts(),
// 2
Payments(),
// 3
GFSCodes()
//Dashboard(),
];// You need to initiate a default page usnig page controller _pageController = PageController(initialPage: _selectedPageIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Controller c = Get.put(Controller());
return GetMaterialApp(
title: 'Accounting App',
home: Scaffold(
resizeToAvoidBottomPadding: false,
appBar: AppBar(title: Obx(() => Text("${c.title.value}"))),
body: PageView(
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: _pages,
),
drawer: createDrawer(context, _responseFuture),
),
);
}
}
The code above has the pages as widgets in a pages array already defined and then I will be using a GetMaterialApp (You can use MaterialApp works just fine).
I have defined PageView as the body and its page controller as well as NeverScrollableScrollPhysics() to prevent it from scrolling pages.
children parameter is you place all screens/widgets from the _pages array.
Since GetMaterialApp or MaterialApp has a default drawer parameter I used it to create a drawer and the code below shows the createDrawer() function being implemented.
Widget createDrawer(
BuildContext context, Future<http.Response> _responseFuture) {
final _controller = ScrollController();
return Drawer(
child: SingleChildScrollView(
controller: _controller,
child: Column(
// Important: Remove any padding from the ListView.
children: <Widget>[
UserAccountsDrawerHeader(
accountName: Text("Matinde Davis"),
accountEmail: Text("matindedavis@gmail.com"),
currentAccountPicture: CircleAvatar(
backgroundColor: Colors.orange,
child: Text(
"M.D",
style: TextStyle(fontSize: 40.0),
),
),
),
new FutureBuilder(
future: _responseFuture,
builder:
(BuildContext context, AsyncSnapshot<http.Response> response) {
if (!response.hasData) {
return const Center(
child: const Text('Loading...'),
);
} else if (response.data.statusCode != 200) {
return const Center(
child: const Text('Error loading data'),
);
} else {
List<dynamic> json = jsonDecode(response.data.body);
return new MyExpansionTileList(elementList: json);
}
},
)
],
),
),
);
}
The above code simply creates the navigation drawer and fetch the menu from a remote server, there is MyExpansionTileList which receives the menu list, iterates it and place an onTap listener to open screens which are shown below here. The area of interest is bolded to show the way to call this onTap to open a screen.
The menu JSON item comes with a unique identifier and in this case, it is a “state” to which I get the value by onTap and I use it to do a switch case comparison so that I can open a corresponding screen. (You could do this by mapping too)
class MyExpansionTileList extends StatefulWidget {
BuildContext context;
final List<dynamic> elementList;
MyExpansionTileList({Key key, this.elementList}) : super(key: key);
@override
State<StatefulWidget> createState() => _DrawerState();
}
class _DrawerState extends State<MyExpansionTileList> {
// You can ask Get to find a Controller that is being used by another page and redirect you to it.
final Controller c = Get.find();// iterating the menu list and present it in the navigation drawer
List<Widget> _getChildren(final List<dynamic> elementList) {
List<Widget> children = [];
elementList.toList().asMap().forEach((index, element) {
int selected = 0;
final subMenuChildren = <Widget>[];
try {
for (var i = 0; i < element['children'].length; i++) {
subMenuChildren.add(new ListTile(
leading: Visibility(
child: Icon(
Icons.account_box_rounded,
size: 15,
),
visible: false,
),
onTap: () => {
setState(() {
log("The item clicked is " + element['children'][i]['state']);
switch (element['children'][i]['state']) {
case '/fund-type':
_selectedPageIndex = 1;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_pageController.hasClients) {
_pageController.animateToPage(1, duration: Duration(milliseconds: 1), curve: Curves.easeInOut);
}
});
c.title.value = "Fund Type";
Navigator.pop(context);
break;
case '/fund-sources':
_selectedPageIndex = 2;
// _pageController.jumpToPage(2);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_pageController.hasClients) {
_pageController.animateToPage(2,
duration: Duration(milliseconds: 1),
curve: Curves.easeInOut);
}
});
c.title.value = "Fund Source";
Navigator.pop(context);
break;
}
})
},
title: Text(
element['children'][i]['title'],
style: TextStyle(fontWeight: FontWeight.w700),
),
));
}
children.add(
new ExpansionTile(
key: Key(index.toString()),
initiallyExpanded: index == selected,
leading: Icon(
Icons.audiotrack,
color: Colors.green,
size: 30.0,
),
title: Text(
element['title'],
style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500),
),
children: subMenuChildren,
onExpansionChanged: ((newState) {
if (newState) {
Duration(seconds: 20000);
selected = index;
log(' selected ' + index.toString());
} else {
selected = -1;
log(' selected ' + selected.toString());
}
}),
),
);
} catch (err) {
print('Caught error: $err');
}
});
return children;
}
@override
Widget build(BuildContext context) {
return new Column(
children: _getChildren(widget.elementList),
);
}
@override
void initState() {
super.initState();
}
}
There you have it, you will need a GetX dependency for this but it's not required if you use MaterialApp
That's it!. It works perfectly and it's very fast and no re-rendering!!
Full Code Here: https://github.com/nickyrabit/NavigationDrawer-with-Dynamic-Menu-and-Submenu-using-Flutter-Pageview
Say hi to me at:
Email: nickyrabit@gmail.com
Twitter: nngailo
Instagram: nickyrabit
Github: nickyrabit