Repository: flutter
Issue: Introduce ListenableFutureBuilder to Simplify Using Controllers in the Composition of a StatelessWidget
Labels: c: new feature,framework,c: proposal,P3,team-framework,triaged-framework

## Use case

I should be able to instantiate a `Listenable` controller with async work and use this controller to trigger rebuilds without using some external means to hold onto the `Listenable` variable. I also shouldn't have to subtype `StatefulWidget` to do this. I should be able to use straightforward composition to achieve this. I've created a widget (See PR #121963)  that solves this problem, and there is a draft PR for it. But, before I jump to that, I will try to explain the existing problem.

Check out a live sample of the proposed solution [here on Dartpad](https://dartpad.dev/?id=3da064ae821e85b20c74c78b255aa03f).

## Existing Problem
On its face, this seems like something that would already be straightforward in Flutter, but it isn't. If you have a controller that extends `ValueNotifier` or `ChangeNotifier`, there is no straightforward way to compose a widget that listens to this without subtyping `StatefulWidget`, using a top-level singleton or something like Provider. Let's take a look at a simple example with `AnimatedBuilder` and `ValueNotifier`.

This example is obviously wrong because the parent widget could reinstantiate `MyHomePage`.

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(),
      );
}

class MyHomePage extends StatelessWidget {
  MyHomePage({super.key});

  //A StatelessWidget should never hold onto state in this way
  ValueNotifier<int> _counter = ValueNotifier<int>(0);

  @override
  Widget build(BuildContext context) => AnimatedBuilder(
        animation: _counter,
        builder: (context, child) => Scaffold(
          appBar: AppBar(),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '${_counter.value}',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => _counter.value++,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        ),
      );
}
```

The above works well but it violates a fundamental principle of Flutter: `StatelessWidget`s should not hold onto state. This is because the parent widget could be reinstantiated, and the child widget would lose its state. This is why `StatefulWidget` exists. It allows the child widget to hold onto its state. So, we have a couple of choices:

- Turn this into a `StatefulWidget` and move the controller to the `State`
- Declare the controller as a top-level singleton
- Use something like Provider to hold onto the variable and pass it down to the child widget

None of these options are elegant solutions to the problem, and the problem gets worse when we consider async work. They all add complexity to the codebase and make it harder to reason about. 

Let's expand on the existing, already problematic example with async work.

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(),
      );
}

class MyHomePage extends StatelessWidget {
  MyHomePage({super.key});

  @override
  Widget build(BuildContext context) => FutureBuilder<ValueNotifier<int>>(
        //This is a problem because even though the FutureBuilder will store the result in 
        //State, if the parent rebuilds, this function will execute many times
        future: getController(),
        builder: (context, snapshot) => AnimatedBuilder(
          animation: snapshot.data ?? ValueNotifier<int>(0),
          builder: (context, child) => Scaffold(
            appBar: AppBar(),
            body: Center(
                child: snapshot.connectionState == ConnectionState.done
                    ? Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                          const Text(
                            'You have pushed the button this many times:',
                          ),
                          Text(
                            '${snapshot.data!.value}',
                            style: Theme.of(context).textTheme.headlineMedium,
                          ),
                        ],
                      )
                    : const CircularProgressIndicator.adaptive()),
            floatingActionButton: FloatingActionButton(
              onPressed: () => snapshot.data?.value++,
              tooltip: 'Increment',
              child: const Icon(Icons.add),
            ),
          ),
        ),
      );
}

Future<ValueNotifier<int>> getController() async =>
    Future.delayed(const Duration(seconds: 2), () => ValueNotifier<int>(0));
```

Again, this example works, but it has a problem. If the parent widget reinstantiates this widget, the async function will execute again, causing the controller to be reinstantiated. Again, the controller resets. You can also see that this version is already getting more complicated and harder to read.

Take a look at the `future` property of the `FutureBuilder`. This is problematic because it's not a function. It forces the composition to reference a declaration or call a function to get the `Future`.  Every time the parent widget rebuilds, the `FutureBuilder` will reference the declaration or call the function again. This recently caused the Flutter team to redo a YouTube video for clarification (#112676). In an ideal world, the FutureBuilder would hold onto the value from the `Future`, as it does, but only call a function lazily when it needs that value. 

## Proposal

The good news is that there is an easy way to solve several of these issues in one hit and massively simplify using controllers in the composition of a  `StatelessWidget`. 

Introducing `ListenableFutureBuilder`:

```dart
import 'package:flutter/material.dart';
import 'package:flutter_application_4/listenable_future_builder.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const MyHomePage(),
      );
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) =>
      ListenableFutureBuilder<ValueNotifier<int>>(
        //This function is safe because it will only execute once
        //ListenableFutureBuilder makes the first call, it will not execute the call again
        listenable: getController,
        builder: (context, child, snapshot) => Scaffold(
          appBar: AppBar(),
          body: Center(
              child: snapshot.connectionState == ConnectionState.done
                  ? Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        const Text(
                          'You have pushed the button this many times:',
                        ),
                        Text(
                          '${snapshot.data!.value}',
                          style: Theme.of(context).textTheme.headlineMedium,
                        ),
                      ],
                    )
                  : const CircularProgressIndicator.adaptive()),
          floatingActionButton: FloatingActionButton(
            onPressed: () => snapshot.data?.value++,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        ),
      );
}

Future<ValueNotifier<int>> getController() async =>
    Future.delayed(const Duration(seconds: 2), () => ValueNotifier<int>(0));
```

This widget simplifies the code from the previous two examples, but more importantly, it solves the problem of the controller being reinstantiated when the parent widget is reinstantiated. It does this by calling `listenable` function only once and then listening to the `Listenable` returned by that function. `ListenableFutureBuilder` is a `StatefulWidget` and it holds onto the `Listenable` controller. This means that the controller will not be reinstantiated when the parent widget is reinstantiated. The result is a large simplification, but also removes dependence on things like Provider to ferry the controller in via an inherited widget.