Fetching data from APIs is essential for most Flutter apps. Here’s how to do it well.
Using the http package
The simplest approach for basic REST calls:
dependencies:
http: ^1.2.0import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List<User>> fetchUsers() async {
final response = await http.get(Uri.parse('https://api.example.com/users'));
if (response.statusCode == 200) {
final List data = json.decode(response.body);
return data.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load users: ${response.statusCode}');
}
}
// POST request
Future<User> createUser(String name) async {
final response = await http.post(
Uri.parse('https://api.example.com/users'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'name': name}),
);
if (response.statusCode == 201) {
return User.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to create user: ${response.statusCode}');
}
}Using Dio — advanced HTTP client
Dio supports interceptors, form data, request cancellation, and timeout configuration:
dependencies:
dio: ^5.4.0import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
headers: {'Accept': 'application/json'},
));
// GET
Future<List<User>> fetchUsers() async {
final response = await dio.get('/users');
return (response.data as List).map((json) => User.fromJson(json)).toList();
}
// POST
Future<User> createUser(String name) async {
final response = await dio.post('/users', data: {'name': name});
return User.fromJson(response.data);
}
// Interceptors — add auth tokens, logging
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer ${AuthToken.get()}';
return handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
// Refresh token and retry
}
return handler.next(error);
},
));JSON serialization with code generation
For production apps, use json_serializable instead of hand-written fromJson/toJson:
dependencies:
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.0
json_serializable: ^6.8.0Define your model:
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
final String? email;
User({required this.id, required this.name, this.email});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}Run the generator:
dart run build_runner build --delete-conflicting-outputsDisplaying network data
Use FutureBuilder to show data with loading and error states:
class UserList extends StatelessWidget {
final Future<List<User>> users;
const UserList({required this.users, super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder<List<User>>(
future: users,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No users found'));
}
final items = snapshot.data!;
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final user = items[index];
return ListTile(title: Text(user.name), subtitle: Text(user.email ?? ''));
},
);
},
);
}
}