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

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.

Related

How to use FAB with FutureBuilder

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.

how to use riverpod, by combing futureprovider with stateprovider?

I am using futureprovider for getting response from api , but i want get the list from api and assign it to the stateprovider by using listprovider.state="data from api" , how to will it work ,
how to combine future provdier with the state provider .
UPDATED ANSWER
After a discussion with #31Carlton7, he's opened an issue on github, and after a discussion with Remi Rousselet (the creator of riverpod) we've reached a better solution for this problem.
(from the final solution on the issue)
Running the app:
main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const ProviderScope(
child: MaterialApp(
home: MyHomePage(),
),
);
}
}
Creating the foo class and provider:
foo.dart
part 'foo.g.dart';
class Foo {
final int bar;
int? baz;
Foo(
this.bar, {
this.baz,
});
}
#riverpod
class FooController extends _$FooController {
FooController(this.foo);
Foo foo;
#override
FutureOr<Foo> build() async {
foo = await getFoo();
return foo;
}
Future<Foo> getFoo() async {
await Future.delayed(const Duration(seconds: 1));
return Foo(1);
}
}
Implementation using Async capabilities:
home.dart
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Consumer(
builder: (context, ref, _) {
// Get the provider and watch it
final fooAsync = ref.watch(fooControllerProvider);
// Use .when to render UI from future
return fooAsync.when(
data: (foo) => Text('bar: ${foo.bar}, baz: ${foo.baz}'),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text(err.toString()),
);
},
),
);
}
}
Implementation using Notifier capabilities: home.dart
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Consumer(
builder: (context, ref, _) {
// Get Foo provider and set the state of it.
// Use it as if it were a State Provider.
ref.watch(fooControllerProvider.notifier).foo = Foo(3);
// Use Foo in UI (.requireValue is used to be able to listen to changes)
final foo = ref.watch(fooControllerProvider).requireValue;
// Use .when to render UI from future
return Text('bar: ${foo.bar}, baz: ${foo.baz}');
},
),
);
}
}
OLD ANSWER
This is a topic that I've been struggling with and thinking about a lot lately.
What I think is missing in Remi's answer, is the ability to convert the Future data to a maniputable data.
When you're recieving Future data using either a FutureProvider and implementing the ui using the when method OR using the FutureBuilder widget, they both will trigger a rebuild when the remote data is received, so if you try to assign the value to your StateProvider it will trigger a rebuild during another rebuild which will throw.
I currently have 2 workarounds for this, and I will be updating my answer as I get more info about this.
For this example, we'll have a future provider that will wait and then return a fake data:
final _futureCounterProv = FutureProvider(
(ref) async {
Future.delayed(
Duration(seconds: 3),
);
return Random().nextInt(100);
},
);
1. Future.microtask:
Future.microtask enables you to run an operation after the current rebuild ends.
You have to make sure that your StateProvider dependencies are in a Consumer below the Future.microtask call or the Future.microtask will be called on each state update, which will keep reseting the StateProvider's value to the future value
// this provider will provide the current value of the counter
final _counterProv = StateProvider((ref) => 0);
class Body extends ConsumerWidget {
const Body({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
return ref.watch(_futureCounterProv).when(
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
error: (error, stackTrace) {
return Text(error.toString());
},
data: (data) {
Future.microtask(
() {
// Assigning the future value to the `StateProvider`
return ref.read(_counterProv.notifier).state = data;
},
);
return Consumer(
builder: (context, ref, _) {
final count = ref.watch(_counterProv);
return Column(
children: [
IconButton(
onPressed: () {
ref
.read(_counterProv.notifier)
.update((value) => value + 1);
},
icon: const Icon(Icons.add),
),
Text(
count.toString(),
),
],
);
},
);
},
);
}
}
2. ChangeNotifierProvider:
StateProvider has 2 options to update its value: the value setter and the update method, and they both trigger a rebuild. In this workaround we want to implement a state update that does not trigger rebuild. A way to do this is by using a ChangeNotifierProvider instead of StateProvider. By using a ChangeNotifierProvider we can control our own update actions and call notifyListeners (which will trigger a rebuild) whenever we want.
You have to make sure that your ChangeNotifierProvider dependencies are in a Consumer below the updateNoNotify call, or the ChangeNotifierProvider's will keep reseting to the future's value. Also you have to make sure that all the widgets that are consuming this ChangeNotifierProvider are in the widget tree below the updateNoNotify, or they will not be rebuilt as we're not triggering a rebuild
// the new `_counterProv`
final _counterProv = ChangeNotifierProvider(
(ref) => _CounterNotifier(),
);
class _CounterNotifier extends ChangeNotifier {
int _value = 0;
int get value => _value;
void update(int Function(int value) update) {
_value = update(_value);
// trigger a rebuild
notifyListeners();
}
void updateNoNotify(int Function(int value) update) {
_value = update(_value);
}
}
// the ui
class Body extends ConsumerWidget {
const Body({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
return ref.watch(_futureCounterProv).when(
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
error: (error, stackTrace) {
return Text(error.toString());
},
data: (data) {
// calling `updateNoNotify` which does not trigger
// trigger rebuild as it does not call `notifyListeners`
ref.read(_counterProv.notifier).updateNoNotify(
(e) => data,
);
return Consumer(
builder: (context, ref, _) {
final count = ref.watch(_counterProv).value;
return Column(
children: [
IconButton(
onPressed: () {
ref.read(_counterProv.notifier).update(
(value) => value + 1,
);
},
icon: const Icon(Icons.add),
),
Text(
count.toString(),
),
],
);
},
);
},
);
}
}
These are not the safest workarounds, but they are workarounds, and I will be updating this answer once I find a safe way to do this.

How to modify a variable using async method before it use in widget build?

I have a variable named userName,which depends on databse query,so async is a must.
My older code can be concluded liks this
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<void> initUser() async{
UserTable().getUserInfo(curUserEmail).then((value)=>null);
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
initUser().then((value){
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () =>router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
//The static global variable is used in Body in other files
body: Body()
),
),
);
});
}
}
It warns that miss return,I dont knwo how to amend my code.
Thanks!!
You can achive this by using the FutureBuilder widget. Please refer the code below.
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<Map> initUser() async {
final data =
await UserTable().getUserInfo(curUserEmail);
return data;
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: initUser(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () => router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
body: Body(),
),
),
);
} else {
// Returns empty container untill the data is loaded
Container();
}
},
);
}
}

Getx argumentsbeing cleared after using showDialog() in Flutter

The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.
One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}

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