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

Question

Asked by irshukhan on September 21, 2022 (source).

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.

Answer

Question answered by TripleNine (source).

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.

DART FLUTTER FLUTTER-FUTUREBUILDER
SHARE: