Integrating Flutter with REST APIs
•7 min read
Connecting to REST APIs is essential for dynamic apps. This article covers the basics of making HTTP requests, parsing JSON, and handling errors in Flutter.
Setting Up Dependencies
Add the required packages to your pubspec.yaml
:
dependencies: http: ^0.13.4 dio: ^4.0.0
Basic HTTP Requests
Using the http Package
import 'package:http/http.dart' as http; Future<void> fetchData() async { final response = await http.get( Uri.parse('https://api.example.com/data'), headers: {'Authorization': 'Bearer your_token'}, ); if (response.statusCode == 200) { // Parse the response final data = jsonDecode(response.body); print(data); } else { throw Exception('Failed to load data'); } }
Using the dio Package
import 'package:dio/dio.dart'; final dio = Dio(); Future<void> fetchData() async { try { final response = await dio.get( 'https://api.example.com/data', options: Options( headers: {'Authorization': 'Bearer your_token'}, ), ); print(response.data); } catch (e) { print('Error: $e'); } }
JSON Serialization
Manual JSON Parsing
class User { final int id; final String name; final String email; User({ required this.id, required this.name, required this.email, }); factory User.fromJson(Map<String, dynamic> json) { return User( id: json['id'], name: json['name'], email: json['email'], ); } Map<String, dynamic> toJson() { return { 'id': id, 'name': name, 'email': email, }; } }
Using json_serializable
Add to pubspec.yaml
:
dependencies: json_annotation: ^4.0.0 dev_dependencies: build_runner: ^2.0.0 json_serializable: ^4.0.0
Create the 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, required this.email, }); factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); Map<String, dynamic> toJson() => _$UserToJson(this); }
Run the build runner:
flutter pub run build_runner build
Error Handling
Basic Error Handling
Future<User> fetchUser(int id) async { try { final response = await dio.get('/users/$id'); return User.fromJson(response.data); } on DioError catch (e) { if (e.response?.statusCode == 404) { throw UserNotFoundException(); } else if (e.type == DioErrorType.connectTimeout) { throw NetworkException(); } else { throw UnknownException(); } } }
Custom Exception Classes
class UserNotFoundException implements Exception { final String message = 'User not found'; } class NetworkException implements Exception { final String message = 'Network error occurred'; } class UnknownException implements Exception { final String message = 'An unknown error occurred'; }
API Service Layer
Create a service class to handle API calls:
class ApiService { final Dio _dio; ApiService() : _dio = Dio() { _dio.options.baseUrl = 'https://api.example.com'; _dio.interceptors.add(LogInterceptor()); } Future<List<User>> getUsers() async { try { final response = await _dio.get('/users'); return (response.data as List) .map((json) => User.fromJson(json)) .toList(); } catch (e) { throw _handleError(e); } } Future<User> createUser(User user) async { try { final response = await _dio.post( '/users', data: user.toJson(), ); return User.fromJson(response.data); } catch (e) { throw _handleError(e); } } Exception _handleError(dynamic error) { if (error is DioError) { switch (error.type) { case DioErrorType.connectTimeout: case DioErrorType.sendTimeout: case DioErrorType.receiveTimeout: return NetworkException(); case DioErrorType.response: if (error.response?.statusCode == 404) { return UserNotFoundException(); } return UnknownException(); default: return UnknownException(); } } return UnknownException(); } }
State Management with API Calls
Using Provider
class UserProvider extends ChangeNotifier { final ApiService _apiService; List<User> _users = []; bool _isLoading = false; String? _error; List<User> get users => _users; bool get isLoading => _isLoading; String? get error => _error; UserProvider(this._apiService); Future<void> fetchUsers() async { _isLoading = true; _error = null; notifyListeners(); try { _users = await _apiService.getUsers(); } catch (e) { _error = e.toString(); } finally { _isLoading = false; notifyListeners(); } } }
Testing API Integration
void main() { group('ApiService Tests', () { late ApiService apiService; late MockDio mockDio; setUp(() { mockDio = MockDio(); apiService = ApiService(mockDio); }); test('getUsers returns list of users', () async { // Arrange final mockUsers = [ {'id': 1, 'name': 'John', 'email': 'john@example.com'}, {'id': 2, 'name': 'Jane', 'email': 'jane@example.com'}, ]; when(mockDio.get('/users')).thenAnswer( (_) async => Response( data: mockUsers, statusCode: 200, requestOptions: RequestOptions(path: '/users'), ), ); // Act final users = await apiService.getUsers(); // Assert expect(users.length, 2); expect(users[0].name, 'John'); expect(users[1].name, 'Jane'); }); }); }
Best Practices
- Use a Service Layer: Separate API calls from UI logic
- Implement Error Handling: Handle all possible error cases
- Add Loading States: Show loading indicators during API calls
- Cache Responses: Implement caching for better performance
- Use Interceptors: Add logging, authentication, etc.
- Test Thoroughly: Write unit and integration tests
- Handle Offline Mode: Implement offline support
Conclusion
Integrating REST APIs in Flutter involves:
- Setting up HTTP clients
- Implementing JSON serialization
- Creating a service layer
- Handling errors and loading states
- Testing the integration
- Following best practices
By following these guidelines, you can create robust and maintainable API integrations in your Flutter applications.