flutter
/

Flutter Async/Await – Writing Clean Asynchronous Code

Last Sync: Today

On this page

9
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Flutter Async/Await – Writing Clean Asynchronous Code

What are Async and Await?

Asynchronous programming allows your app to perform time‑consuming tasks (like network requests, file I/O, or database queries) without freezing the user interface. In Flutter, you can use async and await to write asynchronous code that looks like synchronous code, making it easier to read and maintain. The async keyword marks a function as asynchronous, and await is used to wait for a Future to complete without blocking the main thread.

Basic Syntax

DARTRead-only
1
Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 2)); // simulate network delay
  return 'Data loaded';
}

void main() async {
  print('Start');
  String data = await fetchData();
  print(data); // Data loaded
  print('End');
}

The async function returns a Future automatically. Inside, you can use await to pause execution until the Future completes, but the thread is free to handle other events. When the Future completes, the function resumes with the result.

Using Async/Await in Flutter Widgets

In Flutter, you often need to perform async work in response to user actions or when the widget initializes. The typical places are:

    • initState: For loading initial data (e.g., fetch user profile).
    • onPressed callbacks: For actions like submitting a form or fetching more data.
    • FutureBuilder: To handle async state reactively.

Example: Loading Data in initState

DARTRead-only
1
class _MyHomePageState extends State<MyHomePage> {
  String _data = '';

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    String result = await fetchData();
    setState(() {
      _data = result;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(_data.isEmpty ? 'Loading...' : _data),
      ),
    );
  }
}

Note: initState cannot be marked async because it must return void. Instead, you call an async function and use setState to update the UI when the future completes.

Handling Errors

Use try/catch to handle errors in async functions. This is essential for user‑friendly error messages.

DARTRead-only
1
Future<void> _loadData() async {
  try {
    String result = await fetchData();
    setState(() => _data = result);
  } catch (e) {
    setState(() => _data = 'Error: $e');
  }
}

Async/Await with FutureBuilder

FutureBuilder is often a cleaner way to handle async data without manual state management. You provide a future and a builder that receives a snapshot. The builder is called when the future completes, with snapshot.hasData, snapshot.hasError, and snapshot.data available.

DARTRead-only
1
FutureBuilder<String>(
  future: fetchData(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    } else if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    } else if (snapshot.hasData) {
      return Text(snapshot.data!);
    } else {
      return Text('No data');
    }
  },
)

Common Pitfalls and Best Practices

    • Don't forget await: Calling an async function without await will start it but not wait for completion. This can lead to race conditions.
    • Avoid async in build methods: The build method is called often; starting an async operation inside it will restart on every rebuild. Use initState or FutureBuilder.
    • Check mounted before calling setState: After an async operation, the widget might have been disposed. Use if (mounted) to avoid memory leaks.
    • Cancel ongoing operations if needed: If the widget is disposed while an async operation is running, use a flag to ignore the result.

Complete Example: User Profile Loader

DARTRead-only
1
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Async/Await Demo')),
        body: Center(child: UserProfile()), 
      ),
    );
  }
}

class UserProfile extends StatefulWidget {
  @override
  _UserProfileState createState() => _UserProfileState();
}

class _UserProfileState extends State<UserProfile> {
  String _userName = '';
  bool _isLoading = false;

  Future<String> fetchUserName() async {
    await Future.delayed(Duration(seconds: 2)); // simulate network call
    // Simulate an error condition
    // throw Exception('Failed to load user');
    return 'John Doe';
  }

  Future<void> _loadUser() async {
    setState(() => _isLoading = true);
    try {
      String name = await fetchUserName();
      if (mounted) {
        setState(() => _userName = name);
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: $e')),
        );
      }
    } finally {
      if (mounted) {
        setState(() => _isLoading = false);
      }
    }
  }

  @override
  void initState() {
    super.initState();
    _loadUser();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (_isLoading)
          CircularProgressIndicator()
        else
          Text(
            _userName.isEmpty ? 'No user' : 'Hello, $_userName',
            style: TextStyle(fontSize: 24),
          ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: _loadUser,
          child: Text('Reload'),
        ),
      ],
    );
  }
}

Key Takeaways

    • async marks a function that returns a Future.
    • await pauses the function until the Future completes, without blocking the UI.
    • Use try/catch to handle errors in async code.
    • In Flutter, perform async work in initState, onPressed, or FutureBuilder.
    • Always check mounted before calling setState after an async operation.
    • Prefer FutureBuilder for reactive async UI.

Try it yourself

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Async/Await Demo')),
        body: Center(child: DataLoader()),
      ),
    );
  }
}

class DataLoader extends StatefulWidget {
  @override
  _DataLoaderState createState() => _DataLoaderState();
}

class _DataLoaderState extends State<DataLoader> {
  String _message = 'Press button to load data';
  bool _loading = false;

  Future<String> fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    return 'Data loaded successfully!';
  }

  Future<void> _onLoad() async {
    setState(() => _loading = true);
    try {
      String data = await fetchData();
      if (mounted) {
        setState(() => _message = data);
      }
    } catch (e) {
      if (mounted) {
        setState(() => _message = 'Error: $e');
      }
    } finally {
      if (mounted) {
        setState(() => _loading = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (_loading)
          CircularProgressIndicator()
        else
          Text(_message, style: TextStyle(fontSize: 18)),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: _loading ? null : _onLoad,
          child: Text('Load Data'),
        ),
      ],
    );
  }
}

Test Your Knowledge

Q1
of 4

What does the `await` keyword do in an async function?

A
It starts a new thread
B
It pauses the function until a Future completes, without blocking the UI
C
It cancels the Future
D
It converts a Future into a Stream
Q2
of 4

Which method should you avoid marking as `async`?

A
initState
B
onPressed
C
build
D
a custom async function
Q3
of 4

How do you handle errors in an async function?

A
Use try/catch
B
Use .catchError
C
Both A and B
D
Use onError
Q4
of 4

Why should you check `mounted` before calling `setState` after an async operation?

A
To avoid a memory leak
B
To prevent the widget from rebuilding unnecessarily
C
To prevent an error if the widget has been disposed
D
Both A and C

Frequently Asked Questions

Can I use `async` in the `build` method?

Technically you can, but it's discouraged. The build method can be called many times, and each call would start a new async operation, leading to performance issues and flickering. Use initState, FutureBuilder, or call async functions from user actions instead.

What happens if I call an async function without `await`?

The function runs, but you don't wait for its completion. This can lead to race conditions or missing results. It's usually a mistake unless you intentionally fire‑and‑forget (e.g., logging). Use unawaited from package:flutter/foundation.dart to ignore the future explicitly.

How do I cancel an async operation when the widget is disposed?

Futures themselves are not cancelable. You can use a boolean flag (like _isDisposed) to ignore the result when the widget is no longer mounted. Alternatively, use CancelableOperation from package:async for more advanced cancellation.

What's the difference between `FutureBuilder` and using `setState` after an async operation?

FutureBuilder automatically manages the subscription and rebuilds the UI based on the future's state. It also handles the loading and error states elegantly. Using setState manually gives you more control but requires you to manage state and lifecycle yourself. FutureBuilder is often simpler for one‑time loads.

Previous

flutter json parsing

Next

flutter future

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.