Minimal REST HTTP Package

pub package GitHub stars License: MIT

A minimal, dynamic, and configurable REST HTTP client for Flutter applications with built-in error handling, retry logic, and flexible endpoint management. Perfect for developers who want a robust, type-safe, and easy-to-use HTTP client with comprehensive features.

✨ Features

  • 🚀 Dynamic Configuration: Easy setup with configurable base URLs, timeouts, and settings
  • 🔐 Flexible Authentication: Multiple authentication managers (SharedPreferences, Memory, Custom)
  • 📍 Endpoint Management: Dynamic endpoint management system for better maintainability
  • 🔄 Retry Logic: Built-in retry mechanism for failed requests with configurable attempts
  • 🛡️ Error Handling: Comprehensive error handling with optional UI popups
  • 📱 Connectivity Check: Automatic internet connectivity verification before requests
  • 🎯 Type Safety: Full type safety with generic response handling
  • 📊 Logging: Configurable request/response logging for debugging
  • 🔧 Extensible: Easy to extend and customize for your specific needs
  • 🌐 Any Response Format: Works with any API response structure
  • 🎨 Custom UI Integration: Easy integration with your existing popup/dialog systems
  • 📁 File Upload: Multipart file upload support
  • Performance: Optimized for performance with connection pooling

📦 Installation

Add this to your package's pubspec.yaml file:

dependencies:
  minimal_rest_http: ^1.0.0

Then run:

flutter pub get

🚀 Quick Start

1. Initialize the API Service

import 'package:minimal_rest_http/api_service.dart';

Future<void> initializeMinRestService() async {
  // Create configuration
  final config = ApiConfig(
    baseUrl: 'https://jsonplaceholder.typicode.com',
    timeoutSeconds: 30,
    enableLogging: true,
    enableErrorPopups: true,
  );

  // Create auth manager
  final authManager = SharedPreferencesAuthManager(
    tokenKey: 'access_token',
  );

  // Create endpoint manager (optional)
  final endpointManager = EndpointManager(baseUrl: config.baseUrl);
  endpointManager.addEndpoints({
    'users': '/users',
    'posts': '/posts',
    'login': '/auth/login',
  });

  // Initialize Minimal REST HTTP service
  await MinRestService.initialize(
    config: config,
    authManager: authManager,
    endpointManager: endpointManager,
  );
}

2. Make API Calls

// Using direct endpoints
final users = await MinRestService.instance.get<List<dynamic>>(
  '/users',
  fromJson: (data) => data as List<dynamic>,
);

// Using endpoint manager
final posts = await MinRestService.instance.getByKey<List<dynamic>>(
  'posts',
  fromJson: (data) => data as List<dynamic>,
);

// POST request with authentication
final newUser = await MinRestService.instance.post<Map<String, dynamic>>(
  '/users',
  body: {
    'name': 'John Doe',
    'email': 'john@example.com',
  },
  fromJson: (data) => data as Map<String, dynamic>,
  authToken: true,
);

3. Set Up Automatic Context Detection

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      home: MyHomePage().withApiContext(), // Automatic context detection
    );
  }
}

⚙️ Configuration

ApiConfig

final config = ApiConfig(
  baseUrl: 'https://api.example.com',        // Base URL for API
  timeoutSeconds: 30,                        // Request timeout
  maxRetryAttempts: 2,                       // Retry attempts
  enableLogging: true,                       // Enable request/response logging
  enableErrorPopups: false,                  // Enable error popups
  defaultHeaders: {                          // Default headers
    'Accept': 'application/json',
  },
  authTokenKey: 'access_token',              // Key for storing auth token
);

Runtime Configuration

final minRestService = MinRestService.instance;

// Change base URL temporarily
await minRestService.withScope((service) async {
  return await service.get('/different-endpoint', fromJson: (data) => data);
}, baseUrl: 'https://different-api.com');

// Change timeout temporarily
await minRestService.withScope((service) async {
  return await service.get('/slow-endpoint', fromJson: (data) => data);
}, timeoutSeconds: 60);

// Persistent configuration changes
minRestService.withBaseUrl('https://new-api.com');
minRestService.withTimeout(45);

🔐 Authentication

SharedPreferences Auth Manager

final authManager = SharedPreferencesAuthManager(
  tokenKey: 'access_token',
);

// Set token
await authManager.setToken('your_jwt_token');

// Get token
final token = await authManager.getToken();

// Clear token
await authManager.clearToken();

// Check if token exists
final hasToken = await authManager.hasToken();

Custom Auth Manager

final authManager = CustomAuthManager(
  getTokenFunction: () async => await getTokenFromSecureStorage(),
  setTokenFunction: (token) async => await saveTokenToSecureStorage(token),
  clearTokenFunction: () async => await clearTokenFromSecureStorage(),
  hasTokenFunction: () async => await hasTokenInSecureStorage(),
  tokenStream: tokenChangeStream,
);

📍 Endpoint Management

final endpointManager = EndpointManager(baseUrl: 'https://api.example.com');

// Add single endpoint
endpointManager.addEndpoint('users', '/users');

// Add multiple endpoints
endpointManager.addEndpoints({
  'users': '/users',
  'posts': '/posts',
  'comments': '/comments',
  'login': '/auth/login',
  'profile': '/user/profile',
});

// Use endpoints
final users = await minRestService.getByKey<List<dynamic>>(
  'users',
  fromJson: (data) => data as List<dynamic>,
);

// Build URLs with parameters
final url = endpointManager.buildUrlWithParams(
  'users',
  queryParams: {'page': '1', 'limit': '10'},
);

🛡️ Error Handling with AppPopup

The API Service package integrates seamlessly with your existing AppPopup system. When showErrorPopup is true, it will automatically show error popups using your AppPopup system.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      home: MyHomePage().withApiContext(), // Automatic context detection
    );
  }
}

Using AppPopup System

// API calls will automatically show popups when showErrorPopup is true
Future<void> loadUsers() async {
  try {
    final response = await MinRestService.instance.getByKey<dynamic>(
      'users',
      fromJson: (data) => data,
      showErrorPopup: true, // This will show your AppPopup on error
    );

    // Handle response...
  } catch (e) {
    // Error popup is already shown by the API service
    // Handle any additional logic here if needed
  }
}

🌐 Handling Any Response Format

The API Service package works with any response format. Here's how to handle different response structures:

Simple Array Response

// API returns: [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}]
final response = await MinRestService.instance.get<dynamic>(
  '/users',
  fromJson: (data) => data,
);

List<Map<String, dynamic>> users;
if (response is List) {
  users = response.cast<Map<String, dynamic>>();
} else {
  users = [];
}

Wrapped Response

// API returns: {"success": true, "data": [{"id": 1, "name": "John"}]}
final response = await MinRestService.instance.get<dynamic>(
  '/users',
  fromJson: (data) => data,
);

List<Map<String, dynamic>> users;
if (response is Map<String, dynamic>) {
  final data = ResponseUtils.getData(response);
  if (data is List) {
    users = data.cast<Map<String, dynamic>>();
  } else {
    users = [];
  }
}

// Check if response indicates success
final isSuccess = ResponseUtils.isSuccess(response);
if (!isSuccess) {
  final errorMessage = ResponseUtils.getErrorMessage(response);
  print('API Error: $errorMessage');
}

Using ResponseUtils

// Extract data from any response format
final data = ResponseUtils.getData(response);

// Check if response indicates success
final isSuccess = ResponseUtils.isSuccess(response);

// Get error message
final errorMessage = ResponseUtils.getErrorMessage(response);

// Check for pagination
if (ResponseUtils.hasPagination(response)) {
  final pagination = ResponseUtils.getPaginationInfo(response);
  print('Current page: ${pagination?['currentPage']}');
}

📡 HTTP Methods

GET Request

final data = await minRestService.get<Map<String, dynamic>>(
  '/endpoint',
  fromJson: (data) => data as Map<String, dynamic>,
  authToken: true,
  queryParams: {'param1': 'value1'},
);

POST Request

final result = await minRestService.post<Map<String, dynamic>>(
  '/endpoint',
  body: {'key': 'value'},
  fromJson: (data) => data as Map<String, dynamic>,
  authToken: true,
);

Multipart POST (File Upload)

final result = await minRestService.postMultipart<Map<String, dynamic>>(
  endpoint: '/upload',
  fromJson: (data) => data as Map<String, dynamic>,
  fileKey: 'file',
  filePath: '/path/to/file.jpg',
  fields: {'description': 'My file'},
  authToken: true,
);

PUT Request

final result = await minRestService.put<Map<String, dynamic>>(
  '/endpoint/1',
  {'key': 'updated_value'},
  (data) => data as Map<String, dynamic>,
  authToken: true,
);

DELETE Request

final result = await minRestService.delete<Map<String, dynamic>>(
  '/endpoint/1',
  (data) => data as Map<String, dynamic>,
  authToken: true,
);

🔧 Advanced Usage

Custom Configuration per Request

// Use different base URL for specific request
await minRestService.withScope((service) async {
  return await service.get('/external-api', fromJson: (data) => data);
}, baseUrl: 'https://external-api.com');

// Use different timeout for specific request
await minRestService.withScope((service) async {
  return await service.get('/slow-endpoint', fromJson: (data) => data);
}, timeoutSeconds: 120);

Connectivity Check

// Check connectivity
final hasConnection = await ConnectivityHelper.hasInternetConnection();

// Get detailed connectivity status
final status = await ConnectivityHelper.getConnectivityStatus();
print('Connection type: ${status['connectionTypes']}');

// Listen to connectivity changes
ConnectivityHelper.connectivityStream.listen((results) {
  print('Connectivity changed: $results');
});

📱 Example Project

Check out the example/ directory for a complete working example that demonstrates:

  • Basic API service setup
  • Authentication handling
  • Error handling with popups
  • Different response formats
  • File upload functionality

🚀 Migration from Hardcoded API Services

Before (Hardcoded)

class UrlHelper {
  static const String BaseUrl = "https://api.example.com";
  static const String users = "/users";
  static const String posts = "/posts";
}

// Usage
final response = await http.get(Uri.parse('${UrlHelper.BaseUrl}${UrlHelper.users}'));

After (Dynamic)

// Setup
final endpointManager = EndpointManager(baseUrl: 'https://api.example.com');
endpointManager.addEndpoints({
  'users': '/users',
  'posts': '/posts',
});

// Usage
final response = await apiService.getByKey<Map<String, dynamic>>(
  'users',
  fromJson: (data) => data as Map<String, dynamic>,
);

🎯 Best Practices

  1. Initialize Early: Initialize the API service in your app's main function
  2. Use Endpoint Manager: Define all endpoints in one place for better maintainability
  3. Handle Errors: Always handle errors appropriately for better user experience
  4. Type Safety: Use proper type definitions for your response models
  5. Configuration: Use different configurations for different environments (dev, staging, prod)
  6. Logging: Enable logging in development, disable in production
  7. Timeouts: Set appropriate timeouts based on your API's response times

📚 Documentation

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

👨‍💻 Author

MD Tangim Haque - @imtangim

⭐ Show Your Support

Give a ⭐️ if this project helped you!

🐛 Report Issues

If you find any issues or have suggestions, please open an issue.


Made with ❤️ by MD Tangim Haque