Link

Nesting async builders

Basic nesting

Sometimes you might want a widget to depend on the result of multiple asynchronous tasks, including having one request depend on the result of another.

In this example we are calling getUser with a userId string to get a User object, then once that is complete we call user.searchFriends to get a Friends, finally once that is complete we build a Text that uses them:

build(context) => InitBuilder.arg<Future<User>, String>(
  getter: getUser,
  arg: userId,
  builder: (context, future) => AsyncBuilder<User>(
    future: future,
    waiting: (context) => CircularProgressIndicator(),
    builder: (context, user) => InitBuilder.arg<Future<Friends>, String>(
      getter: user.searchFriends,
      arg: queryString,
      builder: (context, future) => AsyncBuilder<Friends>(
        future: future,
        waiting: (context) => CircularProgressIndicator(),
        builder: (context, friends) =>
          Text('Name: ${user.name} Friends: $friends'),
      ),
    ),
  ),
);

Other than being ugly, this can also cause the progress indicator to look like its stuttering as it would get re-created when the first future completes.

What you should do instead is make a function that completes with every value required by the UI at once:

static Tuple2<User, Friends> getUserAndFriends(
  String userId,
  String queryString,
) async {
  var user = await getUser(userId);
  var friends = await user.searchFriends(queryString);
  return Tuple2(user, friends);
}

build(context) =>
  InitBuilder.arg2<Future<Tuple2<User, Friends>>, String, String>(
    getter: getUserAndFriends,
    arg1: userId,
    arg2: queryString,
    builder: (context, future) => AsyncBuilder(
      future: future,
      builder: (context, tuple) =>
        Text('Name: ${tuple.item1.name} Friends: ${tuple.item2}'),
    ),
  );

In this case we’re using a Tuple2 from package:tuple to return two values at the same time.


Streams

Another common problem is when you build widgets from a stream, but then need to make another request depending on the information from the stream.

In this example, we take a stream of Users rather than a future, but requests Friends in the same way:

build(context) => InitBuilder<Stream<User>, String>(
  getter: getUsers,
  builder: (context, stream) => AsyncBuilder(
    stream: stream,
    waiting: (context) => CircularProgressIndicator(),
    builder: (context, user) => InitBuilder.arg<String, Friends>(
      getter: user.searchFriends,
      arg: queryString,
      builder: (context, future) => AsyncBuilder(
        future: future,
        waiting: (context) => CircularProgressIndicator(),
        builder: (context, friends) =>
          Text('Name: ${user.name} Friends: $friends'),
      ),
    ),
  ),
);

What you can do instead is use Stream.asyncMap to add friends to the stream so that we only need a single builder:

static Stream<Tuple2<User, Friends>> getUsersAndFriends(
  String queryString,
) => getUsers().asyncMap((user) async =>
  Tuple2(user, await user.getFriends(queryString)));

build(context) => InitBuilder.arg<Stream<Tuple2<User, Friends>>, String>(
  getter: getUsersAndFriends,
  arg: queryString,
  builder: (context, stream) => AsyncBuilder(
    stream: stream,
    waiting: (context) => CircularProgressIndicator(),
    builder: (context, tuple) =>
      Text('Name: ${tuple.item1.name} Friends: ${tuple.item2}'),
  ),
);