Lesson 15 of 51 intermediate

Async, Futures & Streams

Handling Things That Take Time in Dart

Open interactive version (quiz + challenge)

Real-world analogy

A Future is like ordering food at a restaurant. You place your order (start an async operation), get a receipt/buzzer (the Future object), and can do other things while the kitchen prepares your meal. When the food is ready, the buzzer goes off (the Future completes). A Stream is like a sushi conveyor belt -- items arrive one by one over time, and you pick up each piece as it passes by. In team_mvp_kit, Futures handle API calls via Dio and Streams power real-time updates through BLoC.

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

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. 1. Define AuthBloc class extending Bloc with AuthEvent and AuthState type parameters
  2. 2. Declare authRepository field -- this is injected via the constructor
  3. 3. Constructor takes required authRepository and calls super with AuthInitial state
  4. 4. Register event handler for LoginRequested events
  5. 5. Register event handler for LogoutRequested events
  6. 6. Register event handler for CheckAuthStatus events
  7. 7. The _onLoginRequested handler is async because it calls the repository
  8. 8. Emit AuthLoading state to show a spinner in the UI
  9. 9. Start try block for error handling
  10. 10. Await the login call to authRepository -- this is the async Dio API call
  11. 11. When login succeeds, emit AuthSuccess with the user data
  12. 12. Catch any exception from the login attempt
  13. 13. Emit AuthFailure with the error message for the UI to display
  14. 14. The _onLogoutRequested handler is also async
  15. 15. Await the logout call to clear the session on the server
  16. 16. Emit AuthInitial state to return to the login screen
  17. 17. The _onCheckAuthStatus handler checks if the user is already logged in
  18. 18. Await isLoggedIn to check stored credentials
  19. 19. If logged in, await getCurrentUser to fetch user data
  20. 20. Emit AuthSuccess with the cached user
  21. 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?
Two problems: one with the return type and one with how the function is called in main.
Show answer
Two bugs: (1) fetchData does not return a value when statusCode is not 200 -- add an else branch that throws an exception or returns a default. The function promises to return String but might return null. (2) In main, fetchData() returns a Future<String>, not a String. You need to either use await with an async main, or use .then(). Fix: async main() with await, and add else throw Exception('Failed to load').

Explain like I'm 5

Imagine you are at a pizza shop. You order a pizza (that is creating a Future). While it bakes, you sit down and play a game on your phone -- you do not stand at the counter staring at the oven. When the pizza is ready, they call your name (the Future completes) and you go pick it up. Now imagine a conveyor belt of small snacks coming out one by one -- that is a Stream. You grab each snack as it arrives. The await keyword is like saying 'I will wait here until my pizza is ready before I order dessert.'

Fun fact

Dart uses a single-threaded event loop, similar to JavaScript. This means there is no parallel execution on the main isolate -- async operations just schedule work and come back to it later. However, Dart also supports Isolates for true parallel computation. Flutter's engine spawns a separate isolate for heavy tasks like JSON parsing on large datasets, keeping the UI buttery smooth at 60fps.

Hands-on challenge

Create a function fetchWeather(String city) that returns Future> simulating an API call with Future.delayed. Create a Stream weatherAlerts() using async* and yield that emits 3 weather alerts with 1-second delays. Write a main function that: (1) calls fetchWeather and prints the result, (2) listens to weatherAlerts and prints each alert, and (3) demonstrates error handling with try-catch.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart