Using Futures and Streams
Using Futures and Streams Interview with follow-up questions
1. What is the difference between Futures and Streams in Flutter?
Both handle asynchronous data, but they differ in how many values they deliver:
Futures represent a single asynchronous result that completes once — with a value or an error. Use a
Futurefor a one-shot operation: a single HTTP request, reading a file, one database query.Streams represent a sequence of asynchronous events over time — zero, one, or many values, plus optional errors and a done signal. Use a
Streamfor continuous or repeated data: WebSocket messages, location updates, button-press events, query results that change.
Future one = Future.value(42); // resolves once
Stream many = Stream.fromIterable([1, 2, 3]); // emits repeatedly
await for (final n in many) print(n); // 1, 2, 3
Interview follow-ups:
- Consumption:
await(or.then) a Future;listen(orawait for) a Stream. In widgets,FutureBuildervsStreamBuilder. - A Future is eager and completes once; a Stream can keep emitting until closed and may be single-subscription or broadcast.
- Cancellation: a Stream subscription must be cancelled (
subscription.cancel(), usually indispose()) to avoid leaks; a Future has nothing to cancel. - Conversion:
someFuture.asStream()turns a Future into a one-event Stream;await foror.firstreduces a Stream toward a single value.
Follow-up 1
Can you give an example of when you would use a Future?
Sure! Here's an example of using a Future in Flutter:
Future fetchData() async {
await Future.delayed(Duration(seconds: 2));
return 'Data fetched from the server';
}
void main() {
print('Fetching data...');
fetchData().then((data) {
print('Data received: $data');
});
print('Continuing execution...');
}
In this example, the fetchData function simulates fetching data from a server by delaying for 2 seconds using Future.delayed. The fetchData function returns a Future that will eventually complete with the fetched data. The then method is used to handle the completion of the Future and print the received data.
Follow-up 2
Can you give an example of when you would use a Stream?
Certainly! Here's an example of using a Stream in Flutter:
Stream countDown(int from) async* {
for (int i = from; i >= 0; i--) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() {
print('Starting countdown...');
countDown(5).listen((count) {
print(count);
});
print('Countdown started.');
}
In this example, the countDown function is a generator function that yields a sequence of integers from a given starting value. The yield keyword is used to emit each value as an event in the Stream. The listen method is used to handle the emitted events and print them as they are received.
Follow-up 3
How can you handle errors in Futures and Streams?
To handle errors in Futures and Streams, you can use the catchError method.
- For Futures: You can chain the
catchErrormethod to a Future to handle any errors that occur during its execution. For example:
Future fetchData() async {
throw Exception('Error fetching data');
}
void main() {
fetchData().catchError((error) {
print('Error occurred: $error');
});
}
- For Streams: You can use the
onErrorcallback when listening to a Stream to handle any errors that occur during its emission. For example:
Stream countDown(int from) async* {
for (int i = from; i >= 0; i--) {
if (i == 3) {
throw Exception('Error during countdown');
}
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() {
countDown(5).listen((count) {
print(count);
}, onError: (error) {
print('Error occurred: $error');
});
}
Follow-up 4
What is the difference between async/await and Streams?
The main difference between async/await and Streams is how they handle asynchronous operations.
async/await:
async/awaitis a syntactic sugar that allows you to write asynchronous code in a more synchronous style. It allows you to pause the execution of a function until a Future completes, and then resume the execution with the value returned by the Future.asyncfunctions return a Future, andawaitcan only be used inside anasyncfunction.Streams: Streams are used for handling continuous flows of data or events. They allow you to listen to a sequence of asynchronous events over time. Streams can emit multiple values and can be listened to continuously using the
listenmethod.
In summary, async/await is used for handling a single asynchronous operation, while Streams are used for handling continuous flows of data or events.
2. How do you convert a Future into a Stream?
The direct way is the Future.asStream() method, which produces a single-event stream that emits the future's value (or error) and then closes:
Future future = Future.delayed(const Duration(seconds: 1), () => 42);
Stream stream = future.asStream(); // emits 42, then done
When you need more than a one-to-one wrapper, use an async* generator, which lets you emit multiple values derived from one or more futures:
Stream countUp(Future total) async* {
final n = await total; // await inside the generator
for (var i = 1; i <= n; i++) {
yield i; // emit each value
}
}
Interview follow-ups and gotchas:
asStream()emits exactly one event (or one error) thendone— it doesn't keep producing values. Reach forasync*or aStreamControllerwhen you need many.- Errors pass through: if the future completes with an error, the stream emits that as an error event.
- The reverse direction (
Stream→ singleFuture) isstream.first,stream.last, orawait for. - Use this when an API gives you a
Futurebut a widget (e.g.StreamBuilder) or pipeline expects aStream.
Follow-up 1
What are the benefits of converting a Future into a Stream?
Converting a Future into a Stream can be useful in scenarios where you want to process the result of a Future as a stream of values. Some benefits of converting a Future into a Stream include:
- Allows you to use stream-specific operations and transformations, such as
map,where,reduce, etc. - Enables you to easily combine multiple futures into a single stream using stream combinators like
merge,concat,zip, etc. - Provides a more flexible and reactive way of handling asynchronous data.
Follow-up 2
Can you give an example of when you would need to convert a Future into a Stream?
Sure! One example where you might need to convert a Future into a Stream is when you want to continuously fetch data from a remote server and process it as a stream of values. By converting the Future returned by the server request into a Stream, you can then use stream operations to handle the data in a more reactive and efficient manner.
Follow-up 3
What happens if an error occurs during the conversion?
If an error occurs during the conversion of a Future into a Stream, the resulting stream will emit the error as an event. You can handle the error using the onError callback of the Stream. It's important to handle errors properly to prevent them from propagating and causing unexpected behavior in your application.
3. What is the purpose of the StreamBuilder widget?
StreamBuilder is the widget that connects a Stream to the UI: it listens to the stream and rebuilds its builder every time a new event arrives, giving you an AsyncSnapshot describing the latest data, error, or connection state.
StreamBuilder(
stream: counterStream,
initialData: 0,
builder: (context, snapshot) {
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
return Text('Value: ${snapshot.data}');
},
)
Interview follow-ups and gotchas:
- Always handle all snapshot states —
hasError,waiting/no data yet, andhasData— or you'll hit null/!errors before the first event. - Don't create the stream inside
build(). A new stream object each rebuild causesStreamBuilderto resubscribe and lose state. Build the stream once (ininitState, a field, or a provider) and pass it in. - It manages the subscription for you (subscribes/cancels with the widget's lifecycle), which is its big advantage over manual
listen. - Future vs Stream: use
FutureBuilderfor a one-shot value,StreamBuilderfor ongoing events. - In 2026, many teams skip
StreamBuilderin favor of Riverpod'sStreamProviderorflutter_bloc'sBlocBuilder, which add caching and testability.
Follow-up 1
How does StreamBuilder handle asynchronous operations?
StreamBuilder handles asynchronous operations by listening to a stream and updating the widget tree whenever new data is emitted. It provides a builder function that is called whenever a new snapshot is available, allowing you to build the widget based on the current state of the stream.
Follow-up 2
Can you give an example of how to use StreamBuilder?
Sure! Here's an example of how to use StreamBuilder to display a list of items fetched from a stream:
Stream> itemStream = getItemStream();
StreamBuilder>(
stream: itemStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(snapshot.data[index].title),
);
},
);
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return CircularProgressIndicator();
}
},
);
Follow-up 3
What are some common use cases for StreamBuilder?
StreamBuilder is commonly used in Flutter applications for handling asynchronous data streams. Some common use cases include:
- Fetching and displaying real-time data from a database or API
- Updating the UI based on user interactions or events
- Implementing reactive programming patterns
- Handling state management in Flutter applications
StreamBuilder provides a convenient way to listen to streams and update the UI in response to changes in the stream's data.
4. How can you test a Stream in Flutter?
You test a stream with package:test (or flutter_test, which re-exports it) using the stream matchers — emits, emitsInOrder, emitsError, emitsDone, neverEmits. Because they're asynchronous, expect returns a Future, so you await it:
import 'package:test/test.dart';
void main() {
test('emits values in order then closes', () async {
final stream = Stream.fromIterable([1, 2, 3]);
await expectLater(
stream,
emitsInOrder([1, 2, 3, emitsDone]),
);
});
test('surfaces errors', () {
final stream = Stream.error(StateError('boom'));
expect(stream, emitsError(isA()));
});
}
Interview follow-ups and gotchas:
- Use
expectLater(andawaitit) for async matchers so the test waits for the stream to drain — a plainexpectcan finish before events arrive. emitsInOrderasserts a precise sequence; appendemitsDoneto assert the stream closes.- Broadcast vs single-subscription: a single-subscription stream can only be listened to once, so don't attach two matchers to the same instance.
- For time-based streams, pump fake time with
fakeAsync(frompackage:fake_async) instead of real delays so tests stay fast and deterministic. - Stream logic inside a Bloc/Cubit is typically tested with
bloc_test'sblocTest.
Follow-up 1
What tools or packages can you use to test Streams?
There are several tools and packages you can use to test Streams in Flutter:
testpackage: This is the official testing package provided by the Flutter team. It includes utilities for testing asynchronous code, including Streams.mockitopackage: This package provides a way to create mock objects and stub their behavior. You can use it to create mock Streams and test how your code interacts with them.flutter_testpackage: This package provides additional testing utilities specifically for Flutter apps. It includes aStreamMatcherclass that can be used to assert the values emitted by a Stream.
These packages can be added to your pubspec.yaml file and imported into your test files to use their functionalities.
Follow-up 2
Can you give an example of a test case for a Stream?
Sure! Here's an example of a test case for a Stream using the test package:
import 'package:test/test.dart';
void main() {
test('Test Stream', () {
final stream = Stream.fromIterable([1, 2, 3]);
expect(stream, emitsInOrder([1, 2, 3]));
});
}
Follow-up 3
How can you handle errors during testing?
To handle errors during testing, you can use the expectLater function from the test package instead of expect. The expectLater function returns a Future that completes when the Stream is done emitting values. You can then use the catchError method on the returned Future to handle any errors that occur.
Here's an example of how you can handle errors during testing:
import 'package:test/test.dart';
void main() {
test('Test Stream with Error', () {
final stream = Stream.error(Exception('Test Error'));
expectLater(stream, emitsError(isException));
});
}
5. What is the difference between a single-subscription Stream and a broadcast Stream?
A single-subscription stream (the default) allows exactly one listener over its lifetime. It holds its events until you call listen, delivers them to that one subscriber, and once you've listened (or cancelled) you can't listen again. Most streams are this kind — file reads, HTTP responses, anything representing a single sequence consumed once.
A broadcast stream allows many listeners simultaneously, and new listeners only receive events emitted after they subscribe — past events aren't replayed. Use it when multiple parts of the app need the same live feed (e.g. an app-wide event bus).
final controller = StreamController(); // single-subscription
final bus = StreamController.broadcast(); // broadcast
final broadcast = singleStream.asBroadcastStream(); // convert
Interview follow-ups and gotchas:
- The classic error — "Stream has already been listened to" — happens when you
listento a single-subscription stream twice (a frequent cause is recreating aStreamBuilder's stream, or listening and also passing it toStreamBuilder). - Broadcast streams don't buffer: a listener that subscribes late misses earlier events. Single-subscription streams do buffer until the first listener attaches.
- Each broadcast listener manages its own subscription and must cancel it; an error or pause in one doesn't affect the others.
- Convert with
asBroadcastStream()— but you can't go back to single-subscription.
Follow-up 1
Can you give an example of when you would use a single-subscription Stream?
A single-subscription Stream is typically used when you have a stream of events or data that should only be consumed by a single listener. For example, if you have a stream that represents user authentication events, you would want to use a single-subscription Stream to ensure that only one listener can handle the authentication events at a time.
Follow-up 2
Can you give an example of when you would use a broadcast Stream?
A broadcast Stream is useful when you have a stream of events or data that needs to be consumed by multiple listeners simultaneously. For example, if you have a stream that represents real-time stock prices and you want to display the prices to multiple users in real-time, you would use a broadcast Stream to allow multiple listeners to receive the stock price updates at the same time.
Follow-up 3
How can you convert a single-subscription Stream into a broadcast Stream?
To convert a single-subscription Stream into a broadcast Stream, you can use the asBroadcastStream() method provided by the Stream class in Dart. This method returns a new broadcast Stream that can be listened to by multiple listeners. Here's an example:
Stream singleSubscriptionStream = ...;
Stream broadcastStream = singleSubscriptionStream.asBroadcastStream();
Live mock interview
Mock interview: Using Futures and Streams
- Read your scene and goals
- Talk it out; goals tick off live
- Get a score and stronger lines
Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.