Flutter Networking — HTTP Requests and REST API Integration
In this tutorial, you will learn about Flutter Networking. We cover key concepts, practical examples, and best practices to help you master this topic.
Flutter networking enables apps to communicate with remote servers using HTTP requests, with packages like http and Dio providing client APIs for RESTful services.
What Will You Learn
- Making HTTP requests with the http package
- Using Dio for advanced networking with interceptors
- JSON serialization and deserialization
- Handling network errors and timeouts
- Authentication tokens and headers
- Uploading files and multipart requests
- Caching responses locally
Why It Matters
Most mobile apps communicate with backend services. Flutter's networking ecosystem provides both simple (http package) and advanced (Dio) HTTP clients. Proper networking code handles loading states, errors, timeouts, and token refresh. JSON serialization with Code Generation tools eliminates manual mapping code. Understanding these patterns is essential for building apps that fetch and submit data reliably.
Real-World Use
The DodaTech Flutter app uses Dio with interceptors for all API calls. An auth interceptor attaches JWT tokens to requests. A retry interceptor retries failed requests with token refresh. A logging interceptor prints request details during development. JSON serialization uses json_serializable with build_runner for automatic code generation.
Learning Path
flowchart LR A[Flutter Forms] --> B[Flutter Networking\nYou are here] B --> C[Flutter Theming] style B fill:#f90,color:#fff
HTTP Package Basics
The http package provides simple HTTP client functionality:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class SimpleApiExample extends StatelessWidget {
const SimpleApiExample({super.key});
Future<void> fetchPosts() async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
try {
final response = await http.get(url);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
print('Fetched ${data.length} posts');
for (var post in data.take(3)) {
print('Post ${post['id']}: ${post['title']}');
}
} else {
print('Server error: ${response.statusCode}');
}
} catch (e) {
print('Network error: $e');
}
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: fetchPosts,
child: Text('Fetch Posts'),
);
}
}
The http package is simple and adequate for basic requests. Always check statusCode and wrap network calls in try-catch blocks.
Data Models with JSON Serialization
Use json_serializable for automatic JSON mapping:
// In pubspec.yaml:
// dependencies:
// json_annotation: ^4.8.0
// dev_dependencies:
// build_runner: ^2.4.0
// json_serializable: ^6.7.0
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
final String username;
final String email;
final Address? address;
User({
required this.id,
required this.name,
required this.username,
required this.email,
this.address,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
@JsonSerializable()
class Address {
final String street;
final String city;
final String zipcode;
Address({
required this.street,
required this.city,
required this.zipcode,
});
factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
}
// Usage:
// final user = User.fromJson(json);
// final jsonString = jsonEncode(user.toJson());
Run dart run build_runner build to generate the fromJson and toJson implementations. The @JsonSerializable() annotation enables code generation for the class.
Dio for Advanced Networking
Dio provides interceptors, request cancellation, and better error handling:
import 'package:dio/dio.dart';
class ApiService {
late final Dio _dio;
ApiService() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
// Logging interceptor
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));
// Auth interceptor
_dio.interceptors.add(AuthInterceptor());
// Retry interceptor
_dio.interceptors.add(RetryInterceptor(
dio: _dio,
retries: 3,
retryDelays: [
Duration(seconds: 1),
Duration(seconds: 2),
Duration(seconds: 3),
],
));
}
Future<List<User>> getUsers() async {
try {
final response = await _dio.get<List<dynamic>>('/users');
return response.data!.map((json) => User.fromJson(json as Map<String, dynamic>)).toList();
} on DioException catch (e) {
throw _handleError(e);
}
}
Future<User> createUser(User user) async {
try {
final response = await _dio.post<Map<String, dynamic>>(
'/users',
data: user.toJson(),
);
return User.fromJson(response.data!);
} on DioException catch (e) {
throw _handleError(e);
}
}
Exception _handleError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return Exception('Connection timed out. Please try again.');
case DioExceptionType.badResponse:
return Exception('Server error: ${e.response?.statusCode}');
case DioExceptionType.cancel:
return Exception('Request was cancelled');
default:
return Exception('Network error. Please check your connection.');
}
}
}
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// Add auth token
options.headers['Authorization'] = 'Bearer YOUR_TOKEN';
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 401) {
// Handle token refresh
print('Token expired. Refreshing...');
}
handler.next(err);
}
}
Dio's configuration options include base URL, timeouts, default headers, and interceptors. Interceptors can modify requests, responses, and handle errors globally.
Loading States with FutureBuilder
Use FutureBuilder for declarative loading states:
class UserListScreen extends StatelessWidget {
const UserListScreen({super.key});
Future<List<User>> _fetchUsers() {
return ApiService().getUsers();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: FutureBuilder<List<User>>(
future: _fetchUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16),
Text('Error: ${snapshot.error}'),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// Rebuild to retry
(context as Element).reassemble();
},
child: Text('Retry'),
),
],
),
);
}
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
leading: CircleAvatar(child: Text('${user.id}')),
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
},
),
);
}
}
FutureBuilder handles the connection state lifecycle: none, waiting, active (streams), and done. Always handle both loading and error states.
File Upload
Upload files using multipart requests:
import 'package:dio/dio.dart';
import 'package:http_parser/http_parser.dart';
class FileUploadService {
final Dio _dio;
FileUploadService(this._dio);
Future<String> uploadImage(String filePath) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
filePath,
filename: 'profile.jpg',
contentType: MediaType('image', 'jpeg'),
),
'description': 'User profile image',
});
try {
final response = await _dio.post<Map<String, dynamic>>(
'/upload',
data: formData,
onSendProgress: (sent, total) {
final progress = sent / total * 100;
print('Upload progress: ${progress.toStringAsFixed(1)}%');
},
);
return response.data!['url'] as String;
} on DioException catch (e) {
throw Exception('Upload failed: ${e.message}');
}
}
Future<String> uploadMultipleFiles(List<String> filePaths) async {
final files = filePaths.map((path) => MultipartFile.fromFileSync(
path,
contentType: MediaType('image', 'jpeg'),
)).toList();
final formData = FormData.fromMap({
'files': files,
});
final response = await _dio.post<Map<String, dynamic>>(
'/upload-multiple',
data: formData,
);
return response.data!['message'] as String;
}
}
FormData constructs multipart requests. MultipartFile.fromFile reads the file. onSendProgress provides upload progress for UI indicators.
Caching Responses
Cache network responses locally for offline support:
import 'dart:convert';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:dio/dio.dart';
class CachingService {
final Dio _dio;
final CacheManager _cacheManager;
CachingService(this._dio, this._cacheManager);
Future<String> fetchWithCache(String url, {Duration maxAge = Duration(hours: 1)}) async {
// Check cache first
final cacheFile = await _cacheManager.getFileFromCache(url);
if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) {
final cachedData = await cacheFile.file.readAsString();
print('Returning cached data for $url');
return cachedData;
}
// Fetch from network
try {
final response = await _dio.get<String>(url);
// Store in cache
await _cacheManager.putFile(
url,
utf8.encode(response.data!),
maxAge: maxAge,
key: url,
);
return response.data!;
} on DioException catch (e) {
// If network fails, return stale cache if available
if (cacheFile != null) {
print('Network failed, returning stale cache');
return await cacheFile.file.readAsString();
}
throw Exception('Failed to fetch data: ${e.message}');
}
}
}
Cache-first Strategy improves perceived performance. Stale cache fallback enables basic offline functionality.
Websocket with Flutter
Flutter supports WebSocket for real-time communication:
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
class WebSocketService {
WebSocketChannel? _channel;
void connect(String url) {
_channel = WebSocketChannel.connect(Uri.parse(url));
_channel!.stream.listen(
(message) {
final data = jsonDecode(message as String);
print('Received: $data');
// Update UI with received data
},
onError: (error) {
print('WebSocket error: $error');
// Reconnect logic
},
onDone: () {
print('WebSocket connection closed');
// Reconnect logic
},
);
}
void sendMessage(Map<String, dynamic> message) {
if (_channel != null) {
_channel!.sink.add(jsonEncode(message));
}
}
void disconnect() {
_channel?.sink.close();
}
}
WebSocket is ideal for chat, real-time notifications, and live data feeds. Handle reconnection in the onDone and onError callbacks.
Common Mistakes
Not handling network errors gracefully: Network calls can fail for many reasons. Always show user-friendly error messages and provide retry options.
Leaving streams open: HTTP responses (especially streams) must be closed. Dio handles this automatically, but custom stream usage requires manual cleanup.
Not setting timeouts: Without timeouts, network requests can hang indefinitely. Always set connect and receive timeouts.
Hardcoding API URLs: Store base URLs in environment config files. Use
--dart-defineflags or.envfiles for different environments.Ignoring response caching: Every network call consumes data and slows the app. Cache responses with appropriate TTL for improved performance and offline support.
Practice Questions
- How does Dio's interceptor system differ from the http package's approach?
- What is the purpose of
json_serializableandbuild_runner? - How does FutureBuilder handle the different connection states?
- What is the advantage of multipart requests for file uploads?
- Challenge: Build a post creation screen with Dio. Include fields for title, body, and an image picker. Upload the image as multipart, submit the post data as JSON, show upload progress, and handle errors with retry.
Mini Project
Build a news reader app:
- Fetch articles from a public API (NewsAPI)
- Display articles in a ListView with title, description, and image
- Cache articles for offline reading
- Pull-to-refresh to reload articles
- Error handling with retry button
- Loading indicator during fetch
- Article detail screen with full content
- Search functionality using query parameters
FAQ
What is Next
Now that you understand networking, learn about theming in Flutter. Proceed to Flutter Theming for custom themes, colors, typography, and dark mode. Then explore Flutter Animations for motion and transitions.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro