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, andawait). - Read our guide on Understanding Widgets in Flutter.
- Added the
httppackage to your project'spubspec.yamldependencies: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:
initStateInitialization: We initialize the future variable insideinitStaterather than declaring it inline in theFutureBuilderconstructor. This prevents the widget from making a new network request every time the page rebuilds.json.decode: Parses the raw HTTP response body from a JSON string into a Dart map or list.FutureBuilderState Handling: We inspect thesnapshotto render the correct UI for each network state:waiting(loading indicator),hasError(error message and retry button), orhasData(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
initStateand reference it inside theFutureBuilder.
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-catchblocks or handle errors gracefully inside yourFutureBuilder.
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:
- Understanding Widgets in Flutter: Learn the difference between layouts and the rendering layer.
- Using Dio for HTTP Requests: Upgrade to an advanced HTTP client featuring interceptors and global configurations.
- Local Storage with Hive: Persist your fetched API data locally for offline usage.
Summary
- Network calls are asynchronous operations that return a
Future. - Convert raw JSON strings into strongly-typed Dart models using
fromJsonfactory constructors. - Use
FutureBuilderto dynamically switch your UI between loading, error, and success states. - Avoid redundant API calls by initializing your future variables inside
initState().
Next Steps
- Previous Guide: Building Responsive Layouts
- Next Guide: Using Dio for HTTP Requests
- 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