All Guides
    API & Data
    API
    HTTP
    Data
    Asynchronous

    Fetching API Data in Flutter

    Published April 2026

    Introduction

    Almost every modern application requires a connection to the internet. Whether you are loading a user profile, fetching a list of catalog items from a remote server, or updating user records, your application needs to talk to a REST API. In Flutter, this is accomplished by running asynchronous network calls.

    Because network operations are dependent on internet speed and server response times, they do not complete instantly. Flutter uses a declarative layout model where widgets are drawn on the screen continuously. If your layout blocking code waited synchronously for a network request to complete, the entire user interface would freeze.

    To prevent interface freezes, Flutter executes network calls asynchronously using Dart's Future type. Combined with the FutureBuilder widget, this allows you to build responsive layouts that seamlessly transition from loading states (like spinners) to display layouts or error handling banners when requests complete. In this guide, you will learn the mechanics of HTTP calls, typed JSON parsing, and how to build a clean API list view.


    Prerequisites

    Before fetching server data, ensure you have:

    • A solid grasp of Dart asynchronous programming (Future, async, and await).
    • Read our guide on Understanding Widgets in Flutter.
    • Added the http package to your project's pubspec.yaml dependencies:
      dependencies:
        http: ^1.2.0
      

    Core Explanation: HTTP Requests & JSON Deserialization

    Connecting to a server involves two primary steps: making an asynchronous request and parsing the returned JSON data.

    ┌──────────────────┐               Request (GET/POST)               ┌──────────────────┐
    │                  ├───────────────────────────────────────────────►│                  │
    │   Flutter App    │                                                │    REST API      │
    │                  │◄───────────────────────────────────────────────┤  (JSON Server)  │
    │                  │             Response (Status Code + JSON)      └──────────────────┘
    └──────────────────┘
    

    1. The HTTP Request Cycle

    When you execute an API call, your application sends an HTTP request (such as a GET request to retrieve data or a POST request to submit data) to a URL. The server processes the request and sends back a response containing:

    • Status Code: A numeric code indicating success (e.g., 200 OK) or failure (e.g., 404 Not Found, 500 Server Error).
    • Body: The payload data, typically formatted as a JSON string.

    2. JSON Deserialization (Data Modeling)

    When your app receives a JSON string, it is initially parsed into a raw Dart map: Map<String, dynamic>. Working directly with raw maps is error-prone because there is no autocomplete support or compile-time type safety.

    To prevent errors, we convert these raw maps into strongly-typed Dart Model classes. We do this by defining a fromJson factory constructor inside our class:

    class User {
      final int id;
      final String name;
    
      User({required this.id, required this.name});
    
      factory User.fromJson(Map<String, dynamic> json) {
        return User(
          id: json['id'] as int,
          name: json['name'] as String,
        );
      }
    }
    

    Practical Example: Fetching and Displaying User Data

    Let's build a clean, compiler-ready page that fetches a list of mock users from a REST API and displays them using a FutureBuilder widget.

    import 'dart:convert';
    import 'package:flutter/material.dart';
    import 'package:http/http.dart' as http;
    
    // ==========================================
    // DATA MODEL CLASS
    // ==========================================
    class ApiUser {
      final int id;
      final String name;
      final String email;
      final String company;
    
      ApiUser({
        required this.id,
        required this.name,
        required this.email,
        required this.company,
      });
    
      // Factory constructor to deserialize raw JSON maps into typed instances
      factory ApiUser.fromJson(Map<String, dynamic> json) {
        return ApiUser(
          id: json['id'] as int,
          name: json['name'] as String,
          email: json['email'] as String,
          company: json['company']['name'] as String,
        );
      }
    }
    
    // ==========================================
    // USER LIST CONTAINER WIDGET
    // ==========================================
    class UserListScreen extends StatefulWidget {
      const UserListScreen({super.key});
    
      @override
      State<UserListScreen> createState() => _UserListScreenState();
    }
    
    class _UserListScreenState extends State<UserListScreen> {
      // A Future representing our pending network operation
      late Future<List<ApiUser>> _usersFuture;
    
      @override
      void initState() {
        super.initState();
        // Initialize the future exactly once during state startup to prevent
        // redundant API calls when the widget rebuilds.
        _usersFuture = fetchUsers();
      }
    
      /// Asynchronously fetches a list of users from a public mockup REST API.
      Future<List<ApiUser>> fetchUsers() async {
        final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
    
        if (response.statusCode == 200) {
          // Decode the raw JSON string body into a Dart List
          final List<dynamic> rawList = json.decode(response.body);
          // Map the raw list items into our typed model classes
          return rawList.map((jsonItem) => ApiUser.fromJson(jsonItem)).toList();
        } else {
          // Throw an exception if the status code indicates failure
          throw Exception('Failed to load user list. Status Code: ${response.statusCode}');
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('API User Directory'),
            centerTitle: true,
          ),
          body: FutureBuilder<List<ApiUser>>(
            future: _usersFuture,
            builder: (context, snapshot) {
              // 1. Connection State: Loading
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(
                  child: CircularProgressIndicator(),
                );
              }
              
              // 2. Error State: Network or Parsing Exception
              if (snapshot.hasError) {
                return Center(
                  child: Padding(
                    padding: const EdgeInsets.all(24.0),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Icon(Icons.error_outline, color: Colors.red, size: 48),
                        const SizedBox(height: 16.0),
                        Text(
                          'An error occurred: ${snapshot.error}',
                          textAlign: TextAlign.center,
                          style: const TextStyle(fontSize: 16),
                        ),
                        const SizedBox(height: 16.0),
                        ElevatedButton(
                          onPressed: () {
                            setState(() {
                              _usersFuture = fetchUsers();
                            });
                          },
                          child: const Text('Try Again'),
                        ),
                      ],
                    ),
                  ),
                );
              }
    
              // 3. Success State: Display List View
              if (snapshot.hasData) {
                final users = snapshot.data!;
                return ListView.builder(
                  padding: const EdgeInsets.all(16.0),
                  itemCount: users.length,
                  itemBuilder: (context, index) {
                    final user = users[index];
                    return Card(
                      margin: const EdgeInsets.only(bottom: 12.0),
                      child: ListTile(
                        leading: CircleAvatar(
                          child: Text(user.name.substring(0, 1)),
                        ),
                        title: Text(
                          user.name,
                          style: const TextStyle(fontWeight: FontWeight.bold),
                        ),
                        subtitle: Text('${user.email}\nCompany: ${user.company}'),
                        isThreeLine: true,
                      ),
                    );
                  },
                );
              }
    
              // Fallback UI
              return const Center(child: Text('No users found.'));
            },
          ),
        );
      }
    }
    

    Code Explanation:

    1. initState Initialization: We initialize the future variable inside initState rather than declaring it inline in the FutureBuilder constructor. This prevents the widget from making a new network request every time the page rebuilds.
    2. json.decode: Parses the raw HTTP response body from a JSON string into a Dart map or list.
    3. FutureBuilder State Handling: We inspect the snapshot to render the correct UI for each network state: waiting (loading indicator), hasError (error message and retry button), or hasData (list of users).

    Common Mistakes

    1. Placing the Future Directly in the FutureBuilder

    A very common pitfall is initiating the API call directly within the FutureBuilder's future parameter.

    // BAD PRACTICE
    FutureBuilder(
      future: fetchUsers(), // Triggered again on EVERY single widget rebuild
      builder: (context, snapshot) { ... }
    )
    
    • The Fix: Initialize the future variable inside initState and reference it inside the FutureBuilder.

    2. Failing to Validate HTTP Status Codes

    Assuming every network response is successful without checking statusCode == 200 can lead to crashes if the server returns an error page.

    • The Fix: Always check the status code before parsing the response body.

    3. Missing try-catch Block Wrappers

    Making API calls without handling potential network exceptions (such as losing internet connection) can cause your app to crash.

    • The Fix: Wrap your network calls in try-catch blocks or handle errors gracefully inside your FutureBuilder.

    Best Practices

    • Implement Strongly-Typed Models: Never work directly with raw maps (Map<String, dynamic>). Always define typed Dart model classes with deserialization logic.
    • Isolate API Calls: Separate your network request functions from your UI page configurations. Use dedicated service classes or state managers (like Provider or Riverpod).
    • Use Caching: Avoid making redundant network requests. Cache static or slow-changing data in local memory or databases.

    Related Concepts

    Continue mastering API integrations in Flutter by exploring these guides:


    Summary

    • Network calls are asynchronous operations that return a Future.
    • Convert raw JSON strings into strongly-typed Dart models using fromJson factory constructors.
    • Use FutureBuilder to dynamically switch your UI between loading, error, and success states.
    • Avoid redundant API calls by initializing your future variables inside initState().

    Next Steps

    1. Previous Guide: Building Responsive Layouts
    2. Next Guide: Using Dio for HTTP Requests
    3. Related Guide: Local Storage with Hive

    Ready to take the next step?

    Start our free Flutter course and build real apps today.

    Start Learning Now