How to use FAB with FutureBuilder - flutter

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.

Related

Flutter - How to pass a future to a widget without executing it?

I am using FutureBuilder in one of my widgets and it requires a future. I pass the future to the widget through its constructor. The problem is that while passing the future to the widget it gets automatically executed. Since the FutureBuilder accepts only a Future and not a Future Function() i am forced to initialize a variable which in turn calls the async function. But i don't know how to pass the Future without it getting executed.
Here is the complete working example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final icecreamSource = DataService.getIcecream();
final pizzaSource = DataService.getPizza();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
children: [
MenuButton(label: 'Ice Cream', dataSource: icecreamSource),
MenuButton(label: 'Pizza', dataSource: pizzaSource),
]
),
),
),
);
}
}
class MenuButton extends StatelessWidget {
final String label;
final Future<String> dataSource;
const MenuButton({required this.label, required this.dataSource});
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
child: Text(label),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => AnotherPage(label: label, dataSource: dataSource)))
),
);
}
}
// Mock service to simulate async data sources
class DataService {
static Future<String> getIcecream() async {
print('Trying to get ice cream...');
return await Future.delayed(const Duration(seconds: 3), () => 'You got Ice Cream!');
}
static Future<String> getPizza() async {
print('Trying to get pizza...');
return await Future.delayed(const Duration(seconds: 2), () => 'Yay! You got Pizza!');
}
}
class AnotherPage extends StatefulWidget {
final String label;
final Future<String> dataSource;
const AnotherPage({required this.label, required this.dataSource});
#override
State<AnotherPage> createState() => _AnotherPageState();
}
class _AnotherPageState extends State<AnotherPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.label)),
body: Center(
child: FutureBuilder<String>(
future: widget.dataSource,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if(snapshot.hasData) {
return Text('${snapshot.data}');
} else if(snapshot.hasError) {
return Text('Error occurred ${snapshot.error}');
} else {
return Text('Fetching ${widget.label}, please wait...');
}
}
),
),
);
}
}
The intended behaviour is that when i press the "Ice Cream" or "Pizza" button on the main page, the widget/screen named "Another Page" should appear and the async request should get executed during which the loading message should be displayed. However, what is happening is that on loading the homepage, even before pressing any of the buttons, both the async requests are getting executed. On pressing any of the buttons, the loading message does not appear as the request is already completed so it directly shows the result, which is totally undesirable. I am now totally confused about Futures and Future Functions. Someone please help me out.
Instead of passing the Future you could pass the function itself which returns the Future. You can try this example here on DartPad.
You have to modify MyApp like this:
final icecreamSource = DataService.getIcecream; // No () as we want to store the function
final pizzaSource = DataService.getPizza; // Here aswell
In MenuButton and in AnotherPage we need:
final Future<String> Function() dataSource; // Instead of Future<String> dataSource
No we could pass the future directly to the FutureBuilder but it's bad practice to let the FutureBuilder execute the future directly as the build method gets called multiple times. Instead we have this:
class _AnotherPageState extends State<AnotherPage> {
late final Future<String> dataSource = widget.dataSource(); // Gets executed right here
...
}
Now we can pass this future to the future builder.
instead passing Future function, why you dont try pass a string ?
Remove all final Future<String> dataSource;. You dont need it.
you can use the label only.
.....
body: Center(
child: FutureBuilder<String>(
future: widget.label == 'Pizza'
? DataService.getPizza()
: DataService.getIcecream(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
....
i just test it in https://dartpad.dev . its work fine.
you dont have to make complex, if you can achive with simple way.

initializing variables before build in flutter?

I am trying to initialize a variable in the initstate. Before initializing null value is used in my scaffold. How can I wait for a variable then later load the build?I am using shared_preference plugin.
This is my initstate:
void initState() {
super.initState();
_createInterstitialAd();
Future.delayed(Duration.zero, () async {
prefs = await SharedPreferences.getInstance();
future = Provider.of<Titles>(context, listen: false).fetchAndSetPlaces();
identifier = await getimages();
});
Here I am checking if it is null and it is always null:
#override
Widget build(BuildContext context) {
print(prefs);
Why you want to initialize variable in init state .
Use Future Builder inside your context and fetch variable data first and then your build will execute.
class FutureDemoPage extends StatelessWidget {
Future<String> getData() {
return Future.delayed(Duration(seconds: 2), () {
return "I am data";
// throw Exception("Custom Error");
});
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Future Demo Page'),
),
body: FutureBuilder(
builder: (ctx, snapshot) {
// Checking if future is resolved or not
if (snapshot.connectionState == ConnectionState.done) {
// If we got an error
if (snapshot.hasError) {
return Center(
child: Text(
'${snapshot.error} occured',
style: TextStyle(fontSize: 18),
),
);
// if we got our data
} else if (snapshot.hasData) {
// Extracting data from snapshot object
final data = snapshot.data as String;
return Center(
child: Text(
'$data',
style: TextStyle(fontSize: 18),
),
);
}
}
// Displaying LoadingSpinner to indicate waiting state
return Center(
child: CircularProgressIndicator(),
);
},
// Future that needs to be resolved
// inorder to display something on the Canvas
future: getData(),
),
),
);
}
}
If you want to initialize a variable later then we have late keyword for that purpose. You can use that like below when declaring a variable :
late String prefs;
First you have to understand the Life cycle of flutter.
I make it simple for you..
createState()
initState()
didChangeDependencies()
build()
didUpdateWidget()
setState()
dispose()
So Every override function have fixed functionality. For example initState can use only for normal initialisation. It can not hold/await the flow for several time. So that why any async method is not applicable inside the initState.
For Solution you have to use FutureBuilder for awaiting data or before navigating to the next page initialise the preferences then proceeded.
i assume you have 3 different condition :
first wait data for prefences,
get future data from that prefences,
and render result.
try this: bloc pattern
its basically a Stream to different State and Logic part of app ui

prevent some items from Refreshing data in Listview.builder

I have a Listview.builder inside a FutureBuilder which taking some data from an http request.i have a bool closed i want to prevent some items from refreshing if status bool is true
how can I do that
You can achieve this by placing your call in initState. In this way you can make sure that it will get the data only once.
example:
class FutureSample extends StatefulWidget {
// Create instance variable
#override
_FutureSampleState createState() => _FutureSampleState();
}
class _FutureSampleState extends State<FutureSample> {
Future myFuture;
Future<String> _fetchData() async {
await Future.delayed(Duration(seconds: 10));
return 'DATA';
}
#override
void initState() {
// assign this variable your Future
myFuture = _fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FutureBuilder(
future: myFuture,
builder: (ctx, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.toString());
}
return CircularProgressIndicator();
},
),
),
);
}
}
In that way you don't need a bool value. There are also different ways to achieve or extend your request. You can check this article for more informations: https://medium.com/flutterworld/why-future-builder-called-multiple-times-9efeeaf38ba2

Trigger snackbar message on page load in Flutter

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

Flutter - How to navigate to another page on an asynchronous value

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