In modern mobile application development, nearly all apps need to interact with backend servers to fetch remote data and display it to users. Flutter provides various ways to handle network requests and data parsing. This lesson will detail how to make network requests, process response data, and parse JSON-formatted data in Flutter.
I. HTTP Library Selection: dio Installation and Basic Usage
While Flutter officially provides the http package for network requests, the dio library is widely used in practical development due to its more powerful features and cleaner API. dio is a robust Dart HTTP client that supports interceptors, FormData, request cancellation, timeout settings, and other advanced features.
1. Installing dio
Add the dependency to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
dio: ^5.9.0 # Use the latest version
Run flutter pub get to install the dependency.
2. Basic dio Usage
First, import the dio package:
import 'package:dio/dio.dart';
Create a dio instance:
// Create a default instance
Dio dio = Dio();
// Or configure an instance with BaseOptions
BaseOptions options = BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(milliseconds: 5000),
receiveTimeout: const Duration(milliseconds: 3000),
headers: {
'Content-Type': 'application/json',
},
);
Dio dio = Dio(options);
II. Making GET/POST Requests and Parameter Handling
1. Making GET Requests
GET requests are typically used to retrieve data from a server:
// Simple GET request
Future<void> fetchData() async {
try {
Response response = await dio.get('/users');
print('Response data: ${response.data}');
print('Status code: ${response.statusCode}');
} catch (e) {
print('Error: $e');
}
}
// GET request with query parameters
Future<void> fetchUserData() async {
try {
// Method 1: Add parameters directly in the URL
Response response1 = await dio.get('/users?userId=123&name=John');
// Method 2: Use queryParameters
Response response2 = await dio.get(
'/users',
queryParameters: {
'userId': 123,
'name': 'John',
},
);
print('Response data: ${response2.data}');
} catch (e) {
print('Error: $e');
}
}
2. Making POST Requests
POST requests are typically used to submit data to a server:
// Submit JSON data
Future<void> submitData() async {
try {
Response response = await dio.post(
'/users',
data: {
'name': 'John Doe',
'email': 'john@example.com',
'age': 30,
},
);
print('Response data: ${response.data}');
} catch (e) {
print('Error: $e');
}
}
// Submit FormData
Future<void> uploadForm() async {
try {
FormData formData = FormData.fromMap({
'name': 'John Doe',
'avatar': await MultipartFile.fromFile(
'/path/to/avatar.jpg',
filename: 'avatar.jpg',
),
'hobbies': [
'reading',
'sports',
],
});
Response response = await dio.post(
'/user/profile',
data: formData,
);
print('Response data: ${response.data}');
} catch (e) {
print('Error: $e');
}
}
3. Custom Request Headers
You can set custom headers for individual requests:
Future<void> fetchWithHeaders() async {
try {
Response response = await dio.get(
'/protected/data',
options: Options(
headers: {
'Authorization': 'Bearer your_token_here',
'Custom-Header': 'custom_value',
},
),
);
print('Response data: ${response.data}');
} catch (e) {
print('Error: $e');
}
}
4. Handling Request Timeouts
You can set timeout times for individual requests:
Future<void> fetchWithTimeout() async {
try {
Response response = await dio.get(
'/slow/endpoint',
options: Options(
sendTimeout: const Duration(seconds: 2),
receiveTimeout: const Duration(seconds: 5),
),
);
print('Response data: ${response.data}');
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout) {
print('Connection timeout');
} else if (e.type == DioExceptionType.receiveTimeout) {
print('Receive timeout');
} else {
print('Other error: $e');
}
}
}
III. JSON Data Parsing
Data returned from servers is usually in JSON format. Flutter provides several ways to parse JSON data.
1. Manual JSON Parsing
Dart has a built-in dart:convert library that can manually parse JSON data:
import 'dart:convert';
// Assume the server returns the following JSON data:
// {
// "id": 1,
// "name": "John Doe",
// "email": "john@example.com",
// "age": 30,
// "hobbies": ["reading", "sports"]
// }
// Create a model class
class User {
final int id;
final String name;
final String email;
final int age;
final List<String> hobbies;
User({
required this.id,
required this.name,
required this.email,
required this.age,
required this.hobbies,
});
// Create a User instance from a JSON map
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
age: json['age'],
hobbies: List<String>.from(json['hobbies']),
);
}
// Convert to a JSON map
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'age': age,
'hobbies': hobbies,
};
}
}
// Parse JSON data
Future<void> parseUserJson() async {
try {
Response response = await dio.get('/users/1');
// Convert JSON string to Map
Map<String, dynamic> jsonData = response.data;
// Convert to User object
User user = User.fromJson(jsonData);
print('User name: ${user.name}');
print('User email: ${user.email}');
} catch (e) {
print('Error: $e');
}
}
// Parse JSON array
Future<void> parseUsersJson() async {
try {
Response response = await dio.get('/users');
// Convert JSON array to List
List<dynamic> jsonList = response.data;
// Convert to List of User objects
List<User> users = jsonList.map((json) => User.fromJson(json)).toList();
print('Number of users: ${users.length}');
print('First user: ${users[0].name}');
} catch (e) {
print('Error: $e');
}
}
Manual parsing is simple and straightforward without additional dependencies, but it becomes tedious and error-prone when dealing with complex JSON structures or many fields.
2. Using json_serializable for Automatic Code Generation
json_serializable is an automated source code generator that generates code for JSON serialization and deserialization, reducing the work of writing parsing code manually.
Install Dependencies
Add dependencies to pubspec.yaml:
dependencies:
# ... other dependencies
json_annotation: ^4.8.1 # Annotation package
dev_dependencies:
# ... other dev dependencies
build_runner: ^2.4.4 # Build tool
json_serializable: ^6.7.1 # Code generator
Run flutter pub get to install dependencies.
Create Model Classes
import 'package:json_annotation/json_annotation.dart';
// Generated code will be in user.g.dart
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
// Use @JsonKey annotation when JSON field name differs from class property
@JsonKey(name: 'email_address')
final String email;
final int age;
// Ignore this field in serialization/deserialization
@JsonKey(ignore: true)
final String? token;
final List<String> hobbies;
User({
required this.id,
required this.name,
required this.email,
required this.age,
this.token,
required this.hobbies,
});
// Create a User instance from a JSON map
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// Convert to a JSON map
Map<String, dynamic> toJson() => _$UserToJson(this);
}
Generate Code
Run the following command in the project root directory to generate serialization code:
flutter pub run build_runner build
For automatic code generation during development (when model classes change), use watch mode:
flutter pub run build_runner watch
After successful execution, a user.g.dart file will be generated containing automatically generated serialization and deserialization code.
Using Generated Code
Future<void> useGeneratedCode() async {
try {
Response response = await dio.get('/users/1');
// Parse using the automatically generated fromJson method
User user = User.fromJson(response.data);
print('User name: ${user.name}');
print('User email: ${user.email}');
// Serialization example
Map<String, dynamic> userJson = user.toJson();
print('Serialized user: $userJson');
} catch (e) {
print('Error: $e');
}
}
The advantage of json_serializable is that it reduces the work of writing parsing code manually, improving code reliability and maintainability, especially for complex JSON structures.
IV. Network State Handling
In practical applications, network requests typically have several states: loading, success, and error. We need to display different UIs based on these states.
1. Create Network State Management Class
enum NetworkStatus { initial, loading, success, error }
class NetworkResult<T> {
final NetworkStatus status;
final T? data;
final String? errorMessage;
NetworkResult.initial()
: status = NetworkStatus.initial,
data = null,
errorMessage = null;
NetworkResult.loading()
: status = NetworkStatus.loading,
data = null,
errorMessage = null;
NetworkResult.success(this.data)
: status = NetworkStatus.success,
errorMessage = null;
NetworkResult.error(this.errorMessage)
: status = NetworkStatus.error,
data = null;
}
2. Display Different UIs Based on State
class DataScreen extends StatefulWidget {
const DataScreen({super.key});
@override
State<DataScreen> createState() => _DataScreenState();
}
class _DataScreenState extends State<DataScreen> {
final Dio _dio = Dio();
NetworkResult<List<User>> _result = NetworkResult.initial();
@override
void initState() {
super.initState();
fetchUsers();
}
Future<void> fetchUsers() async {
setState(() {
_result = NetworkResult.loading();
});
try {
Response response = await _dio.get('https://api.example.com/users');
List<User> users = (response.data as List)
.map((json) => User.fromJson(json))
.toList();
setState(() {
_result = NetworkResult.success(users);
});
} catch (e) {
setState(() {
_result = NetworkResult.error(e.toString());
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('User List')),
body: _buildBody(),
);
}
Widget _buildBody() {
switch (_result.status) {
case NetworkStatus.initial:
return const Center(child: Text('Tap to load data'));
case NetworkStatus.loading:
return const Center(child: CircularProgressIndicator());
case NetworkStatus.success:
return _buildUserList(_result.data!);
case NetworkStatus.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${_result.errorMessage}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: fetchUsers,
child: const Text('Retry'),
),
],
),
);
}
}
Widget _buildUserList(List<User> users) {
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
User user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
trailing: Text('Age: ${user.age}'),
);
},
);
}
}
V. dio Interceptors
dio provides interceptor functionality that can handle requests before they're sent or responses after they're received, such as adding authentication tokens or handling errors.
1. Request Interceptor
// Add request interceptor
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// Do something before request is sent
print('Request: ${options.method} ${options.uri}');
// Add authentication token
options.headers['Authorization'] = 'Bearer your_token_here';
// Continue with the request
return handler.next(options);
// If you want to cancel the request, call handler.reject()
// return handler.reject(DioException(requestOptions: options, type: DioExceptionType.cancel));
},
),
);
2. Response Interceptor
// Add response interceptor
dio.interceptors.add(
InterceptorsWrapper(
onResponse: (response, handler) {
// Do something with response data
print('Response: ${response.statusCode} ${response.data}');
// Continue with the response
return handler.next(response);
},
),
);
3. Error Interceptor
// Add error interceptor
dio.interceptors.add(
InterceptorsWrapper(
onError: (DioException e, handler) {
// Handle error
print('Error: ${e.message}');
// Unified handling of 401 unauthorized errors
if (e.response?.statusCode == 401) {
// You can navigate to login page here
print('Unauthorized, redirecting to login');
}
// Continue with the error
return handler.next(e);
// If you want to mask the error, return a successful response
// return handler.resolve(Response(requestOptions: e.requestOptions, data: {}));
},
),
);
4. Log Interceptor
dio provides a built-in log interceptor for debugging:
import 'package:dio/io.dart';
dio.interceptors.add(LogInterceptor(
request: true, // Print request information
requestHeader: true, // Print request headers
requestBody: true, // Print request body
responseHeader: true, // Print response headers
responseBody: true, // Print response body
error: true, // Print error information
logPrint: (object) {
print('Dio Log: $object');
},
));
VI. Example: Fetch and Display News List with Open API
We'll implement a complete example that fetches and displays a news list using a public news API.
1. Create News Model Classes
import 'package:json_annotation/json_annotation.dart';
part 'news.g.dart';
@JsonSerializable()
class NewsArticle {
@JsonKey(name: 'source')
final NewsSource source;
@JsonKey(name: 'author')
final String? author;
@JsonKey(name: 'title')
final String title;
@JsonKey(name: 'description')
final String? description;
@JsonKey(name: 'url')
final String url;
@JsonKey(name: 'urlToImage')
final String? urlToImage;
@JsonKey(name: 'publishedAt')
final String publishedAt;
@JsonKey(name: 'content')
final String? content;
NewsArticle({
required this.source,
this.author,
required this.title,
this.description,
required this.url,
this.urlToImage,
required this.publishedAt,
this.content,
});
factory NewsArticle.fromJson(Map<String, dynamic> json) =>
_$NewsArticleFromJson(json);
Map<String, dynamic> toJson() => _$NewsArticleToJson(this);
}
@JsonSerializable()
class NewsSource {
@JsonKey(name: 'id')
final String? id;
@JsonKey(name: 'name')
final String name;
NewsSource({
this.id,
required this.name,
});
factory NewsSource.fromJson(Map<String, dynamic> json) =>
_$NewsSourceFromJson(json);
Map<String, dynamic> toJson() => _$NewsSourceToJson(this);
}
@JsonSerializable()
class NewsResponse {
@JsonKey(name: 'status')
final String status;
@JsonKey(name: 'totalResults')
final int totalResults;
@JsonKey(name: 'articles')
final List<NewsArticle> articles;
NewsResponse({
required this.status,
required this.totalResults,
required this.articles,
});
factory NewsResponse.fromJson(Map<String, dynamic> json) =>
_$NewsResponseFromJson(json);
Map<String, dynamic> toJson() => _$NewsResponseToJson(this);
}
Run the code generation command:
flutter pub run build_runner build
2. Create News Service Class
import 'package:dio/dio.dart';
class NewsService {
final Dio _dio = Dio();
final String _apiKey = 'your_news_api_key'; // Replace with your API Key
final String _baseUrl = 'https://newsapi.org/v2';
NewsService() {
// Configure dio
_dio.options.baseUrl = _baseUrl;
_dio.options.connectTimeout = const Duration(seconds: 5);
_dio.options.receiveTimeout = const Duration(seconds: 3);
// Add log interceptor
_dio.interceptors.add(LogInterceptor(responseBody: true));
}
// Get top headlines
Future<NewsResponse> getTopHeadlines({String country = 'us'}) async {
try {
Response response = await _dio.get(
'/top-headlines',
queryParameters: {
'country': country,
'apiKey': _apiKey,
},
);
return NewsResponse.fromJson(response.data);
} on DioException catch (e) {
print('News API error: ${e.message}');
throw Exception('Failed to fetch news: ${e.message}');
}
}
// Search news
Future<NewsResponse> searchNews(String query) async {
try {
Response response = await _dio.get(
'/everything',
queryParameters: {
'q': query,
'apiKey': _apiKey,
},
);
return NewsResponse.fromJson(response.data);
} on DioException catch (e) {
print('News API error: ${e.message}');
throw Exception('Failed to search news: ${e.message}');
}
}
}
Note: You need to register at News API to get an API Key.
3. Implement News List Page
class NewsListScreen extends StatefulWidget {
const NewsListScreen({super.key});
@override
State<NewsListScreen> createState() => _NewsListScreenState();
}
class _NewsListScreenState extends State<NewsListScreen> {
final NewsService _newsService = NewsService();
NetworkResult<List<NewsArticle>> _newsResult = NetworkResult.initial();
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_fetchTopHeadlines();
}
Future<void> _fetchTopHeadlines() async {
setState(() {
_newsResult = NetworkResult.loading();
});
try {
NewsResponse response = await _newsService.getTopHeadlines();
setState(() {
_newsResult = NetworkResult.success(response.articles);
});
} catch (e) {
setState(() {
_newsResult = NetworkResult.error(e.toString());
});
}
}
Future<void> _searchNews() async {
String query = _searchController.text.trim();
if (query.isEmpty) return;
setState(() {
_newsResult = NetworkResult.loading();
});
try {
NewsResponse response = await _newsService.searchNews(query);
setState(() {
_newsResult = NetworkResult.success(response.articles);
});
} catch (e) {
setState(() {
_newsResult = NetworkResult.error(e.toString());
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Latest News'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search news...',
suffixIcon: IconButton(
icon: const Icon(Icons.search),
onPressed: _searchNews,
),
border: const OutlineInputBorder(),
),
onSubmitted: (value) => _searchNews(),
),
),
Expanded(
child: _buildNewsContent(),
),
],
),
);
}
Widget _buildNewsContent() {
switch (_newsResult.status) {
case NetworkStatus.initial:
return const Center(child: Text('Loading news...'));
case NetworkStatus.loading:
return const Center(child: CircularProgressIndicator());
case NetworkStatus.success:
return _buildNewsList(_newsResult.data!);
case NetworkStatus.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${_newsResult.errorMessage}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _fetchTopHeadlines,
child: const Text('Retry'),
),
],
),
);
}
}
Widget _buildNewsList(List<NewsArticle> articles) {
if (articles.isEmpty) {
return const Center(child: Text('No news found'));
}
return ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) {
NewsArticle article = articles[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Column(
children: [
if (article.urlToImage != null)
Image.network(
article.urlToImage!,
height: 180,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 180,
color: Colors.grey[200],
child: const Center(child: Icon(Icons.image_not_supported)),
);
},
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.source.name,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
const SizedBox(height: 8),
Text(
article.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
if (article.description != null)
Text(
article.description!,
style: const TextStyle(fontSize: 14),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
_formatDate(article.publishedAt),
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
],
),
);
},
);
}
String _formatDate(String dateString) {
DateTime date = DateTime.parse(dateString);
return DateFormat.yMMMd().add_jm().format(date);
}
}
Note: You need to add the intl dependency for date formatting. Add intl: ^0.18.1 to pubspec.yaml and run flutter pub get.
4. Configure Network Permissions
For Android, add network permissions in android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
For iOS, add in ios/Runner/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
VII. Network Request Best Practices
- Encapsulate network layer: Encapsulate network request logic in dedicated service classes, separating it from the UI layer to improve code reusability and maintainability.
- Unified error handling: Use interceptors to handle network errors uniformly, such as timeouts, no network, and authentication failures.
- Manage request states properly: Clearly display loading, success, error, and other states to provide a good user experience.
- Data caching: Implement local caching for infrequently changing data to reduce network requests and improve app performance.
- Request cancellation: Cancel unfinished network requests when a page is destroyed to avoid memory leaks and unnecessary resource consumption.
- Image handling: Use libraries like cached_network_image to handle network images, implementing caching and placeholder functionality.
- Avoid complex tasks on UI thread: Ensure network requests execute on asynchronous threads to avoid blocking the UI.
- Add logging: Add detailed network logs in the development environment for debugging; disable or simplify logs in the production environment.
Top comments (0)