Flutter Crash Course - Step 2
Tags: builders, flutter, streams, futures
Published: 2021-02-19 16:00:55

[Part one](https://dev.to/nmwilk/flutter-crash-course-step-1-4f59) showed us how to create a static UI. Now we'll make it dynamic.

Given our previous example, let's say we want the button to be disabled on startup because our app needs to talk to an API before enabling it. To achieve this we'll need a few steps:

1/ The enabled state of a `Button` `Widget` is determined by whether or not there's an action set on the `onPressed` property. At present there is one set:

```dart
            RaisedButton(
              child: Text("A Button"),
              onPressed: () => print("Pressed"),
            ),
```

Let's change that:

```dart
            RaisedButton(
              child: Text("A Button"),
              onPressed: buttonEnabled ? () => print("Pressed") : null,
            ),
```

`buttonEnabled` is a variable that currently doesn't exist. So how do we pass this in? Via a Builder class (not a type - just a concept), in this case a `FutureBuilder`.

2/ We wrap the `RaisedButton` inside this other `FutureBuilder` `Widget`.

```dart
            FutureBuilder<bool>(
              future: null,
              builder: (context, AsyncSnapshot<bool> snapshot) {
                final buttonEnabled = snapshot.hasData && snapshot.data;
                return RaisedButton(
                  child: Text("A Button"),
                  onPressed: buttonEnabled ? () => print("Pressed") : null,
                );
              }
            ),
```

We don't have a `Future<bool>` yet, hence it's been left blank `future: null`, but let's explain the rest. To build the UI, the `FutureBuilder`'s `builder` function will be called (at least) once before the future has completed, then once it has completed. Whether it's completed and, if it has, its result will be delivered in the snapshot. We enable or disable the button based on this snapshot.

A good way of thinking of a Builder is: 

**I need a small piece of logic to determine which Widget to show, or what property to set on a Widget. So I need a Builder to put the condition in.**

Or even simpler:

**I need an if statement, so I need a Builder.**

Now we'll create a `Future` that the `FutureBuilder` can listen to.

3/ To do this, we'll need to change our `Widget` to be one that has state. Currently, it's a `StatelessWidget`, which is for `Widget`s that don't need to know anything about state. As we're introducing a `Future`, and we only want that `Future` to execute once, we'll need the `Widget` to have state.

Change `MyApp` to extend `StatefulWidget` rather than `StatelessWidget` via Android Studio's alt+enter:

![Stateless to Stateful](https://thepracticaldev.s3.amazonaws.com/i/w37y8lr6i98zg2uloxu1.gif)

The eagle-eyed may notice that, in terms of text, nothing much changed. Our `build` function moved to a new inner class, and `MyApp` now just creates an instance of this inner class.

4/ So now let's add the `Future` as a property of `_MyAppState` and create it in the `_MyAppState`'s `initState` method:

```dart
class _MyAppState extends State<MyApp> {
  Future<bool> future;

  @override
  void initState() {
    super.initState();
    future = Future.delayed(Duration(seconds: 2), () => true);
  }
  
  @override
  Widget build(BuildContext context) {
```

5/ Now hook it up to the `FutureBuilder`:

```dart
              FutureBuilder<bool>(
                future: future,
                builder: (context, AsyncSnapshot<bool> snapshot) {
```

Run the app:

![App starts up with button disabled, then enabled 2 seconds later](https://thepracticaldev.s3.amazonaws.com/i/dmt14pvwhmyxelq0wkwt.gif)

## Other Builders

Just as frequently used as `FutureBuilder`, there is `StreamBuilder` (actually far more often used in my case). This is very similar with the exception that it listens to a stream of data that can change multiple times. E.g. visibility of a progress indicator, or the contents of a list.

A good practice is to keep all of the state of your screen in a data class, and have your business logic update this state and send out the updates via a stream. Your UI (`Widget`s) can listen to this using a `StreamBuilder`, and update the UI to reflect whatever is in this state.

In this example we'll adapt Android Studio's default Flutter project to use a stream and perform asynchronous code whenever the button is tapped, like this:

![Each button tap increments the counter after 1 second](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yvf6n0a7edf76wig6i89.gif)

Let's define a simple class to hold our state:

```dart
class MyHomePageState {
  final int counter;
  final bool showSpinner;

  MyHomePageState(this.counter, this.showSpinner);
}
```

Now we need a plain Dart class (i.e. not Flutter code) to perform business logic:

```dart
class Logic {
  // private stream controller that will manage our stream
  final _streamController = StreamController<MyHomePageState>();

  // expose the stream publicly
  Stream<MyHomePageState> get homePageState => _streamController.stream;

  MyHomePageState _currentState;

  Logic() {
    _updateState(MyHomePageState(0, false));
  }

  // update the state and broadcast it via the stream controller
  void _updateState(MyHomePageState newState) {
    _currentState = newState;
    _streamController.sink.add(_currentState);
  }

  // increment the counter after waiting 1 second, to simulate a network call for example.
  Future<void> incrementCounter() async {
    _updateState(MyHomePageState(_currentState.counter, true));
    await Future.delayed(Duration(seconds: 1));
    _updateState(MyHomePageState(_currentState.counter + 1, false));
  }
}
```

The UI will now play dumb and simply use this `Logic` class by sending events to it (button taps), and listening for what to display (the contents of the Stream).

```dart
class MyHomePage extends StatelessWidget {
  final Logic logic = Logic();

  Widget build(BuildContext context) {
    return StreamBuilder<MyHomePageState>(
      stream: logic.homePageState,
      builder: (context, AsyncSnapshot<MyHomePageState> snapshot) {
        print("snapshot: ${snapshot?.data ?? "null"} ");
        final asyncInProgress = snapshot.data?.showSpinner ?? true;
        return Scaffold(
          appBar: AppBar(title: Text("Stream Example")),
          body: Center(
            child: asyncInProgress
                ? CircularProgressIndicator()
                : Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Text('You have pushed the button this many times:'),
                      Text(
                        '${snapshot.data.counter}',
                        style: Theme.of(context).textTheme.headline4,
                      ),
                    ],
                  ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: asyncInProgress ? null : logic.incrementCounter,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
        );
      },
    );
  }
}
```
Note that the floating action button is only enabled when the progress spinner isn't shown, so it can't be pressed again until the asynchronous operation is complete.

The final part of this short series will cover navigation, which is a simple pattern that can be applied across any app.