bosc_flutter_network_client (1.0.4)
Published 2026-02-27 07:01:29 +00:00 by mansi.kansara
Installation
dart pub add bosc_flutter_network_client:1.0.4 --hosted-url=About this package
Production-ready HTTP client for Flutter with comprehensive error handling, authentication, and advanced features.
Flutter Network Client
A production-ready, enterprise-grade HTTP client for Flutter applications built on top of Dio. This package provides a robust, reusable networking layer following clean architecture principles with comprehensive error handling, authentication support, and advanced features.
What changed (By Dhruv)
- Fixed retry loop (no extra retry) and added exponential backoff + jitter
- Token refresh retry now reuses the configured Dio (no “new Dio()” bypass)
- Added
baseUrlvalidation + normalization (requireshttp(s)://, trims trailing/) - Improved certificate pinning logic (removed dev bypass, fail-secure on validation errors)
- Added basic token validation in
AuthInterceptor - Removed extra generated docs (kept only this
README.md)
🌟 Features
- ✅ Clean Architecture: Separated concerns with clear boundaries
- ✅ Type-Safe: Strongly typed responses using generics
- ✅ Null Safety: Full null safety compliance
- ✅ Authentication: JWT/Bearer token support with automatic refresh
- ✅ Error Handling: Comprehensive error mapping with custom exceptions
- ✅ Request Retry: Configurable retry logic with exponential backoff
- ✅ Network Detection: Automatic offline detection
- ✅ File Operations: Upload/download with progress tracking
- ✅ Logging: Pretty formatted logs for development
- ✅ Certificate Pinning: Optional SSL certificate pinning
- ✅ Interceptors: Extensible interceptor system
- ✅ Testing: Fully testable with mock support
📦 Installation
Add to your pubspec.yaml:
dependencies:
dio: ^5.4.0
connectivity_plus: ^5.0.2
dev_dependencies:
mockito: ^5.4.4
build_runner: ^2.4.6
🚀 Quick Start
Basic Usage
import 'package:your_app/core/network/network_module.dart';
// Create a simple client
final networkClient = NetworkModule.createSimpleClient(
baseUrl: 'https://api.example.com',
enableLogging: true,
);
// Make a GET request
final user = await networkClient.get<Map<String, dynamic>>(
'/users/1',
fromJson: (json) => json,
);
With Authentication
final networkClient = NetworkModule.createAuthClient(
baseUrl: 'https://api.example.com',
enableLogging: true,
getToken: () async {
// Get token from secure storage
return await secureStorage.read(key: 'auth_token');
},
onTokenExpired: () async {
// Handle token expiration (e.g., navigate to login)
print('Token expired, please login again');
},
);
With Token Refresh
final networkClient = NetworkModule.createFullClient(
baseUrl: 'https://api.example.com',
enableLogging: true,
getToken: () async => await secureStorage.read(key: 'access_token'),
saveToken: (token) async => await secureStorage.write(key: 'access_token', value: token),
refreshToken: () async {
// Call refresh token endpoint
final response = await dio.post('/auth/refresh', data: {
'refresh_token': await secureStorage.read(key: 'refresh_token'),
});
return response.data['access_token'];
},
onRefreshFailed: () async {
// Clear auth state and navigate to login
await secureStorage.deleteAll();
navigatorKey.currentState?.pushReplacementNamed('/login');
},
);
🏗️ Architecture
Repository Pattern
class UserRepository {
final NetworkClient _networkClient;
UserRepository({required NetworkClient networkClient})
: _networkClient = networkClient;
Future<UserModel> getUser(int userId) async {
try {
final response = await _networkClient.get<Map<String, dynamic>>(
'/users/$userId',
fromJson: (json) => json,
);
return UserModel.fromJson(response);
} on NetworkException {
rethrow; // Let UI handle
}
}
Future<List<UserModel>> getUsers({int page = 1, int limit = 20}) async {
final response = await _networkClient.get<Map<String, dynamic>>(
'/users',
queryParameters: {'page': page, 'limit': limit},
fromJson: (json) => json,
);
final usersJson = response['data'] as List;
return usersJson
.map((json) => UserModel.fromJson(json as Map<String, dynamic>))
.toList();
}
Future<UserModel> createUser({
required String name,
required String email,
}) async {
final response = await _networkClient.post<Map<String, dynamic>>(
'/users',
data: {'name': name, 'email': email},
fromJson: (json) => json,
);
return UserModel.fromJson(response);
}
}
🎯 Usage Examples
GET Request with Query Parameters
final users = await networkClient.get<Map<String, dynamic>>(
'/users',
queryParameters: {
'page': 1,
'limit': 20,
'role': 'admin',
},
fromJson: (json) => json,
);
POST Request
final newUser = await networkClient.post<Map<String, dynamic>>(
'/users',
data: {
'name': 'John Doe',
'email': 'john@example.com',
},
fromJson: (json) => json,
);
File Upload with Progress
final result = await networkClient.uploadFiles<Map<String, dynamic>>(
'/users/123/avatar',
files: {'avatar': '/path/to/image.jpg'},
fields: {'description': 'Profile picture'},
fromJson: (json) => json,
onProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(0);
print('Upload progress: $progress%');
},
);
File Download with Progress
await networkClient.downloadFile(
'/reports/monthly.pdf',
'/local/path/report.pdf',
onProgress: (received, total) {
final progress = (received / total * 100).toStringAsFixed(0);
print('Download progress: $progress%');
},
);
Form Data
final result = await networkClient.postForm<Map<String, dynamic>>(
'/auth/login',
data: {
'username': 'user@example.com',
'password': 'secure_password',
},
fromJson: (json) => json,
);
⚠️ Error Handling
The client maps all errors to specific exception types:
try {
final user = await userRepository.getUser(123);
} on NoInternetException catch (e) {
showError('No internet connection');
} on TimeoutException catch (e) {
showError('Request timeout - please try again');
} on UnauthorizedException catch (e) {
navigateToLogin();
} on NotFoundException catch (e) {
showError('User not found');
} on UnprocessableEntityException catch (e) {
// Handle validation errors
if (e.validationErrors != null) {
e.validationErrors!.forEach((field, errors) {
print('$field: ${errors.join(", ")}');
});
}
} on ServerException catch (e) {
showError('Server error - please try again later');
} on NetworkException catch (e) {
showError(e.message);
} catch (e) {
showError('An unexpected error occurred');
}
Available Exception Types
NoInternetException- No network connectionTimeoutException- Request timeoutCancelledException- Request was cancelledBadRequestException- 400 Bad RequestUnauthorizedException- 401 UnauthorizedForbiddenException- 403 ForbiddenNotFoundException- 404 Not FoundConflictException- 409 ConflictUnprocessableEntityException- 422 Validation ErrorTooManyRequestsException- 429 Rate LimitedInternalServerException- 500 Internal Server ErrorServiceUnavailableException- 503 Service UnavailableSerializationException- JSON parsing errorUnknownException- Unexpected error
🔧 Configuration
Development Configuration
final config = NetworkConfig.development(
baseUrl: 'https://dev-api.example.com',
headers: {'X-App-Version': '1.0.0'},
);
Production Configuration
final config = NetworkConfig.production(
baseUrl: 'https://api.example.com',
headers: {'X-App-Version': '1.0.0'},
certificateHashes: [
'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
],
);
Custom Configuration
final config = NetworkConfig(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 60),
receiveTimeout: Duration(seconds: 60),
enableLogging: false,
maxRetries: 3,
retryDelay: Duration(seconds: 2),
retryStatusCodes: [408, 429, 500, 502, 503, 504],
);
🧪 Testing
Unit Testing with Mocks
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@GenerateMocks([NetworkClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockNetworkClient mockNetworkClient;
late UserRepository userRepository;
setUp(() {
mockNetworkClient = MockNetworkClient();
userRepository = UserRepository(networkClient: mockNetworkClient);
});
test('should fetch user successfully', () async {
// Arrange
final userData = {'id': 1, 'name': 'Test User'};
when(mockNetworkClient.get<Map<String, dynamic>>(
any,
fromJson: anyNamed('fromJson'),
)).thenAnswer((_) async => userData);
// Act
final user = await userRepository.getUser(1);
// Assert
expect(user.id, equals(1));
expect(user.name, equals('Test User'));
});
}
📱 Integration with State Management
Using with BLoC
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository _userRepository;
UserBloc({required UserRepository userRepository})
: _userRepository = userRepository,
super(UserInitial()) {
on<FetchUser>(_onFetchUser);
}
Future<void> _onFetchUser(
FetchUser event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
try {
final user = await _userRepository.getUser(event.userId);
emit(UserLoaded(user: user));
} on NetworkException catch (e) {
emit(UserError(message: e.message));
}
}
}
Using with Provider
class UserProvider extends ChangeNotifier {
final UserRepository _userRepository;
UserModel? _user;
bool _isLoading = false;
String? _error;
UserProvider({required UserRepository userRepository})
: _userRepository = userRepository;
UserModel? get user => _user;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> fetchUser(int userId) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_user = await _userRepository.getUser(userId);
} on NetworkException catch (e) {
_error = e.message;
} finally {
_isLoading = false;
notifyListeners();
}
}
}
⚡ Performance Tips
- Reuse NetworkClient Instance: Create once, use throughout the app
- Cancel Unnecessary Requests: Use
cancelTokenfor requests that may become obsolete - Implement Pagination: For large datasets
- Cache Responses: Implement caching layer on top of the client
- Use Isolates: For heavy JSON parsing, consider using compute()
🔒 Security Best Practices
- Never Log Tokens: The logger automatically masks sensitive headers
- Use Secure Storage: Store tokens using flutter_secure_storage
- Enable Certificate Pinning: For production apps
- Validate SSL: Never disable SSL verification in production
- Implement Token Refresh: Prevent frequent re-authentication
🐛 Common Pitfalls
1. Forgetting to Handle Network Exceptions
❌ Wrong:
final user = await networkClient.get('/users/1');
✅ Correct:
try {
final user = await networkClient.get('/users/1');
} on NetworkException catch (e) {
handleError(e.message);
}
2. Not Using fromJson
❌ Wrong:
final response = await networkClient.get('/users/1');
final user = UserModel.fromJson(response); // May fail
✅ Correct:
final response = await networkClient.get<Map<String, dynamic>>(
'/users/1',
fromJson: (json) => json,
);
final user = UserModel.fromJson(response);
3. Creating Multiple Client Instances
❌ Wrong:
// In each repository
final client = NetworkModule.createSimpleClient(...);
✅ Correct:
// Create once, inject everywhere
class DependencyInjection {
static final NetworkClient networkClient = NetworkModule.createFullClient(...);
}
4. Not Disposing the Client
❌ Wrong:
// Forgetting to dispose
✅ Correct:
@override
void dispose() {
networkClient.dispose();
super.dispose();
}
📚 Additional Resources
🤝 Contributing
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch
- Write tests for new features
- Ensure all tests pass
- Submit a pull request
📄 License
This project is licensed under the MIT License.
👥 Authors
Your development team
🙏 Acknowledgments
- Built on top of Dio
- Inspired by clean architecture principles
- Community feedback and contributions