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 baseUrl validation + normalization (requires http(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 connection
  • TimeoutException - Request timeout
  • CancelledException - Request was cancelled
  • BadRequestException - 400 Bad Request
  • UnauthorizedException - 401 Unauthorized
  • ForbiddenException - 403 Forbidden
  • NotFoundException - 404 Not Found
  • ConflictException - 409 Conflict
  • UnprocessableEntityException - 422 Validation Error
  • TooManyRequestsException - 429 Rate Limited
  • InternalServerException - 500 Internal Server Error
  • ServiceUnavailableException - 503 Service Unavailable
  • SerializationException - JSON parsing error
  • UnknownException - 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

  1. Reuse NetworkClient Instance: Create once, use throughout the app
  2. Cancel Unnecessary Requests: Use cancelToken for requests that may become obsolete
  3. Implement Pagination: For large datasets
  4. Cache Responses: Implement caching layer on top of the client
  5. Use Isolates: For heavy JSON parsing, consider using compute()

🔒 Security Best Practices

  1. Never Log Tokens: The logger automatically masks sensitive headers
  2. Use Secure Storage: Store tokens using flutter_secure_storage
  3. Enable Certificate Pinning: For production apps
  4. Validate SSL: Never disable SSL verification in production
  5. 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:

  1. Fork the repository
  2. Create a feature branch
  3. Write tests for new features
  4. Ensure all tests pass
  5. 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
Details
Pub
2026-02-27 07:01:29 +00:00
3
28 KiB
Assets (1)
1.0.4.tar.gz 28 KiB
Versions (4) View all
1.0.5 2026-03-04
1.0.4 2026-02-27
1.0.3 2026-02-19
1.0.2 2026-02-16