Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Flutter scaffold with Navigation Rail, Bottom Navigation Bar, Drawer and Nested Navigator.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class DynamicScaffold extends StatefulWidget {
const DynamicScaffold({
Key? key,
required this.modules,
this.onGenerateRoute,
this.navKey,
}) : super(key: key);
final List<ContentModule> modules;
final Route<dynamic>? Function(RouteSettings settings)? onGenerateRoute;
final GlobalKey<NavigatorState>? navKey;
@override
_DynamicScaffoldState createState() => _DynamicScaffoldState();
}
class _DynamicScaffoldState extends State<DynamicScaffold> {
late final _moduleIndex = ValueNotifier<int>(0);
late final _pinnedIndex = ValueNotifier<int>(0);
late final GlobalKey<NavigatorState> _navKey =
widget.navKey ?? GlobalKey<NavigatorState>();
List<ContentModule> get pinnedModules {
return widget.modules.where((e) => e.buildOptions(context).pinned).toList();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: _moduleIndex,
builder: (context, index, child) {
return LayoutBuilder(builder: (context, dimens) {
if (dimens.maxWidth >= 1440) return buildDesktop(context, index);
if (dimens.maxWidth >= 720) return buildTablet(context, index);
return buildMobile(context, index);
});
},
);
}
Widget buildDesktop(BuildContext context, int selected) {
return Scaffold(
drawer: buildDrawer(context, selected),
body: Row(
children: [
if (pinnedModules.length >= 2) buildNavRail(context, selected, true),
Expanded(
child: buildBodyContent(context, selected),
),
],
),
);
}
Widget buildTablet(BuildContext context, int selected) {
return Scaffold(
drawer: buildDrawer(context, selected),
body: Row(
children: [
if (pinnedModules.length >= 2) buildNavRail(context, selected, false),
Expanded(
child: buildBodyContent(context, selected),
),
],
),
);
}
Widget buildMobile(BuildContext context, int selected) {
return Scaffold(
drawer: buildDrawer(context, selected),
body: buildBodyContent(context, selected),
bottomNavigationBar: pinnedModules.length == 0 || pinnedModules.length < 2
? null
: buildBottomNavigationBar(context, selected),
);
}
Widget buildNavRail(BuildContext context, int selected, bool extended) {
return ValueListenableBuilder<int>(
valueListenable: _pinnedIndex,
builder: (context, index, child) => NavigationRail(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
extended: extended,
labelType: extended ? null : NavigationRailLabelType.all,
destinations: pinnedModules.map((e) {
final item = e.buildOptions(context);
return NavigationRailDestination(
icon: item.icon,
label: Text(item.label),
);
}).toList(),
selectedIndex: index,
onDestinationSelected: onPinnedIndexChanged,
),
);
}
Widget buildBottomNavigationBar(BuildContext context, int selected) {
return ValueListenableBuilder<int>(
valueListenable: _pinnedIndex,
builder: (context, index, child) => BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: index,
onTap: onPinnedIndexChanged,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
items: pinnedModules.map((e) {
final item = e.buildOptions(context);
return BottomNavigationBarItem(
icon: item.icon,
label: item.label,
);
}).toList(),
));
}
Widget buildDrawer(BuildContext context, int selected) {
return Drawer(
child: ListView.builder(
itemCount: widget.modules.length,
itemBuilder: (context, index) {
final module = widget.modules[index].buildOptions(context);
return ListTile(
leading: module.icon,
title: Text(module.label),
selected: selected == index,
onTap: () {
_moduleIndex.value = index;
Navigator.of(context).maybePop();
},
);
},
),
);
}
Widget buildBodyContent(BuildContext context, int selected) {
final current = widget.modules[selected];
final currentModule = current.buildOptions(context);
final body = currentModule.body(context, (title) {
currentModule._controller.setTitle(title);
});
if (widget.onGenerateRoute != null) {
return Navigator(
key: _navKey,
initialRoute: current.routeName,
onGenerateRoute: widget.onGenerateRoute,
);
}
return body;
}
void onPinnedIndexChanged(int index) {
_pinnedIndex.value = index;
_moduleIndex.value = widget.modules
.map((e) => e.buildOptions(context).label)
.toList()
.indexOf(pinnedModules[index].buildOptions(context).label);
final route = widget.modules[_moduleIndex.value].routeName;
final nav = _navKey.currentState!;
nav.maybePop();
nav.pushNamed(route);
}
}
abstract class ContentModule extends StatelessWidget {
ContentModuleOptions buildOptions(BuildContext context);
String get routeName;
@override
Widget build(BuildContext context) {
final options = buildOptions(context);
return Scaffold(
appBar: _buildAppBar(context, options),
body: _buildBody(context, options),
);
}
PreferredSizeWidget? _buildAppBar(
BuildContext context,
ContentModuleOptions options,
) {
return AppBar(
title: ValueListenableBuilder<String>(
valueListenable: options._controller.title,
builder: (context, title, child) {
if (title.isNotEmpty) {
return Text(title);
}
return child!;
},
child: Text(options.title ?? options.label),
),
);
}
Widget _buildBody(BuildContext context, ContentModuleOptions options) {
return options.body(context, (title) {
options._controller.setTitle(title);
});
}
}
class ContentModuleOptions {
ContentModuleOptions({
required this.body,
required this.label,
required this.icon,
ContentModuleController? controller,
this.pinned = false,
this.title,
}) : _controller = controller ?? ContentModuleController();
final Widget Function(BuildContext context, void Function(String) updateTitle)
body;
final String label;
final Icon icon;
final bool pinned;
final ContentModuleController _controller;
final String? title;
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ContentModuleOptions &&
other.body == body &&
other.label == label &&
other.icon == icon &&
other.pinned == pinned &&
other.title == title;
}
@override
int get hashCode {
return body.hashCode ^
label.hashCode ^
icon.hashCode ^
pinned.hashCode ^
title.hashCode;
}
}
class ContentModuleController {
final _title = ValueNotifier<String>("");
ValueListenable<String> get title => _title;
void setTitle(String value) {
_title.value = value;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment