Flutter showDialog with navigator key rather than passing context - flutter

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);

Related

Flutter persistent sidebar

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.

how to navigate data between two screens in flutter?

I have homeScreen, which every another screen returns to it.
and one of the screens have parameters from TextField, which I need in the homeScreen, how I can navigate these parameters to the home?
NOTE: when I use constructor, the other screens show an error in the line that navigate to Home because there is no parameters.
You can either use an optional parameter in your constructor:
Homepage({String textfield});
and use it on your Homepage (don't forget that this value is nullable)
Or you need to use some kind of state management with ValueNotifiers
You can pass data using parameters
class XYZ extends StatelessWidget {
final TextEditingController textController = TextEditingController();
void navigationFunction() {
//send the data from XYZ to HomePage
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomePage(textValue: textController.text)),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({this.textValue});
final String textValue; //use this textValue in HomePage
#override
Widget build(BuildContext context) {
return Scaffold();
}
}

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.

How to reload the page whenever the page is on screen - flutter

Is there any callbacks available in flutter for every time the page is visible on screen? in ios there are some delegate methods like viewWillAppear, viewDidAppear, viewDidload.
I would like to call a API call whenever the particular page is on-screen.
Note: I am not asking the app states like foreground, backround, pause, resume.
Thank You!
Specifically to your question:
Use initState but note that you cannot use async call in initState because it calls before initializing the widget as the name means. If you want to do something after UI is created didChangeDependencies is great. But never use build() without using FutureBuilder or StreamBuilder
Simple example to demostrate:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MaterialApp(home: ExampleScreen()));
}
class ExampleScreen extends StatefulWidget {
ExampleScreen({Key key}) : super(key: key);
#override
_ExampleScreenState createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
List data = [];
bool isLoading = true;
void fetchData() async {
final res = await http.get("https://jsonplaceholder.typicode.com/users");
data = json.decode(res.body);
setState(() => isLoading = false);
}
// this method invokes only when new route push to navigator
#override
void initState() {
super.initState();
fetchData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: isLoading
? CircularProgressIndicator()
: Text(data?.toString() ?? ""),
),
);
}
}
Some lifecycle method of StatefulWidget's State class:
initState():
Describes the part of the user interface represented by this widget.
The framework calls this method in a number of different situations:
After calling initState.
After calling didUpdateWidget.
After receiving a call to setState.
After a dependency of this State object changes (e.g., an InheritedWidget referenced by the previous build changes).
After calling deactivate and then reinserting the State object into the tree at another location.
The framework replaces the subtree below this widget with the widget
returned by this method, either by updating the existing subtree or by
removing the subtree and inflating a new subtree, depending on whether
the widget returned by this method can update the root of the existing
subtree, as determined by calling Widget.canUpdate.
Read more
didChangeDependencies():
Called when a dependency of this State object changes.
For example, if the previous call to build referenced an
InheritedWidget that later changed, the framework would call this
method to notify this object about the change.
This method is also called immediately after initState. It is safe to
call BuildContext.dependOnInheritedWidgetOfExactType from this method.
Read more
build() (Stateless Widget)
Describes the part of the user interface represented by this widget.
The framework calls this method when this widget is inserted into the
tree in a given BuildContext and when the dependencies of this widget
change (e.g., an InheritedWidget referenced by this widget changes).
Read more
didUpdateWidget(Widget oldWidget):
Called whenever the widget configuration changes.
If the parent widget rebuilds and request that this location in the
tree update to display a new widget with the same runtimeType and
Widget.key, the framework will update the widget property of this
State object to refer to the new widget and then call this method with
the previous widget as an argument.
Read more
Some widgets are stateless and some are stateful. If it's a stateless widget, then only values can change but UI changes won't render.
Same way for the stateful widget, it will change for both as value as well as UI.
Now, will look into methods.
initState(): This is the first method called when the widget is created but after constructor call.
#override
void initState() {
// TODO: implement initState
super.initState();
}
didChangeDependecies() - Called when a dependency of this State object changes.Gets called immediately after initState method.
#override
void didChangeDependencies() {
super.didChangeDependencies();
}
didUpdateWidget() - It gets called whenever widget configurations gets changed. Framework always calls build after didUpdateWidget
#override
void didUpdateWidget (
covariant Scaffold oldWidget
)
setState() - Whenever internal state of State object wants to change, need to call it inside setState method.
setState(() {});
dispose() - Called when this object is removed from the tree permanently.
#override
void dispose() {
// TODO: implement dispose
super.dispose();
}
You don't need StatefulWidget for calling the api everytime the screen is shown.
In the following example code, press the floating action button to navigate to api calling screen, go back using back arrow, press the floating action button again to navigate to api page.
Everytime you visit this page api will be called automatically.
import 'dart:async';
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => ApiCaller())),
),
);
}
}
class ApiCaller extends StatelessWidget {
static int counter = 0;
Future<String> apiCallLogic() async {
print("Api Called ${++counter} time(s)");
await Future.delayed(Duration(seconds: 2));
return Future.value("Hello World");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Api Call Count: $counter'),
),
body: FutureBuilder(
future: apiCallLogic(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const CircularProgressIndicator();
if (snapshot.hasData)
return Text('${snapshot.data}');
else
return const Text('Some error happened');
},
),
);
}
}
This is the simple code with zero boiler-plate.
The simplest way is to use need_resume
1.Add this to your package's pubspec.yaml file:
dependencies:
need_resume: ^1.0.4
2.create your state class for the stateful widget using type ResumableState instead of State
class HomeScreen extends StatefulWidget {
#override
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends ResumableState<HomeScreen> {
#override
void onReady() {
// Implement your code inside here
print('HomeScreen is ready!');
}
#override
void onResume() {
// Implement your code inside here
print('HomeScreen is resumed!');
}
#override
void onPause() {
// Implement your code inside here
print('HomeScreen is paused!');
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Go to Another Screen'),
onPressed: () {
print("hi");
},
),
),
);
}
}
If you want to make an API call, then you must be (or really should be) using a StatefulWidget.
Walk through it, let's say your stateful widget receives some id that it needs to make an API call.
Every time your widget receives a new id (including the first time) then you need to make a new API call with that id.
So use didUpdateWidget to check to see if the id changed and, if it did (like it does when the widget appears because the old id will be null) then make a new API call (set the appropriate loading and error states, too!)
class MyWidget extends StatefulWidget {
Suggestions({Key key, this.someId}) : super(key: key);
String someId
#override
State<StatefulWidget> createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
dynamic data;
Error err;
bool loading;
#override
Widget build(BuildContext context) {
if(loading) return Loader();
if(err) return SomeErrorMessage(err);
return SomeOtherStateLessWidget(data);
}
#override
void didUpdateWidget(covariant MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// id changed in the widget, I need to make a new API call
if(oldWidget.id != widget.id) update();
}
update() async {
// set loading and reset error
setState(() => {
loading = true,
err = null
});
try {
// make the call
someData = await apiCall(widget.id);
// set the state
setState(() => data = someData)
} catch(e) {
// oops an error happened
setState(() => err = e)
}
// now we're not loading anymore
setState(() => loading = false);
}
}
I'm brand new to Flutter (literally, just started playing with it this weekend), but it essentially duplicates React paradigms, if that helps you at all.
Personal preference, I vastly prefer this method rather than use FutureBuilder (right now, like I said, I'm brand new). The logic is just easier to reason about (for me).

Global snackbar/dialog utils class in flutter

I want to create a GlobalMessageUtils class that would open a material snackbar or dialog without having to pass the build context. The idea is that whenever there's any error (no network, bad request, etc) I am able to pop open a snackbar and relay the message to the user.Is there a concept of global context?
I was playing with the idea of making my GlobalMessageUtils class a singleton that takes in a build context and instantiate it at the MaterialApp level, but I haven't gotten this to work. Any body have any ideas? Is this even a good pattern in flutter? If not, how do you guys deal with error handling at a global level?
Using the BLOC pattern and Rxdart, I created a UiErrorUtils class
class UiErrorUtils {
// opens snackbar
void openSnackBar(BuildContext context, String message) async {
await Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}
// subscribes to stream that triggers open snackbar
void subscribeToSnackBarStream(BuildContext context, PublishSubject<String> stream){
stream.listen((String message){
openSnackBar(context, message);
});
}
}
In your StatefulWidget, you can use the context provided in the initState hook:
class WidgetThatUsesUIErrorUtils extends StatefulWidget {
final UiErrorUtils uiErrorUtils;
final Bloc bloc;
WidgetThatUsesUIErrorUtils({this.uiErrorUtils, this.bloc});
WidgetThatUsesUIErrorUtils createState() => WidgetThatUsesUIErrorUtilsState(
uiErrorUtils: uiErrorUtils,
bloc: bloc,
);
}
class WidgetThatUsesUIErrorUtilsState extends State<WidgetThatUsesUIErrorUtils> {
final Bloc _bloc;
final UiErrorUtils _uiErrorUtils;
WidgetThatUsesUIErrorUtilsState({Bloc bloc, UiErrorUtils uiErrorUtils})
: _bloc = bloc ?? Bloc(),
_uiErrorUtils = uiErrorUtils ?? UiErrorUtils();
#override
void initState() {
super.initState();
// Subscribe to UI feedback streams from provided _bloc
_uiErrorUtils.subscribeToSnackBarStream(context, _bloc.snackBarSubject);
}
}
BLOC
class Bloc extends BlocBase {
// UI Feedback Subjects
final PublishSubject<String> snackBarSubject = PublishSubject<String>();
// some function that gets data from network
Future<bool> getDataRequest() async {
try {
// get request code here
} catch(error) {
this.snackBarSubject.add(error);
}
}
#override
void dispose() {
snackBarSubject?.close();
}
}
Now your widget has subscribed to the bloc's snackBarStream.
So in your bloc whenever a request fails you can add the message to the snackBarStream and since your widget has subscribed via UiErrorUtils the snackbar will trigger with the message.
define global variable e.g. inside main.dart
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
MaterialApp widget has scaffoldMessengerKey property so set this key to property
return MaterialApp(
debugShowCheckedModeBanner: false,
scaffoldMessengerKey: rootScaffoldMessengerKey,
home: Scaffold(),
);
now you're able to show SnackBar from any place in app
rootScaffoldMessengerKey.currentState?.showSnackBar(SnackBar(content: Text('some text')));
I wrote a Package that supports application wide message display.
EZ Flutter supports displaying a message to the user from anywhere inside the app with just one line of code. Global Messaging is handled with a BLOC and widget added as the body of a Scaffold.
Github : https://github.com/Ephenodrom/EZ-Flutter
dependencies:
ez_flutter: ^0.2.0
Add the message wrapper
Add the EzGlobalMessageWrapper as the body to a Scaffold.
Scaffold{
appBar: ...
body: EzGlobalMessageWrapper(
MyWidget(
...
)
)
}
Add message to the bloc
Load the EzMessageBloc via the EzBlocProvider using the get method.
Add a EzMessage to the bloc. The supported EzMessageTypes are :
SUCCESS (default color : Colors.green)
INFO (default color : Colors.blue)
WARNING (default color : Colors.orange)
ERROR (default color : Colors.red)
EzBlocProvider.of<EzGlobalBloc>(context)
.get<EzMessageBloc>(EzMessageBloc)
.addition
.add(EzMessage("This is a success message", EzMessageType.SUCCESS));
https://github.com/Ephenodrom/EZ-Flutter/blob/master/documentation/GLOBAL_MESSAGE.md
For a Provider solution if you are using BaseWidget, then you can just create a base class method and use it everywhere you are using provider.
class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {
final Widget Function(BuildContext context, T model, Widget child) builder;
.
.
.
showToast(BuildContext context, String message, {int durationSeconds = 5}) {
final ScaffoldMessengerState scaffoldMessenger =
ScaffoldMessenger.of(context);
scaffoldMessenger.showSnackBar(SnackBar(
content: Text(message),
duration: Duration(seconds: durationSeconds),
action: SnackBarAction(
label: 'HIDE',
onPressed: () {
scaffoldMessenger.hideCurrentSnackBar();
},
)));
}
Then just call it from your views.
.
.
.
showToast(context, "Factor Model Created Successfully",
durationSeconds: 30);
Navigator.pop(context, 'save');