Working with Futures and Streams in Dart and Flutter

Joshua Hall

The most important features of almost any app relies on the ability to interact with external API’s and databases. With that comes the problem of dealing with code that runs in an order different to how it was written while waiting for certain requests and operations to complete. We’re going to look into how Dart, particularly for Flutter, works with managing that complexity.

Much of this may seem very familiar if you come from a front-end background, since Dart resembles JavaScript in a lot of ways. I’m going to assume that you have little to no knowledge of asynchronous concepts and techniques.

Setup

For our first example, I’m going to be using the REST Countries API, which will just return some basic information on whatever country we give it. This will also of course require the http package.

For our UI we’ll just use a centered button that will run our GetCountry function.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  http: ^0.12.0+2

main.dart

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert'; //This allows us to convert the returned JSON data into something Dart can use.

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  void GetCountry() async {}

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: MaterialButton(
          onPressed: () => GetCountry(),
          child: Container(
            color: Colors.blue,
            padding: EdgeInsets.all(15),
            child: Text('Get Country', style: TextStyle(color: Colors.white))),
        ),
      ),
    );
  }
}

The Problem with Synchronous Code

While having every line in our code run in order is pretty straight forward to understand, that’s not always possible to achieve. When we send a request for information to an API, it’ll take some time before we get a response while our machine will be waiting for it to complete, halting things that may have nothing to do with the initial request. The problem is we don’t want our script to stop running every time something takes a while, but we also wouldn’t want it to run anything that relies on the return data prematurely, which could cause an error despite the request being successful.

The best of both worlds would be to set up our logic in a way that allows our machine to work ahead while waiting for a request to return while only letting code dependent on that request run when it’s available.

Our app’s data is most likely going to be in one of four forms, depending on whether or not they’re already available and whether or not they’re singular.

Async Table

You’re probably already very familiar with the standard strings and lists, futures and streams are the more interesting things that we’ll be exploring.

Then/CatchError

Very similar to try…catch in JavaScript, Dart lets us chain methods together so we can easily pass the return data from one to the next and it even returns a promise-like data type, called futures. Futures are any singular type of data, like a string, which will be available later.

To use this technique, we just need to do whatever operations we need, like send a get request to REST Countries, then just chain .then with our returned data passed-in as a parameter, use it however we want and/or we could keep chaining .then forever. For error handling we can cap off our chain with a .catchError and throw whatever was passed to it.

main.dart

// Since we're returning a Future, we must set our function to type Future.
Future GetCountry(country) {
    String countryUrl = 'https://restcountries.eu/rest/v2/name/$country';
    http
      .get(countryUrl)
      .then((response) => jsonDecode(response.body)[0]['name'])
      .then((decoded) => print(decoded))
      .catchError((error) => throw(error));
  }

// Update our Button to use our function
MaterialButton(
  onPressed: () => GetCountry('canada'),
  (...)
)

Async/Await

Try/CatchError definitely works well, but there’s also an alternative syntax that many find to be much for readable.

Async/Await works exactly the same as in JavaScript, we use the async keyword after our function name and add the await keyword before anything that needs some time to run, like our get request. Now everything after it will be ran when a value has been returned. For error handling we can just throw the whole thing into a try/catch block.

main.dart

Future GetCountry(country) async {
  String countryUrl = 'https://restcountries.eu/rest/v2/name/$country';

  try {
    http.Response response = await http.get(countryUrl);
    Object decoded = jsonDecode(response.body)[0]['name'];
    print(decoded);
  } catch (e) { throw(e); }
}

Streams

Something special with Dart is its use of streams for when we have many values being loaded asynchronously and instead of opening a connection once, like with our get request, we can make it stay open and ready for new data.

Since our example would get a bit too complicated by setting it up with a backend that allows streams, like using Firebase or GraphQL, we’ll simulate a change in a chat application database by emitting a new ‘message’ every second.

We can create a simple stream with the StreamController class, which works similarly to a List, since it’s just a list of futures. We can control our stream with the properties on stream, like listen and close to start and stop it.

It's important to always use close() when your widget is removed. Streams will run continuously until they are shut off and will eat away at computing power even when the original widget is gone.

main.dart

StreamController<String> streamController = StreamController();

  void newMessage(int number, String message) {
    final duration = Duration(seconds: number);
    Timer.periodic(duration, (Timer t) => streamController.add(message));
  }

  void initState() {
    super.initState();

    streamController.stream.listen((messages) => print(messages));

    newMessage(1, 'You got a message!');
  }

  void dispose() {
    streamController.close();
    super.dispose();
  }

Standard streams can be a bit limited in that they only allow for one listener at a time. Instead we can use the broadcast property on the StreamController class to open up multiple channels.

main.dart

StreamController<String> streamController = StreamController.broadcast();

void initState() {
  super.initState();

  streamController.stream.listen((messages) => print('$messages First'));
  streamController.stream.listen((messages) => print('$messages Second'));

  newMessage(1, 'You got a message!');
}

Conclusion

For me, working with asynchronous code was one of the hardest parts about learning programming and I hope I helped spare someone from any of the difficulty of understanding this strange style of code. Hopefully this short intro to asynchronous programming in Dart was helpful in your start to developing intelligent and dynamic apps.

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#