HTTP Requests in Flutter
HTTP requests are an essential part of modern Flutter applications. They allow an app to communicate with remote servers, REST APIs, databases, authentication services, payment systems, and other online services. In Flutter, the http package provides a simple and cross-platform way to make HTTP requests.
1. What is an HTTP Request?
An HTTP request is a message sent by a client application to a server to request or send information over the internet.
In a Flutter application, the Flutter app acts as the client and communicates with a backend server through HTTP requests.
Basic HTTP Communication
Flutter App
|
| HTTP Request
v
Backend / API Server
|
| HTTP Response
v
Flutter App
Example
Suppose a Flutter shopping application needs to display products. The application can send a GET request to an API:
GET https://example.com/api/products
The server may return JSON data containing the products.
2. Why Are HTTP Requests Used in Flutter?
- Fetching data from REST APIs
- Sending form data to a server
- User login and registration
- Fetching products, users, orders, and categories
- Updating existing records
- Uploading or downloading data
- Communicating with cloud services
- Building real-time and data-driven applications
3. HTTP Package in Flutter
The official Flutter documentation recommends the http package as a simple way to perform HTTP networking. It supports Android, iOS, macOS, Windows, Linux, and web applications.
Install the HTTP Package
flutter pub add http
Import the Package
import 'package:http/http.dart' as http;
The http package provides methods such as get(), post(), put(), patch(), and delete().
4. Android Internet Permission
Android applications that communicate over the internet should declare the Internet permission in the Android manifest.
This permission is normally placed inside the element of AndroidManifest.xml.
5. Understanding the HTTP Request Flow
- User performs an action in the Flutter application.
- Flutter creates an HTTP request.
- The request is sent to the API server.
- The server processes the request.
- The server returns an HTTP response.
- Flutter checks the response status.
- The response data is decoded, commonly from JSON.
- The application converts the data into Dart objects when appropriate.
- The UI displays the resulting data.
6. Common HTTP Methods
| Method |
Purpose |
Typical Use |
| GET |
Retrieve data |
Get products or users |
| POST |
Create or submit data |
Create an account or order |
| PUT |
Update or replace data |
Update a complete user record |
| PATCH |
Partially update data |
Update only a user's name |
| DELETE |
Delete data |
Delete a product or account |
7. Making a GET Request
A GET request is commonly used to retrieve information from an API.
Basic GET Example
import 'package:http/http.dart' as http;
Future fetchData() {
return http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
}
The http.get() method returns a Future because network communication is asynchronous.
8. Using async and await
Network requests should normally be handled asynchronously so the Flutter UI does not wait synchronously for the server response.
Future fetchData() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
print(response.statusCode);
print(response.body);
}
Understanding the Code
async allows a function to perform asynchronous operations.
await waits for the HTTP request to complete.
response.statusCode contains the HTTP status code.
response.body contains the response body.
9. Understanding http.Response
The response returned by an HTTP request contains useful information from the server.
| Property |
Description |
statusCode |
HTTP status code returned by the server |
body |
Response body as a string |
headers |
HTTP response headers |
request |
Information about the originating request when available |
10. HTTP Status Codes
| Status Code |
Meaning |
| 200 |
OK - Request completed successfully |
| 201 |
Created - New resource was created |
| 204 |
No Content - Request succeeded without response content |
| 400 |
Bad Request |
| 401 |
Unauthorized |
| 403 |
Forbidden |
| 404 |
Resource Not Found |
| 500 |
Internal Server Error |
| 503 |
Service Unavailable |
11. Checking the Status Code
Always check the response status before treating the response as successful.
Future fetchPost() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
if (response.statusCode == 200) {
print('Request successful');
print(response.body);
} else {
print('Request failed');
}
}
12. Reading JSON Response Data
Most REST APIs return JSON data. Dart provides the dart:convert library for encoding and decoding JSON.
import 'dart:convert';
import 'package:http/http.dart' as http;
Future fetchPost() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print(data['id']);
print(data['title']);
print(data['body']);
}
}
13. Creating a Dart Model from JSON
For larger applications, it is better to convert JSON responses into strongly typed Dart model classes instead of accessing raw maps throughout the UI.
class Post {
final int id;
final int userId;
final String title;
final String body;
const Post({
required this.id,
required this.userId,
required this.title,
required this.body,
});
factory Post.fromJson(Map json) {
return Post(
id: json['id'],
userId: json['userId'],
title: json['title'],
body: json['body'],
);
}
}
14. Converting an HTTP Response into a Model
import 'dart:convert';
import 'package:http/http.dart' as http;
Future fetchPost() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map;
return Post.fromJson(json);
}
throw Exception('Failed to load post');
}
15. Making a POST Request
A POST request is commonly used to create a new resource or submit data to a server.
import 'dart:convert';
import 'package:http/http.dart' as http;
Future createPost() async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode({
'title': 'Flutter HTTP',
'body': 'Learning HTTP requests in Flutter',
'userId': 1,
}),
);
if (response.statusCode == 201) {
print('Post created successfully');
print(response.body);
} else {
print('Failed to create post');
}
}
16. Understanding HTTP Headers
Headers provide additional information about an HTTP request or response.
Common Request Headers
Content-Type - Describes the format of the request body.
Accept - Indicates the response formats the client can accept.
Authorization - Sends authentication credentials or tokens.
Example
final response = await http.get(
Uri.parse('https://example.com/api/products'),
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN',
},
);
17. Sending JSON Data
When an API expects JSON, use jsonEncode() to convert a Dart object or map into a JSON string.
final requestData = {
'name': 'Rahul',
'email': '[email protected]',
'age': 25,
};
final response = await http.post(
Uri.parse('https://example.com/api/users'),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode(requestData),
);
18. PUT Request
PUT is commonly used to update an existing resource.
Future updatePost() async {
final response = await http.put(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode({
'id': 1,
'title': 'Updated Flutter Post',
'body': 'Updated post content',
'userId': 1,
}),
);
if (response.statusCode == 200) {
print('Post updated successfully');
} else {
print('Update failed');
}
}
19. PATCH Request
PATCH is generally used when only part of a resource needs to be updated.
Future updateUserName() async {
final response = await http.patch(
Uri.parse('https://example.com/api/users/10'),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'name': 'New Name',
}),
);
if (response.statusCode >= 200 && response.statusCode < 300) {
print('User updated');
}
}
20. DELETE Request
DELETE requests are used to remove resources from a server.
Future deletePost(int id) async {
final response = await http.delete(
Uri.parse('https://jsonplaceholder.typicode.com/posts/$id'),
);
if (response.statusCode == 200 || response.statusCode == 204) {
print('Post deleted successfully');
} else {
print('Failed to delete post');
}
}
21. Handling Exceptions
Network requests can fail because of connectivity problems, invalid URLs, server failures, timeouts, or other conditions. Use try-catch to handle errors.
Future fetchData() async {
try {
final response = await http.get(
Uri.parse('https://example.com/api/data'),
);
if (response.statusCode == 200) {
print(response.body);
} else {
throw Exception('Server returned ${response.statusCode}');
}
} catch (e) {
print('Request failed: $e');
}
}
22. Using Timeout with HTTP Requests
A timeout prevents the application from waiting indefinitely for a response.
Future fetchData() async {
try {
final response = await http
.get(Uri.parse('https://example.com/api/data'))
.timeout(const Duration(seconds: 10));
print(response.body);
} catch (e) {
print('Request failed or timed out: $e');
}
}
23. Displaying API Data with FutureBuilder
FutureBuilder is useful for building UI based on the result of an asynchronous operation such as an HTTP request.
FutureBuilder(
future: fetchPost(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.hasData) {
return Text(snapshot.data!.title);
}
return const Text('No data available');
},
)
24. Avoid HTTP Requests Directly Inside build()
Do not create a new HTTP request directly inside the build() method because Flutter may call build() many times, which can cause repeated network requests.
Incorrect Approach
@override
Widget build(BuildContext context) {
final future = fetchPost();
return FutureBuilder(
future: future,
builder: (context, snapshot) {
return const SizedBox();
},
);
}
Better Approach
late Future postFuture;
@override
void initState() {
super.initState();
postFuture = fetchPost();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: postFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return Text(snapshot.data!.title);
},
);
}
25. Displaying a List from an API
When an API returns a list, convert each JSON object into a Dart model and display the models using a builder widget.
Future> fetchPosts() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
if (response.statusCode != 200) {
throw Exception('Failed to load posts');
}
final List jsonList = jsonDecode(response.body);
return jsonList
.map((json) => Post.fromJson(json as Map))
.toList();
}
Displaying the List
FutureBuilder>(
future: fetchPosts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
final posts = snapshot.data ?? [];
if (posts.isEmpty) {
return const Center(
child: Text('No posts found'),
);
}
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text(post.title),
subtitle: Text(post.body),
);
},
);
},
)
26. Query Parameters
Query parameters are values added to the URL to filter or customize an API request.
Example URL
https://example.com/api/products?category=mobile&page=1
Using Uri
final uri = Uri.https(
'example.com',
'/api/products',
{
'category': 'mobile',
'page': '1',
},
);
final response = await http.get(uri);
27. Path Parameters
Path parameters are commonly used to identify a specific resource.
final userId = 25;
final uri = Uri.parse(
'https://example.com/api/users/$userId',
);
final response = await http.get(uri);
28. Authentication with HTTP Requests
Many APIs require authentication. A common approach is sending an authentication token through the Authorization header.
final response = await http.get(
Uri.parse('https://example.com/api/profile'),
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Accept': 'application/json',
},
);
Never hard-code real production secrets, private keys, or sensitive credentials directly into publicly distributed Flutter source code.
29. API Service Class
As an application grows, HTTP requests should be separated from UI widgets. A service class can contain API-related operations.
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiService {
static const String baseUrl = 'https://example.com/api';
Future> getPosts() async {
final response = await http.get(
Uri.parse('$baseUrl/posts'),
headers: {
'Accept': 'application/json',
},
);
if (response.statusCode != 200) {
throw Exception('Failed to load posts');
}
final List data = jsonDecode(response.body);
return data
.map((item) => Post.fromJson(item as Map))
.toList();
}
}
30. Separating Model, Service, and UI
A clean Flutter application can separate responsibilities into different layers.
UI
|
v
Controller / ViewModel
|
v
Repository / Service
|
v
HTTP Client
|
v
REST API
|
v
Server / Database
Responsibilities
- Model: Represents application data.
- Service: Handles communication with an external API.
- Repository: Provides a data-access abstraction and can coordinate sources such as network and cache.
- ViewModel/Controller: Manages UI-related state and actions.
- UI: Displays data and accepts user interaction.
31. Loading, Success, Empty, and Error States
A good API-based Flutter screen should handle more than just the successful response.
| State |
Example UI |
| Loading |
CircularProgressIndicator or skeleton UI |
| Success |
Display API data |
| Empty |
Display "No data found" |
| Error |
Display an understandable error message and retry option |
32. Retry API Requests
Providing a retry button is useful when a temporary network or server problem occurs.
ElevatedButton(
onPressed: () {
setState(() {
postFuture = fetchPost();
});
},
child: const Text('Retry'),
)
33. Pull-to-Refresh
Flutter applications can use RefreshIndicator to allow users to manually refresh API data.
RefreshIndicator(
onRefresh: () async {
setState(() {
postFuture = fetchPost();
});
await postFuture;
},
child: ListView(
children: const [
ListTile(
title: Text('Pull down to refresh'),
),
],
),
)
34. Using http.Client
For larger applications and testing, using an http.Client can make network-dependent functions easier to test and allows the HTTP client to be injected.
Future fetchPost(http.Client client) {
return client.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
}
35. Large JSON Responses
Parsing a very large JSON response can require significant CPU work. Flutter's networking documentation demonstrates moving expensive JSON parsing to another isolate with compute() when appropriate.
import 'package:flutter/foundation.dart';
List> parseJson(String responseBody) {
final data = jsonDecode(responseBody) as List;
return data
.map((item) => item as Map)
.toList();
}
final data = await compute(parseJson, response.body);
36. Common HTTP Request Errors
- DNS or connectivity failure
- Unexpected JSON structure
- Missing required request fields
- Expired authentication token
37. Better Error Handling
Future fetchPost() async {
try {
final response = await http
.get(
Uri.parse('https://example.com/api/posts/1'),
)
.timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return Post.fromJson(
data as Map,
);
}
if (response.statusCode == 404) {
throw Exception('Post not found');
}
if (response.statusCode == 401) {
throw Exception('Authentication required');
}
if (response.statusCode >= 500) {
throw Exception('Server error');
}
throw Exception('Request failed');
} catch (e) {
throw Exception('Unable to fetch post: $e');
}
}
38. Complete HTTP Request Example
The following example demonstrates a complete flow: making a GET request, checking the response, decoding JSON, converting it to a model, and displaying it in Flutter.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class Post {
final int id;
final String title;
final String body;
const Post({
required this.id,
required this.title,
required this.body,
});
factory Post.fromJson(Map json) {
return Post(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
);
}
}
Future fetchPost() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
headers: {
'Accept': 'application/json',
},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body)
as Map;
return Post.fromJson(data);
}
throw Exception('Failed to load post');
}
class PostScreen extends StatefulWidget {
const PostScreen({super.key});
@override
State createState() => _PostScreenState();
}
class _PostScreenState extends State {
late Future postFuture;
@override
void initState() {
super.initState();
postFuture = fetchPost();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('HTTP Request Example'),
),
body: FutureBuilder(
future: postFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
if (!snapshot.hasData) {
return const Center(
child: Text('No data found'),
);
}
final post = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
post.title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(post.body),
],
),
);
},
),
);
}
}
39. Practical API Request Flow
User taps button
↓
Flutter Widget
↓
Service / Repository
↓
HTTP Request
↓
API Server
↓
HTTP Response
↓
Check Status Code
↓
Decode JSON
↓
Dart Model
↓
Update State
↓
Flutter UI
40. Best Practices for HTTP Requests in Flutter
- Use the
http package for straightforward HTTP networking.
- Use
async and await for asynchronous requests.
- Always check HTTP status codes.
- Use model classes instead of passing raw JSON throughout the UI.
- Separate API logic from widgets.
- Avoid making HTTP requests directly inside
build().
- Handle loading, success, empty, and error states.
- Use timeouts for important requests.
- Provide retry functionality when appropriate.
- Use authentication headers when required by the API.
- Do not expose sensitive production secrets in client-side code.
- Use an
http.Client when dependency injection and testing are beneficial.
- Move expensive JSON parsing to an isolate when large responses require it.
- Keep API models and networking code organized in separate files.
41. Suggested Flutter Project Structure
lib/
├── main.dart
├── models/
│ └── post.dart
├── services/
│ └── api_service.dart
├── repositories/
│ └── post_repository.dart
├── screens/
│ └── post_screen.dart
├── widgets/
│ └── post_card.dart
└── viewmodels/
└── post_viewmodel.dart
42. Common Mistakes
- Calling an API repeatedly from
build().
- Ignoring the HTTP status code.
- Assuming every response contains valid JSON.
- Not handling network exceptions.
- Not displaying a loading state.
- Showing technical errors directly to end users.
- Hard-coding sensitive credentials.
- Putting all API logic inside a widget.
- Ignoring empty API responses.
- Parsing huge JSON responses on the main isolate when it causes UI jank.
43. HTTP Requests vs WebSockets
| HTTP |
WebSocket |
| Usually request-response communication |
Persistent two-way communication |
| Useful for REST APIs |
Useful for real-time communication |
| Common for CRUD operations |
Common for chat, live updates, and real-time events |
| Each request normally receives a response |
Server can send messages without a new HTTP request |
44. Practice Exercise
Create a Flutter application that retrieves posts from an API and displays them in a ListView.builder.
- Create a new Flutter project.
- Add the
http package.
- Create a
Post model.
- Create an API service.
- Send a GET request.
- Decode the JSON response.
- Convert JSON objects into
Post objects.
- Display a loading indicator.
- Display the posts after a successful response.
- Display an error message when the request fails.
- Add a retry button.
- Add pull-to-refresh functionality.
45. Interview Questions
- What is an HTTP request?
- How do you make an HTTP GET request in Flutter?
- What is the
http package?
- Why does
http.get() return a Future?
- What is the difference between GET and POST?
- What are HTTP status codes?
- How do you decode JSON in Dart?
- Why are model classes useful when working with APIs?
- How do you send JSON data using POST?
- What is the purpose of HTTP headers?
- How do you add an Authorization header?
- Why should API calls not normally be created directly inside
build()?
- How does
FutureBuilder work with HTTP requests?
- How do you handle API errors in Flutter?
- What is the purpose of a timeout?
- What is the difference between PUT and PATCH?
- How do you delete data from an API?
- Why should networking code be separated from the UI?
- When would you use an
http.Client?
- When should large JSON parsing be moved to a separate isolate?
46. Quick Revision
| Concept |
Key Point |
| HTTP |
Protocol used for client-server communication |
| GET |
Retrieve data |
| POST |
Create or submit data |
| PUT |
Update or replace a resource |
| PATCH |
Partially update a resource |
| DELETE |
Remove a resource |
| http |
Flutter/Dart package for HTTP networking |
| Future |
Represents a value available asynchronously |
| jsonDecode() |
Converts JSON text into Dart data |
| jsonEncode() |
Converts Dart data into JSON text |
| FutureBuilder |
Builds UI based on asynchronous state |
| Headers |
Provide metadata and authentication information |
| Status Code |
Indicates the result of an HTTP request |
47. Official Flutter Resources
48. Learn Flutter with JustAcademy
To learn Flutter development through structured training, explore the JustAcademy Flutter Training Course.
You can also register for a course demonstration through the JustAcademy Flutter Course Demo Registration page.
Conclusion
HTTP requests allow Flutter applications to communicate with backend servers and build data-driven experiences. By learning GET, POST, PUT, PATCH, and DELETE requests, JSON processing, authentication headers, status-code handling, error handling, asynchronous programming, and API architecture, developers can create Flutter applications that interact reliably with real-world APIs.
A clean implementation generally follows the flow: UI → Service/Repository → HTTP Request → API → HTTP Response → JSON → Model → State → UI.