Make use of ScaffoldMessenger in a GetX Controller - flutter

Is it possible to use something like the scaffoldMessenger in a GetX Controller?
I'd like to display a SnackBar via the scaffoldMessenger when a task is complete.
For example, I have a controller that uploads data to Firestore, and I'd like to show the SnackBar when this is complete:
final ScaffoldMessengerState scaffoldMessenger = Get.find<ScaffoldMessengerState>();
Future<void> startUpload(SaveGame saveGame) async {
final UploadTask taskSnapshot = backupService.uploadFile(saveGame);
await taskSnapshot
.whenComplete(() =>
scaffoldMessenger.showSnackBar(
const SnackBar(content: Text('Snack-tastic')),
));
}
I tried wiring that into my main.dart
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
final ScaffoldMessengerState scaffoldMessenger =
Get.put<ScaffoldMessengerState>(ScaffoldMessenger.of(context));
return GetMaterialApp(...)
}
}
But that just threw an exception
No ScaffoldMessenger widget found.
Typically, the ScaffoldMessenger widget is introduced by the MaterialApp at the top of your application widget tree.
I'm aware of the GetX GetBar and SnackBar alternative.
How would you typically solve this, would the pattern by to have my startUpload return and then in the 'screen/page' have the logic to display the snackbar where it has access to the BuildContext?

You need to wrap using the Builder to use the context of the MaterialApp like below.
#override
Widget build(BuildContext context) {
return GetMaterialApp(
home: Builder(builder: (context) {
Get.put<ScaffoldMessengerState>(ScaffoldMessenger.of(context));
return const TestPage();
}),
);
}

Related

How to get StreamProvider data in children with new routes in Flutter (Dart)

I am using the StreamProvider method to wrap my widgets with certain data, such as Auth (which is working anywhere in my app) from Firebase Auth. I want to do the same with a Firestore value but it only seems to work one level deep.
I have a database call that finds an employees profile once the auth check is done. When I try get the employee from my Home() widget with Provider.of(context) it works great:
This is my wrapper widget (which is my main file's home: widget)
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
print(user.uid);
// Return either home or authenticate widget
if (user == null) {
return Authenticate();
}
else {
return StreamProvider<Employee>.value(
value: DatabaseService().linkedEmployee(user.uid),
child: Home(),
);
}
}
}
The Database Service function from DatabaseService():
// Get Linked Employee
Stream<Employee> linkedEmployee(String uid) {
return employeesCollection.where("linkedUser", isEqualTo: uid).snapshots().map(_linkedEmployeeFromSnapShot);
}
Employee _linkedEmployeeFromSnapShot(QuerySnapshot snapshot) {
final doc = snapshot.documents[0];
return Employee(
eId: doc.data["eId"],
employeeCode: doc.data["employeeCode"],
fName: doc.data["fName"],
lName: doc.data["lName"],
docId: doc.documentID
);
}
I can access Provider.of<User>(context) from any widget anywhere in my tree. So why can't I do the same for Provider.of<Employee>(context) ?
When I try that in any widget other than Home() I get the error:
Error: Could not find the correct Provider above this Vehicles Widget
For example, in my widget Vehicles:
class Vehicles extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
final employee = Provider.of<Employee>(context);
...
The User Provider works fine, I can print it out, but the employee provider does not work.
Is it something to do with context? Thanks, any advice would be appreciated.
How I'm navigating to the Vehicles() widget from Home() with a raised button with this event :
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Vehicles())
);
},
Here is a more explained reply hence I think some encounter this issue and I also think it's a bit tricky to get the head around it, especially when you have rules in your Firestore that requires a user to be authorized to access the database.
But generally, you want to wrap providers (that you want to access around all of the app) around MaterialApp().
So I'll show you a simple example to easier understand it.
//The App() handles makes the providers globally accessible
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FirebaseAuthProviderLayer(
child: AuthorizedProviderLayer(
authorizedChild: MatApp(child: StartSwitch()),
unAuthorizedChild: MatApp(child: SignInScreen()),
),
);
}
}
//The MaterialApp Wrapped so that it not has to be rewritten
class MatApp extends StatelessWidget {
Widget child;
MatApp({this.child});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App',
home: child,
);
}
}
class FirebaseAuthProviderLayer extends StatelessWidget {
final Widget child;
FirebaseAuthProviderLayer({this.child});
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: FirebaseAuth.instance.authStateChanges(),
child: child,
);
}
}
//And the layer that decides either or not we should attach all the providers that requires the user to be authorized.
class AuthorizedProviderLayer extends StatelessWidget {
Widget authorizedChild;
Widget unAuthorizedChild;
AuthorizedProviderLayer({this.unAuthorizedChild, this.authorizedChild});
User user;
final FirestoreService firestoreService =
FirestoreService(); //The Service made to access Firestore
#override
Widget build(BuildContext context) {
user = Provider.of<User>(context);
if (user is User)
return MultiProvider(
providers: [
StreamProvider<FirestoreUserData>.value(
value: firestoreService.streamUser(),
),
StreamProvider<AppSettings>.value(
value: firestoreService.streamSettings(),
initialData: null,
)
],
child: authorizedChild,
);
return unAuthorizedChild;
}
}

Spawning an ephemeral OverlayEntry based on a StateProvider update

TL;DR: I want to spawn a temporary overlay whenever there's a state change, and I don't really know how to approach it. (I'm using riverpod)
I have an OverlayEntry widget that I want to spawn whenever there's a state change in a StateProvider (I'm not married to anything, it doesn't have to be a StateProvider and this doesn't have to be a widget)
I tried messing around with ChangeNotifier and StateNotifier but to no avail. I'm having a tough time understanding them and I couldn't find any example of stuff that is similar to what I'm looking.
My current approach is to create a widget which is a builder for the main page, the widget for the main page should have a listener for the StateProvider and will build the main page widget. Whenever the listener is called, I want to spawn that overlay (by calling a method?)
But I can't seem to figure it out. This is my current code refs and ideas. this does not work.
CustomOverlay widget:
class CustomOverlay extends HookWidget {
final bool currentState;
PlayerOverlay({this.currentState});
#override
Widget build(BuildContext context) {
final controller = useAnimationController(
duration: Duration(milliseconds: 500);
Animation<double> _fadeInFadeOut = createCurvedAnimation(controller);
controller.addStatusListener((status) => loopAnimation(status, controller));
return createFadeTransition(context, _fadeInFadeOut);
}
...
}
MainPageBuilder
class PlayerPageBuilder extends HookWidget {
final CustomOverlay playerOverlay;
final MainPage playerPage;
MainPageBuilder(this.customOverlay, this.mainPage);
#override
Widget build(BuildContext context) {
final currentState = useProvider
currentStateProvider.addListener(() { // obviously this doesnt exist, and I'm having a tough time figuring it out (how to use StateNotifierProvider or anything that fits)
spawnOverlayAnimation(context, customOverlay);
});
return mainPage;
}
spawnPlayerOverlayAnimation(BuildContext context, Widget child) async {
OverlayState overlayState = Overlay.of(context);
OverlayEntry overlayEntry = OverlayEntry(
builder: (context) => Positioned(
child: child,
),
);
overlayState.insert(overlayEntry);
await Future.delayed(Duration(milliseconds: 500));
overlayEntry.remove();
}
}

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

How to get a context for Navigator in Widget initState()?

I want my app to work offline without a user set, and asking for a login when connectivity is back
en excerpt of the code I'm trying:
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) =>
checkConnectivity().then((isOnline) {
if (isOnline && MyApp.store.state.user == null)
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => LoginPage()),
);
}));
}
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
...
but all I can get is this error:
Unhandled Exception: 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.
I tried to wrap my Navigator call inside a Future.delayed as described here but I got the same error

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