Async, Futures & Streams
Handling Things That Take Time in Dart
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Asynchronous programming lets your app do multiple things without freezing. A Future represents a single value that arrives later -- like an API response. A Stream represents a sequence of values that arrive over time -- like real-time updates. The async/await syntax makes asynchronous code look and behave like regular synchronous code, while Dart's event loop handles the scheduling behind the scenes. In Flutter, the UI runs on the main thread, so long operations must be async to prevent jank.
Real-world relevance
In team_mvp_kit, nearly everything is asynchronous. Dio API calls return Futures that carry response data or errors. BLoC event handlers are async functions that emit loading states, await repository calls, then emit success or failure states. Firebase authentication operations are Futures. Hive cache reads use Futures for the initial box opening. go_router navigation guards await authentication checks. Without async/await, the app would freeze on every network call, making it unusable.
Key points
- Futures: Values That Arrive Later — A Future represents a value of type T that will be available sometime in the future. It can complete with a value (success) or an error (failure). Every API call, file read, or database query returns a Future.
- async and await — Mark a function as async to use await inside it. The await keyword pauses execution until a Future completes, making asynchronous code read like synchronous code. The function still returns a Future to its caller.
- Error Handling with try-catch — Wrap await calls in try-catch to handle errors from Futures. This is cleaner than using .catchError() chains and keeps error handling close to the code that might fail.
- Future.then and Chaining — You can also handle Futures with .then() for a callback-based style. Chain multiple .then() calls for sequential operations. While async/await is preferred for readability, .then() is useful in some patterns.
- Running Futures in Parallel — Use Future.wait to run multiple Futures simultaneously and wait for all of them. This is much faster than awaiting each one sequentially when the operations are independent.
- Streams: Multiple Values Over Time — A Stream delivers multiple values over time, unlike a Future which delivers just one. Streams are perfect for real-time data like chat messages, sensor readings, or state changes in BLoC.
- Listening to Streams — Use listen() or await for to consume stream values. The await for loop is cleaner and handles cancellation automatically. The listen() method gives you more control with onError and onDone callbacks.
- StreamController — A StreamController lets you create your own streams and push values into them manually. This is the mechanism behind BLoC's state emission. Use StreamController.broadcast() when multiple listeners need the same stream.
- Stream Transformations — Streams support map, where, expand, and other transformations just like lists. These create new streams that process each event from the original stream.
- Async in team_mvp_kit — API calls through Dio return Futures. BLoC uses Streams to emit state changes. The presentation layer listens to BLoC streams with BlocBuilder. Hive database reads are synchronous but writes can be async.
Code example
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository authRepository;
AuthBloc({required this.authRepository}) : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
on<LogoutRequested>(_onLogoutRequested);
on<CheckAuthStatus>(_onCheckAuthStatus);
}
Future<void> _onLoginRequested(
LoginRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
try {
final user = await authRepository.login(
event.email,
event.password,
);
emit(AuthSuccess(user: user));
} catch (e) {
emit(AuthFailure(message: e.toString()));
}
}
Future<void> _onLogoutRequested(
LogoutRequested event,
Emitter<AuthState> emit,
) async {
await authRepository.logout();
emit(AuthInitial());
}
Future<void> _onCheckAuthStatus(
CheckAuthStatus event,
Emitter<AuthState> emit,
) async {
final isLoggedIn = await authRepository.isLoggedIn();
if (isLoggedIn) {
final user = await authRepository.getCurrentUser();
emit(AuthSuccess(user: user));
} else {
emit(AuthInitial());
}
}
}Line-by-line walkthrough
- 1. Define AuthBloc class extending Bloc with AuthEvent and AuthState type parameters
- 2. Declare authRepository field -- this is injected via the constructor
- 3. Constructor takes required authRepository and calls super with AuthInitial state
- 4. Register event handler for LoginRequested events
- 5. Register event handler for LogoutRequested events
- 6. Register event handler for CheckAuthStatus events
- 7. The _onLoginRequested handler is async because it calls the repository
- 8. Emit AuthLoading state to show a spinner in the UI
- 9. Start try block for error handling
- 10. Await the login call to authRepository -- this is the async Dio API call
- 11. When login succeeds, emit AuthSuccess with the user data
- 12. Catch any exception from the login attempt
- 13. Emit AuthFailure with the error message for the UI to display
- 14. The _onLogoutRequested handler is also async
- 15. Await the logout call to clear the session on the server
- 16. Emit AuthInitial state to return to the login screen
- 17. The _onCheckAuthStatus handler checks if the user is already logged in
- 18. Await isLoggedIn to check stored credentials
- 19. If logged in, await getCurrentUser to fetch user data
- 20. Emit AuthSuccess with the cached user
- 21. If not logged in, emit AuthInitial to show the login screen
Spot the bug
Future<String> fetchData() async {
final response = await http.get(Uri.parse('https://api.example.com/data'));
if (response.statusCode == 200) {
return response.body;
}
}
void main() {
final data = fetchData();
print(data);
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Dart Asynchronous Programming (dart.dev)
- Dart Streams (dart.dev)
- Futures and Error Handling (dart.dev)