Flutter persistent sidebar - flutter

In my application I want to have a sidebar that allows me to have access to specific functions everywhere in my application.
What I want :
That the sidebar remains visible when I push my pages
That I can pushNamed route or open a modal with one of the sidebar functions
That I can not display the sidebar on certain pages
What I do :
In red, the persistent sidebar and in yellow my app content.
If I click on my profil button in the HomeView, the ProfilView is displayed and my sidebar remains visible so it's ok
My AppView :
class AppView extends StatelessWidget {
const AppView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: AppConfig.kAppName,
debugShowCheckedModeBanner: false,
theme: AppTheme().data,
builder: (context, child) => SidebarTemplate(child: child), // => I create a template
onGenerateRoute: RouterClass.generate,
initialRoute: RouterName.kHome,
);
}
My SidebarTemplate : (Display the sidebar and load the page with my router)
class SidebarTemplate extends StatelessWidget {
final Widget? child;
const SidebarTemplate({Key? key, this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body : Row(
children: [
SidebarAtom(), // => My sidebar Widget
Expanded(
child: ClipRect(
child: child! // => My view
),
)
],
)
),
);
}
}
My RouterClass :
abstract class RouterClass{
static Route<dynamic> generate(RouteSettings settings){
final args = settings.arguments;
switch(settings.name){
case RouterName.kHome:
return MaterialPageRoute(
builder: (context) => HomeView()
);
case RouterName.kProfil:
return MaterialPageRoute(
builder: (context) => ProfilView(title: "Profil",)
);
default:
return MaterialPageRoute(
builder: (context) => Error404View(title: "Erreur")
);
}
}
}
How to do :
To pushNamed or open a modal with a button from my sidebar because I have an error
The following assertion was thrown while handling a gesture:
I/flutter (28519): Navigator operation requested with a context that does not include a Navigator.
I/flutter (28519): The context used to push or pop routes from the Navigator must be that of a widget that is a
I/flutter (28519): descendant of a Navigator widget.
To hide the sidebar when I want like SplashScreen for example
Any guidance on the best way to accomplish this would be appreciated.

You can use a NavigatorObserver to listen to the changes in the route.
class MyNavObserver with NavigatorObserver {
final StreamController<int> streamController;
MyNavObserver({required this.streamController});
#override
void didPop(Route route, Route? previousRoute) {
if (previousRoute != null) {
if (previousRoute.settings.name == null) {
streamController.add(3);
} else {
streamController
.add(int.parse(previousRoute.settings.name!.split('/').last));
}
}
}
#override
void didPush(Route route, Route? previousRoute) {
if (route.settings.name == null) {
streamController.add(3);
} else {
streamController.add(int.parse(route.settings.name!.split('/').last));
}
}
}
and using StreamController you can make changes to your SidebarTemplate by putting it inside StreamBuilder. This will take care of all the requirements you have mentioned in the question.
Check out the live example here.

As you can see from the Profil screenshot, the sidebar is not part of the widget subtree of the Navigator (the back button is only on the profil widget). This means that you cannot find the Navigator from the context of the sidebar. That is happening because you are using builder in your MaterialApp which inserts widgets above the navigator.
That is also the reason why you cannot hide the sidebar when you want to show a splash screen.
Do you really need to use the builder on MaterialApp? Then you can save the Navigator globally and access it from the sidebar. This is the first article when I search on DuckDuckGo, that you can follow.
To show a SplashScreen you would need to add a state to AppView and change the builder function. Not very nice if you ask me.
I suggest you to re-think your architecture and get rid of the builder in the MaterialApp.

Related

Navigation inside NavigationBody fluent_ui Flutter

I'm writing a flutter windows application using fluent_ui package.
I have NavigationView with NavigationPage and NavigationBody items.
On some NavigationBody item, I want to navigate to the other page (for example from page app/cars/ to app/cars/id) but leave the same NavigationPane state (which mean changing only a NavigationBody widget).
How can I achieve this? Only by using some kind of SetState() which totally changes the content of the widget or some solutions with using Navigator?
Basic page structure:
- NavigationView
- NavigationPane
- NavigationBody
So. I found a solution to this problem. My solution is based on using Navigator as a base widget. In NavigationView as a NavigationBody item used not concrete page (widget), but Navigator which takes care of inner navigation. So Push() method changes only the NavigationBody of NavigationView and not the entire window of the application.
Here is Code example for this. Hope someone will find this helpful:
nav_view_page.dart
NavigationView(
appBar: //some app bar
pane: // your NavigationPane
content: NavigationBody(
index: _selectedIndex,
children: [
//some other body items
CarsPageNavigator(
navigatorKey: GlobalKey<NavigatorState>(),
carsController: widget.carsController,
), //this is our navigator
],
),
);
custom_navigator.dart
class CarsPageNavigator extends StatelessWidget {
const CarsPageNavigator(
{Key? key, required this.navigatorKey, required this.carsController})
: super(key: key);
final GlobalKey<NavigatorState> navigatorKey;
final CarsController carsController;
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
initialRoute: '/car',
onGenerateRoute: (RouteSettings routeSettings) {
return FluentPageRoute(
settings: routeSettings,
builder: (context) {
return getPage(routeSettings.name);
});
});
}
Widget getPage(String? url) {
switch (url) {
case CarsManagementPage.routeName:
{
return CarsManagementPage(carsController: carsController);
}
case CarManagementPage.routeName:
return CarManagementPage(controller: carsController);
default:
return CarsManagementPage(carsController: carsController);
}
}
}
In this code in OnGenerateRoute method returns page that you want to navigate to.
To navigate to other page from some inner page use pushNamed(or other named push methods of Navigator):
Navigator.pushNamed(context, CarManagementPage.routeName, arguments: car);
small remark: some "basic" code is skipped to make answer smaller

Issue regarding nested MaterialApp.router() in Flutter Navigator 2.0

Ask the question denotes I'm trying to create a nested Navigator using Navigator 2.0 for my Flutter web app. Below is the starting point of my app.
void main() {
runApp(App());
}
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return MaterialApp.router(
routeInformationParser: AppRouteParser(), routerDelegate: AppRouterDelegate(),
title: "Demo",
);
}
}
As you can see I've added a MaterialApp.router() to handle all the top layer navigations.
Now I wanted to add a nested navigator inside this one which will work the same way as above and will handle the url changes properly. That why I decided to use the same MaterialApp.router() widget inside as a child as my nested Navigator.
Everything is working fine after doin this but I am getting two debug banners like the image below :
This makes me wonder if I using the proper method to achieve the result.
The child Navigator belongs in Page1 widget of the root navigator like below is the Navigator widget of root MaterialApp.router:
class AppRouterDelegate extends RouterDelegate<AppRoute>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoute> {
final GlobalKey<NavigatorState> _navigatorKey;
bool isPage1A = false;
bool isPage1B = false;
bool isUnknown = false;
AppRouterDelegate() : _navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return Navigator(
pages: [
MaterialPage(key: ValueKey("Page 1"),child: Page1(_valueChangeCallback)),
if(isPage1A)
MaterialPage(key: ValueKey("Page 1A"),child: Page1A(_valueChangeCallback)),
if(isPage1B)
MaterialPage(key: ValueKey("Page 1B"),child: Page1B(_valueChangeCallback)),
/* if(isUnknown)
MaterialPage(key: ValueKey("404"),child: TestPage()) */
],
onPopPage: (route,result){print("Pop !!!!"); return route.didPop(result);}
);
}
_valueChangeCallback(bool value,String subPage,[String subPage2]) {
//print("Value change callback");
if(subPage2 == null) {
if(subPage == "A")
isPage1A = value;
else if(subPage == "B")
isPage1B = value;
}
else {
if(subPage2 == "B") {
isPage1A = !value;
isPage1B = value;
}
else if(subPage2 == "A") {
isPage1A = value;
isPage1B = !value;
}
}
notifyListeners();
}
And below is the Page1 widget where the child MaterialApp.router is located :
class Page1 extends StatefulWidget {
Function valueChangeCallback;
Page1(this.valueChangeCallback);
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
#override
Widget build(BuildContext context) {
print("Page 1");
return Scaffold(
body: Column(
children: [
InkWell(
onTap: () {
widget.valueChangeCallback(true,"A");
},
child: Text("Move to Sub Pages")
),
Expanded(child: MaterialApp.router(routeInformationParser: NestedAppRouteInformationParser(), routerDelegate: NestedAppRouterDelegate())),
],
),
);
}
}
If you look into app.dart MaterialApp Is a convenience widget that wraps a handful of "widgets that are commonly required for material design applications."
If you were to use the default constructor a top level Navigator object is configured for you.
The MaterialApp.router() is another convenience.
"Creates a [MaterialApp] that uses the [Router] instead of a [Navigator]."
The router constructor provides you a way to create a MaterialApp and configure and return a custom Navigator.
What you are doing, when you use this constructor, is wrapping descendent widgets in all the convenience widgets that MaterialApp has to offer(Including the debug banner).
For Nested Routers what you want to do instead is just use the Router() widget directly, and you will avoid invoking all the extras that MaterialApp affords you during the initialization of your app.
Also of note, there should ideally only be one information parser per app.
as per the notes in router.dart you should pass null to the nester Router Wdiget.
"To opt out of URL updates entirely, pass null for [routeInformationProvider]
/// and [routeInformationParser]. This is not recommended in general, but may be
/// appropriate in the following cases:
///
/// * The application does not target the web platform.
///
/// * **There are multiple router widgets in the application. Only one [Router]
/// widget should update the URL (typically the top-most one created by the
/// [WidgetsApp.router], [MaterialApp.router], or [CupertinoApp.router]).**
///
/// * The application does not need to implement in-app navigation using the
/// browser's back and forward buttons."
class _Page1State extends State<Page1> {
#override
Widget build(BuildContext context) {
print("Page 1");
return Scaffold(
body: Column(
children: [
InkWell(
onTap: () {
widget.valueChangeCallback(true,"A");
},
child: Text("Move to Sub Pages")
),
Expanded(child: Router(routeInformationParser: null, routerDelegate: NestedAppRouterDelegate(), backButtonDispatcher: ChildBackButtonDispatcher(Router.of(context).backButtonDispatcher),)),
],
),
);
}
Also providing the child back button dispatcher as shown will allow you to contact the parent router when executing back button presses...Hope that helps!

StateNotifierProvider not keeping state between app restarts

Using flutter_riverpod: ^0.12.4 and testing in the android emulator as well as on a physical device.
What am I doing wrong that the Sign In screen state value does not persist in the StateNotifierProvider after a restart of the app?
The accountSetupProvider's state defaults to the Intro Screen. After the Intro Screen's onPressed button is clicked the state is updated to the Sign In screen and it triggers correctly a rebuild to display the Sign In screen.
However, after a flutter hot restart or opening/closing the app, the Intro Screen, rather than the Sign In screen displays. Shouldn't the state, which now is set to the Sign In screen after clicking onPressed in the Intro Screen persist between restarts and cause the Intro Screen to be skipped and the Sign In screen to display?
As you can see below main.dart has an initial AppRoutes.root route. In app_router.dart, this "root" screen opens root_screen.dart, which is a ConsumerWidget that is watch(ing) my StateNotifierProvider called "accountSetupProvider" in account_setup_provider.dart.
main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: AppRoutes.root,
onGenerateRoute: (settings) => AppRouter.onGenerateRoute(settings),
);
}
}
app_router.dart
class AppRoutes {
static const String root = RootScreen.id;
static const String intro = IntroScreen.id;
static const String signIn = SignInScreen.id;
}
class AppRouter {
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
final _args = settings.arguments;
switch (settings.name) {
case AppRoutes.root:
return MaterialPageRoute<dynamic>(
builder: (_) => RootScreen(),
settings: settings,
);
case AppRoutes.intro:
return MaterialPageRoute<dynamic>(
builder: (_) => IntroScreen(),
settings: settings,
);
case AppRoutes.signIn:
return MaterialPageRoute<dynamic>(
builder: (_) => SignInScreen(),
settings: settings,
);
}
}
}
root_screen.dart
class RootScreen extends ConsumerWidget {
const RootScreen({Key key}) : super(key: key);
static const String id = 'root_screen';
#override
Widget build(BuildContext context, ScopedReader watch) {
final screen = watch(accountSetupProvider.state);
if (screen == AppRoutes.signIn) {
return SignInScreen();
} else if (screen == AppRoutes.intro) {
return IntroScreen();
}
}
}
intro_screen.dart (only including the onPressed portion of intro screen, which I'm expecting to set the state to the new screen, even after a flutter hot restart or app restart.)
onPressed: () {
context
.read(accountSetupProvider) // see accountSetupProvider StateNotifierProvider below.
.setScreen(AppRoutes.signIn);
},
account_setup_provider.dart (inits to the AppRoutes.intro screen.)
class AccountSetupNotifier extends StateNotifier<String> {
AccountSetupNotifier() : super(AppRoutes.intro);
void setScreen(String screen) {
state = screen;
}
}
final accountSetupProvider = StateNotifierProvider<AccountSetupNotifier>((ref) {
return AccountSetupNotifier();
});
Without even looking at your code, and looking only at your subject line, it is not at all surprising to me. "Hot Restart" resets all variables. How could there be any state preserved? Are you instead looking for "Hot Reload"?

Access a parent class variable in its child in Flutter?

I am trying to use a custom statefull PageWrapper widget to wrap all my pages. The idea is to make it return a Scaffold and use the same menu drawer and bottom navigation bar, and call the appropriate page as page parameter.
My bottomNavigationBar is working well and I am setting the correct selectedIndex, but I can't find a way to access it in the child page (that is in another file), since I don't know how to access the parent's selectedIndex and display the appropriate widget from my page's list.
import 'package:flutter/material.dart';
class PageWrapper extends StatefulWidget {
final Widget page;
final AppBar appBar;
final BottomNavigationBar bottomNav;
final Color bckColor;
PageWrapper({#required this.page, this.appBar, this.bckColor, this.bottomNav});
#override
_PageWrapperState createState() => _PageWrapperState();
}
class _PageWrapperState extends State<PageWrapper> {
int _selectedIndex;
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
#override
void initState() {
super.initState();
_selectedIndex = 0;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: widget.appBar,
backgroundColor: widget.bckColor,
bottomNavigationBar: CustomBottomNavigation(selectedIndex: _selectedIndex, onItemTapped: _onItemTapped),
body: widget.page,
drawer: Drawer(...),
);
}
}
Named roots in my main.dart:
home: PageWrapper(page: HomeScreen()),
routes: {
'form': (context) => PageWrapper(page: RoomService()),
},
I would like to access that bottom navigation bar's current index somehow in my HomeScreen and RoomService screen. Is there a way to do it?
You can solve that by using a State Management tool like Provider or Bloc. To keep things simple, lets use Provider to do it.
Wrap MaterialApp with a ChangeNotifierProvider in your main.dart.
return MultiProvider(
providers: [
ChangeNotifierProvider<IndexModel>(
create: (context) => IndexModel()),
],
child: MaterialApp(...)
);
Create a model that will hold your index value:
Also, you have to override the getter and setter of index in order to call notifyListeners after its value is set. Here is an example:
class IndexModel extends ChangeNotifier {
int _index;
get index => _index;
set index(int index) {
_index = index;
notifyListeners(); //Notifies its listeners that the value has changed
}
}
Here is how you can display your data according to its index (Ideally, you should use Selector instead of Consumer so that the widget only rebuilds if the value it is listening to, changes):
#override
Widget build(BuildContext context) {
//other widgets
Selector<IndexModel, String>(
selector: (_, model) => model.index,
builder: (_, i, __) {
switch(i){
//do your returning here based on the index
}
},
);
}
)
}
Extra note. Here is how you can access the values of ImageModel in your UI:
final model=Provider.of<IndexModel>(context,listen:false);
int index =model.index; //get index value
model.index=index; //set your index value
You have to pass listen:false when you aren't listening for changes. This is needed when you are accessing it in initState or in onPressed.

Flutter showDialog with navigator key rather than passing context

Currently its very hectic to show dialog from any layer of code in app just because one has to pass context in it. Hence i thought to pass navigatorKey.currentContext (Navigator key is a global key passed to Material app navigatorKey parameter) to show dialog. But i got the error
"Navigator operation requested with a context that does not include a Navigator.The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget."
The issue is showDialog calls Navigator.of(context) internally and which looks for the navigator ancestor which ofcourse will return null as the navigator is itself the root. Hence it will not find the navigator as ancestor.
Is there a way we can directly pass the navigator state/context to showDialog function to show the dialog? Or is there a more easy way to show Dialog without passing context to it if we want to show it from bloc?
I found a simple solution:
navigatorKey.currentState.overlay.context
I use this in a redux middleware where I keep navigatorKey, and want to show a dialog globally anywhere in the app everytime I dispatch a specific action.
Since this one is merged:
https://github.com/flutter/flutter/pull/58259
You can use:
navigatorKey.currentContext;
You can make use of InheritedWidget here. Make a InheritedWidget the root for your application which holds a navigator key. Then you can pass any context of child widgets to get the current navigator state.
Example:
InheritedWidget:
// Your InheritedWidget
class NavigatorStateFromKeyOrContext extends InheritedWidget {
const NavigatorStateFromKeyOrContext({
Key key,
#required this.navigatorKey,
#required Widget child,
}) : super(key: key, child: child);
final GlobalKey<NavigatorState> navigatorKey;
static GlobalKey<NavigatorState> getKey(BuildContext context) {
final NavigatorStateFromKeyOrContext provider =
context.inheritFromWidgetOfExactType(NavigatorStateFromKeyOrContext);
return provider.navigatorKey;
}
static NavigatorState of(BuildContext context) {
NavigatorState state;
try {
state = Navigator.of(context);
} catch (e) {
// Assertion error thrown in debug mode, in release mode no errors are thrown
print(e);
}
if (state != null) {
// state can be null when context does not include a Navigator in release mode
return state;
}
final NavigatorStateFromKeyOrContext provider =
context.inheritFromWidgetOfExactType(NavigatorStateFromKeyOrContext);
return provider.navigatorKey?.currentState;
}
#override
bool updateShouldNotify(NavigatorStateFromKeyOrContext oldWidget) {
return navigatorKey != oldWidget.navigatorKey;
}
}
HomeScreen:
// Your home screen
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: NavigatorStateFromKeyOrContext.getKey(context),
home: InitPage(),
);
}
}
The root of the application will look like,
final GlobalKey navigator = GlobalKey<NavigatorState>(debugLabel: 'AppNavigator');
runApp(
NavigatorStateFromKeyOrContext(
navigatorKey: navigator,
child: HomePage(),
),
);
Now from anywhere in the app, pass any context to get the NavigatorState like
NavigatorStateFromKeyOrContext.of(context)
Note: This is one approach I came up with where I used InheritedWidget, there are many other ways to achieve the same, like using Singleton, having a global bloc to provide navigator key, storing the navigator key in a Redux store or any other global state management solutions, etc.
Hope this helps!
Currently, I am showing a dialog by creating a function in my util class which takes the context as a parameter.
static void showAlertDialog(String title, String message, BuildContext context) {
// flutter defined function
showDialog(
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: new Text(title),
content: new Text(message),
actions: <Widget>[
// usually buttons at the bottom of the dialog
new FlatButton(
child: new Text("Close"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
Using the above function as:
UtilClass. showAlertDialog("Title", "Message", context);