I am new to Flutter and trying to trigger a snack bar on page load if a message was returned from the page I navigated from. I have managed to get the message to display on a button click, but get an error stating that my context does not have a Scaffold if I try to do it elsewhere.
I am also struggling to find an example of how to show a sack bar without user interaction, so if anyone has a reference, that would surely go a long way in helping as well.
Here is a simplified version of my view:
class LandingView extends StatefulWidget {
final LandingViewModel viewModel;
LandingView(this.viewModel);
#override
State<StatefulWidget> createState() {
return new _ViewState();
}
}
class _ViewState extends State<LandingView> {
#override
void initState() {
super.initState();
}
void _showSnackbar(context, message) {
final snackBar = SnackBar(
content: Text(message),
);
Scaffold.of(context).showSnackBar(snackBar);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: new GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(new FocusNode());
},
child: _buildLayout(context),
),
),
);
}
Widget _buildLayout(BuildContext context) {
Map<String, dynamic> args = getArgs(context); //get value from previous page
if (args != null &&
args["Toast Message"] != null) //check if a value was returned from the previous page. This has been tested and a valid string is being returned
_showSnackbar(
context, args["Toast Message"]); //if so call snack bar function
// this throws an error saying "Scaffold.of() called with a context that does not contain a Scaffold"
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: boxConstraints.maxHeight),
child: RaisedButton(
child: Text(
"Show Snack Bar",
),
onPressed:
() {
if (args != null &&
args["Toast Message"] !=
null) //check if a value was returned from the previous page. This has been tested and a valid string is being returned
_showSnackbar(context,
args["Toast Message"]); //if so call snack bar function
//this works perfectly
}),
),
);
});
}
}
Any advice would be greatly appreciated
You're getting that because your LandingView widget is not in a Scaffold. You can fix this by putting the LandingView widget inside a StatelessWidget with a Scaffold and changing any references to LandingView to LandingViewPage:
class LandingViewPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: LandingView()
);
}
}
We can do this with addPostFrameCallback method
#override
void initState(){
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => scaffold.showSnackBar(SnackBar(content: Text("snackbar")));
}
In a stateful widget put:
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Error")));
});
}
Related
I have a Widget (MainScaffold) that listens to whether there is an internet connection or not. I have wrapped that widget around all my screens.
So depending on the state of the internet connection I show a Snackber to alert the user.
But what I have realized is that when the internet changes, all the widgets in the navigation stack call a snackbar. Therefore the snackbar calling more than ones.
So what I did was to pass the context of the screens to the MainScaffold then check if the context is on top of the stack, it will show the snackbar
So I need something like if(widget.context.mounted) show SnackBar() but I can't seem to find something like that.
Any suggestion?
class MainScaffold extends StatefulWidget {
MainScaffold(
{super.key,
required this.child,
this.ctx,
this.appBar,
this.bottomNavigationBar});
final Widget child;
final BuildContext? ctx;
final AppBar? appBar;
final BottomNavigationBar? bottomNavigationBar;
#override
State<MainScaffold> createState() => _MainScaffoldState();
}
class _MainScaffoldState extends State<MainScaffold> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: widget.appBar,
body: BlocListener<InternetCubit, InternetState>(
listener: (_, state) {
if (state is InternetDisconnected) {
if (widget.ctx != null) {
if (mounted) {
print('mounted');
ScaffoldMessenger.of(widget.ctx!).showSnackBar(
const SnackBar(
content: Text('Please check your internet connection'),
),
);
}
}
}
},
listenWhen: (previous, current) {
if (previous is InternetDisconnected &&
current is InternetConnected) {
if (widget.ctx != null) {
if (mounted) {
ScaffoldMessenger.of(widget.ctx!).showSnackBar(
const SnackBar(
content: Text('We\'re back online'),
),
);
}
}
}
return true;
},
child: widget.child,
),
bottomNavigationBar: widget.bottomNavigationBar,
);
}
}
I'm new at Flutter and still struggling with overall concept of widgets structure especially stateful ones. As far as I understand I should create all widgets stateless unless they can be changed. Then they should be stateful and hold their state.
So I have a simple screen with a list of items that is dynamic and action button:
class WalletWidget extends StatelessWidget {
final GlobalKey<_WalletItemsState> _key = GlobalKey();
void _addWalletItem(BuildContext context) async {
var asset = await Navigator.pushNamed(context, '/add_wallet_item');
if (asset == null) {
return;
}
_key.currentState!._refresh();
}
#override
Widget build(BuildContext context) => Scaffold(
body: _WalletItems(),
floatingActionButton: FloatingActionButton(
onPressed: () => _addWalletItem(context),
child: const Icon(Icons.add)),
);
}
class _WalletItems extends StatefulWidget {
const _WalletItems();
#override
State<StatefulWidget> createState() => _WalletItemsState();
}
class _WalletItemsState extends State<_WalletItems> {
List<Asset> _walletAssets = [];
void _refresh() {
setState(() {});
}
#override
Widget build(BuildContext context) => ListView.builder(
itemCount: _walletAssets.length,
itemBuilder: (context, index) =>
Text(Asset.persistenceMgr.getAll().toList()[index].title),
);
}
in my understanding whole screen should be a stateless as it merely builds a UI structure. Hence class WalletWidget extends StatelessWidget. The list widget is stateful. Adding of new elements is done at separate screen when the action button is pressed. So when I'm back from adding screen the list is updated and I need to refresh the state of the list.
Only way I've found is using a GlobalKey, which points to the _WalletItemsState. But when it comes to line _key.currentState!._refresh(); the currentState is always null. So I assume the _key isn't properly associated with list's state. How do I do that?
_key is just intialized and not used in any widget. So, the currentState is always null
Try passing the _key to Scaffold of WalletWidget.
class WalletWidget extends StatelessWidget {
final GlobalKey<_WalletItemsState> _key = GlobalKey();
void _addWalletItem(BuildContext context) async {
var asset = await Navigator.pushNamed(context, '/add_wallet_item');
if (asset == null) {
return;
}
_key.currentState!.build(context);
}
#override
Widget build(BuildContext context) => Scaffold(
key:_key,
body: _WalletItems(),
floatingActionButton: FloatingActionButton(
onPressed: () => _addWalletItem(context),
child: const Icon(Icons.add)),
);
}
What I would like to achieve: show a FAB only if a webpage responds with status 200.
Here are the necessary parts of my code, I use the async method to check the webpage:
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
late Future<Widget> futureWidget;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
void initState() {
super.initState();
futureWidget = _getFAB();
}
Future<Widget> _getFAB() async {
final response = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// return something to create FAB
return const Text('something');
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load url');
}
}
And with the following FutureBuilder I am able to get the result if the snapshot has data:
body: Center(
child: FutureBuilder<Widget>(
future: futureWidget,
builder: (context, snapshot) {
if (snapshot.hasData) {
return FloatingActionButton(
backgroundColor: Colors.deepOrange[800],
child: Icon(Icons.add_shopping_cart),
onPressed:
null); // navigate to webview, will be created later
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
)
My problem is that I want to use it here, as a floatingActionButton widget:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
[further coding...]
),
body: // Indexed Stack to keep data
IndexedStack(
index: _selectedIndex,
children: _pages,
),
floatingActionButton: _getFAB(),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>
[further coding...]
But in this case Flutter is throwing the error
The argument type 'Future' can't be assigned to the parameter type 'Widget?'.
Sure, because I am not using the FutureBuilder this way. But when I use FutureBuilder like in the coding above then Flutter expects further positional arguments like column for example. This ends in a completely different view as the FAB is not placed over the indexedstack in the typical FAB position anymore.
I have searched for several hours for a similar question but found nothing. Maybe my code is too complicated but Flutter is still new to me. It would be great if someone could help me :)
You can use the just _getFAB() method to do it. You can't assign _getFab() method's return value to any widget since it has a return type Future. And also, when you are trying to return FAB from the FutureBuilder it will return FAB inside the Scaffold body.
So, I would suggest you fetch the data from the _getFAB() method and assign those data to a class level variable. It could be bool, map or model class etc. You have to place conditional statements in the widget tree to populate the state before the data fetching and after the data fetching. Then call setState((){}) and it will rebuild the widget tree with new data. Below is an simple example.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class FabFuture extends StatefulWidget {
const FabFuture({Key? key}) : super(key: key);
#override
State<FabFuture> createState() => _FabFutureState();
}
class _FabFutureState extends State<FabFuture> {
bool isDataLoaded = false;
#override
void initState() {
super.initState();
_getFAB();
}
Future<void> _getFAB() async {
final response = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
if (response.statusCode == 200) {
isDataLoaded = true;
setState(() {});
} else {
isDataLoaded = false;
//TODO: handle error
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: const Center(
child: Text('Implemet body here'),
),
floatingActionButton: isDataLoaded
? FloatingActionButton(
backgroundColor: Colors.deepOrange[800],
child: const Icon(Icons.add_shopping_cart),
onPressed: null)
: const SizedBox(),
);
}
}
Here I used a simple bool value to determine if I should show the FAB or not. The FAB will only show after the data is successfully fetched.
After practicing these ways and you get confident about them, I would like to suggest learning state management solutions to handle these types of works.
I've got a MaterialApp which uses a builder with a scaffold in it. When I navigate from page to page the scaffold and app bar does not rebuild, but the body of the scaffold does:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:raven_front/pages/pages.dart';
Future<void> main() async {
runApp(RavenMobileApp());
}
class RavenMobileApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/splash',
routes: pages.routes(context),
builder: (context, child) {
return SafeArea(
child: Scaffold(
extendBodyBehindAppBar: false,
appBar: BackdropAppBar(), // pretty much regular app bar
body: child!, // pages of app
));
},
);
}
}
but when I'm on a page where I need to show a bottom modal sheet, or alert box, or anything with a scrim, it doesn't apply to the app bar:
for example, I might make the ModalBottomSheet this way
await showModalBottomSheet<void>(
context: context,
elevation: 1,
barrierColor: AppColors.black38, // not applied to app bar
shape: components.shape.topRounded,
builder: (BuildContext context) {
...
});
With my setup of having a builder in the MaterialApp, how can I get the scrim to cover everything?
I tried saving the context used in the MaterialApp (highest level) and using that in the modal sheet, but that errored saying that context doesn't have a Navigator. I'm hoping I can keep the current design but extend the scrim over the app bar somehow.
can you believe it, I had to roll my own. the other option was to abandon the builder material app design mentioned in the question. This is what I had to do:
app bar:
Stack(
children: [
appBar,
AppBarScrim(),
])
app bar scrim
class AppBarScrim extends StatefulWidget {
const AppBarScrim({Key? key}) : super(key: key);
#override
State<AppBarScrim> createState() => _AppBarScrimState();
}
class _AppBarScrimState extends State<AppBarScrim> {
late List listeners = [];
final Duration waitForSheetDrop = Duration(milliseconds: 50);
bool applyScrim = false;
#override
void initState() {
super.initState();
listeners.add(streams.app.scrim.listen((bool value) async {
if (applyScrim && !value) {
await Future.delayed(waitForSheetDrop);
setState(() {
applyScrim = value;
});
}
if (!applyScrim && value) {
setState(() {
applyScrim = value;
});
}
}));
}
#override
void dispose() {
for (var listener in listeners) {
listener.cancel();
}
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
Navigator.of(components.navigator.routeContext!).pop();
streams.app.scrim.add(false);
},
child: AnimatedContainer(
duration: waitForSheetDrop,
color: applyScrim ? Colors.black38 : Colors.transparent,
height: applyScrim ? 56 : 0,
));
}
}
showDialog( // and show bottom modal sheet...
..
builder: (BuildContext context) {
streams.app.scrim.add(true); // trigger
return AlertDialog(...);
}).then((value) => streams.app.scrim.add(false)); // remove trigger
I'm trying to show a splash screen on initial app startup until I have all of the data properly retrieved. As soon as it's there, I want to navigate to the main screen of the app.
Unfortunately, I can't find a good way to trigger a method that runs that kind of Navigation.
This is the code that I'm using to test this idea. Specifically, I want to run the command Navigator.pushNamed(context, 'home'); when the variable shouldProceed becomes true. Right now, the only way I can think to do it is to display a button that I need to press to trigger the navigation code:
import 'package:flutter/material.dart';
import 'package:catalogo/src/navigationPage.dart';
class RouteSplash extends StatefulWidget {
#override
_RouteSplashState createState() => _RouteSplashState();
}
class _RouteSplashState extends State<RouteSplash> {
ValueNotifier<bool> buttonTrigger;
bool shouldProceed = false;
_fetchPrefs() async { //this simulates the asynchronous function
await Future.delayed(Duration(
seconds:
1)); // dummy code showing the wait period while getting the preferences
setState(() {
shouldProceed = true; //got the prefs; ready to navigate to next page.
});
}
#override
void initState() {
super.initState();
_fetchPrefs(); // getting prefs etc.
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: shouldProceed
? RaisedButton(
onPressed: () {
print("entered Main");
Navigator.pushNamed(context, 'home'); // <----- I want this to be triggered by shouldProceed directly
},
child: Text("Continue"),
)
: CircularProgressIndicator(), //show splash screen here instead of progress indicator
),
);
}
}
So, in short, how can I trigger a function that runs the Navigation code when shouldProceed changes?
All you have to do is after you get the preferences, just navigate to the screen and have the build method just build a progress indicator.
Try this:
_fetchPrefs() async {
await Future.delayed(Duration(seconds: 1));
Navigator.of(context).pushNamed("home"); //stateful widgets give you context
}
Here's your new build method:
#override
Widget build(BuildContext context) {
return Center(child: CircularProgressIndicator());
}
I've made a DartPad to illustrate: https://dartpad.dartlang.org/431fcd9a1ea5748a82506f13be042e85
Create a widget which can be shown or hidden similar to my ProgressBar code. Show and hide based on a timer or when the data load from the api has completed.
ProgressBar progressBar = new ProgressBar();
progressBar.show(context);
Provider.of<Api>(context, listen: false)
.loadData(context, param)
.then((data) {
progressBar.hide();
Provider.of<Api>(context, listen: false).dataCache.login = loginValue;
Navigator.pop(context, true);
Navigator.pushNamed(context, "/home");
}
});
replace the ProgressBar code with your splash screen:
class ProgressBar {
OverlayEntry _progressOverlayEntry;
void show(BuildContext context){
_progressOverlayEntry = _createdProgressEntry(context);
Overlay.of(context).insert(_progressOverlayEntry);
}
void hide(){
if(_progressOverlayEntry != null){
_progressOverlayEntry.remove();
_progressOverlayEntry = null;
}
}
OverlayEntry _createdProgressEntry(BuildContext context) =>
OverlayEntry(
builder: (BuildContext context) =>
Stack(
children: <Widget>[
Container(
color: Colors.white.withOpacity(0.6),
),
Positioned(
top: screenHeight(context) / 2,
left: screenWidth(context) / 2,
child: CircularProgressIndicator(),
)
],
)
);
double screenHeight(BuildContext context) =>
MediaQuery.of(context).size.height;
double screenWidth(BuildContext context) =>
MediaQuery.of(context).size.width;
}