HTTP Requests
Making network requests in Flutter using the http package — the foundation of API integration.
HTTP (Hypertext Transfer Protocol) is the foundation of data communication on the web. In Flutter, HTTP requests are how your app talks to servers and exchanges data with APIs (Application Programming Interfaces).
When you make an HTTP request, you're essentially asking a server to:
- GET — Retrieve data (e.g., fetch a list of users)
- POST — Send new data (e.g., create a new user account)
- PUT — Update existing data (e.g., edit a user profile)
- DELETE — Remove data (e.g., delete a user account)
✅ The Big Idea
HTTP requests are how your Flutter app connects to the internet and fetches or sends data . Without them, your app would be isolated and unable to display dynamic content, user data, or interact with web services.
HTTP defines several methods (verbs) that indicate the desired action to be performed on a resource. Here are the most common ones you'll use in Flutter:
The http package is the official and most popular way to make HTTP requests in Flutter and Dart. It provides a simple, composable, and Future-based API for working with HTTP resources.
📦 Why the http Package?
- Official – Maintained by the Dart team
- Multi-platform – Works on mobile, desktop, and web
- Simple API – Easy to use with top-level functions
- Composable – Can be extended with custom clients
- Testable – Supports mocking for unit tests
Before you can make HTTP requests, you need to add the
http
package to your project
and configure your app's permissions.
# Run this command in your terminal
flutter pub add http
import
'package:http/http.dart'
as
http;
<!-- AndroidManifest.xml -->
<uses-permission android:name=
"android.permission.INTERNET"
/>
<!-- macos/Runner/DebugProfile.entitlements -->
<key>com.apple.security.network.client</key>
<true/>
⚠️ Important
Always check your platform permissions! Android requires the Internet permission, and macOS requires the network client entitlement. Without these, your HTTP requests will fail.
The most common HTTP request is
GET
, which retrieves data from a server.
Here's how to make a GET request using the
http
package.
import
'package:http/http.dart'
as
http;
// Make a GET request
Future
<http.Response>
fetchAlbum
() {
return
http.get(
Uri.parse(
'https://jsonplaceholder.typicode.com/albums/1'
),
headers: {
'Accept'
:
'application/json'
},
);
}
// Using the response
void
main
() async {
final
response = await fetchAlbum();
print(
'Status: ${response.statusCode}'
);
print(
'Body: ${response.body}'
);
}
🔑 Key Points
-
Uri.parse()
– Converts a string URL to a
Uriobject -
headers
– Optional headers like
AcceptorAuthorization -
Future
– The request is asynchronous, so it returns a
Future - response.statusCode – 200 means success, 404 means not found
- response.body – The raw response data (usually JSON)
When you make an HTTP request, the server responds with a status code and a body . You need to handle different scenarios:
import
'package:http/http.dart'
as
http;
Future
<Map<String, dynamic>>
fetchData
() async {
final
response = await http.get(
Uri.parse(
'https://jsonplaceholder.typicode.com/albums/1'
),
headers: {
'Accept'
:
'application/json'
},
);
// Handle different status codes
if
(response.statusCode ==
200
) {
// Success – parse the JSON
return
jsonDecode(response.body)
as
Map<String, dynamic>;
}
else
if
(response.statusCode ==
401
) {
throw
Exception(
'Unauthorized: Please check your credentials'
);
}
else
if
(response.statusCode ==
404
) {
throw
Exception(
'Not Found: The requested resource does not exist'
);
}
else
if
(response.statusCode >=
500
) {
throw
Exception(
'Server Error: Please try again later'
);
}
else
{
throw
Exception(
'Failed with status code: ${response.statusCode}'
);
}
}
⚠️ Common Status Codes
- 200 – Success (OK)
- 201 – Created (POST successful)
- 400 – Bad Request (invalid data)
- 401 – Unauthorized (need to log in)
- 404 – Not Found (resource doesn't exist)
- 500 – Internal Server Error (server problem)
Here's a complete example that fetches an album from the JSONPlaceholder API and displays it in a Flutter app. This follows the official Flutter cookbook recipe.
import
'dart:convert'
;
import
'package:flutter/material.dart'
;
import
'package:http/http.dart'
as
http;
// 1. Define the data model
class
Album
{
final
int
userId;
final
int
id;
final
String title;
const
Album({
required
this
.userId,
required
this
.id,
required
this
.title,
});
// Factory constructor for JSON parsing
factory Album.fromJson(Map<String, dynamic> json) {
return
Album(
userId: json[
'userId'
]
as
int
,
id: json[
'id'
]
as
int
,
title: json[
'title'
]
as
String,
);
}
}
// 2. Fetch function
Future
<Album>
fetchAlbum
() async {
final
response = await http.get(
Uri.parse(
'https://jsonplaceholder.typicode.com/albums/1'
),
headers: {
'Accept'
:
'application/json'
},
);
if
(response.statusCode ==
200
) {
final
data = jsonDecode(response.body)
as
Map<String, dynamic>;
return
Album.fromJson(data);
}
else
{
throw
Exception(
'Failed to load album: ${response.statusCode}'
);
}
}
// 3. Flutter UI
void
main
() => runApp(
MyApp
());
class
MyApp
extends
StatefulWidget {
@override
State<MyApp>
createState
() => _MyAppState();
}
class
_MyAppState
extends
State<MyApp> {
late
Future<Album> futureAlbum;
@override
void
initState
() {
super
.initState();
futureAlbum = fetchAlbum();
// Fetch data once when the widget is created
}
@override
Widget
build
(BuildContext context) {
return
MaterialApp(
title:
'Fetch Data Example'
,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: Scaffold(
appBar: AppBar(title: Text(
'Fetch Data Example'
)),
body: Center(
child: FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
// Loading state
if
(snapshot.connectionState == ConnectionState.waiting) {
return
CircularProgressIndicator();
}
// Error state
if
(snapshot.hasError) {
return
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size:
64
, color: Colors.red),
SizedBox(height:
16
),
Text(
'${snapshot.error}'
),
SizedBox(height:
16
),
ElevatedButton(
onPressed: () {
setState(() {
futureAlbum = fetchAlbum();
// Retry
});
},
child: Text(
'Retry'
),
),
],
);
}
// Data state
return
Card(
margin: EdgeInsets.all(
16
),
child: Padding(
padding: EdgeInsets.all(
16
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
snapshot.data!.title,
style: TextStyle(fontSize:
20
, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
SizedBox(height:
8
),
Text(
'Album ID: ${snapshot.data!.id}'
),
Text(
'User ID: ${snapshot.data!.userId}'
),
],
),
),
);
},
),
),
),
);
}
}
flutter pub add http
to add the dependency.
Album
class with a
fromJson
factory constructor to parse JSON data.
fetchAlbum()
makes a GET request,
checks the status code, and parses the response into an
Album
object.
Future
in a state variable
and call it once when the widget is created.
FutureBuilder
to handle loading,
error, and data states automatically.
For better testability, it's recommended to use a
Client
instance rather than
the top-level functions like
http.get()
. This makes it easier to mock HTTP requests in your tests.
import
'package:http/http.dart'
as
http;
class
DataService
{
final
http.Client client;
DataService
({http.Client? client}) : client = client ?? http.Client();
Future
<String>
fetchData
() async {
final
response = await client.get(
Uri.parse(
'https://example.com/data.json'
),
);
return
response.body;
}
}
import
'package:http/testing.dart'
;
import
'package:test/test.dart'
;
void
main
() {
test(
'fetchData returns expected data'
, () async {
final
mockClient = MockClient((request) async {
return
http.Response(
'{"key": "value"}'
,
200
);
});
final
dataService = DataService(client: mockClient);
final
result = await dataService.fetchData();
expect(result,
'{"key": "value"}'
);
});
}
✅ Best Practice
Always use a Client instance for testability.
Accept a
Client
parameter
in your service classes and use it instead of the top-level functions. This allows you to inject
mock clients in your tests.
Always check the
statusCode
before parsing the response. A 404 or 500 error will
contain error information, not the data you expect.
Use
if (response.statusCode == 200)
before parsing, and throw exceptions for other status codes.
Android needs the Internet permission, and macOS needs the network client entitlement. Without these, your requests will fail silently.
Add
<uses-permission android:name="android.permission.INTERNET" />
to
AndroidManifest.xml
and the network client entitlement to your macOS files.
The
build()
method is called frequently. Making network requests inside it will
cause unnecessary API calls and slow down your app.
Call your fetch function in
initState()
and store the
Future
in a state variable.
🎯 Key Takeaway
HTTP requests are the
foundation of API integration
in Flutter. The
http
package provides a simple, powerful way to make GET, POST, PUT, and DELETE requests.
Always check status codes, handle errors, and use a Client for testability.