iws_http_model 1.2.0
iws_http_model: ^1.2.0 copied to clipboard
Library for HTTP requests with the common operations CRUD, pagination and error handling.
Library for HTTP requests with the common CRUD operations, pagination and error handling.
Features #
- Authentication: JWT token and API key support.
- HTTP Error Handling: Built-in error handling for HTTP requests.
- REST Route Builder: Helper methods for building REST API endpoints.
- Caching Support: Optional caching functionality for improved performance:
- Memory Cache - Fast in-memory caching for temporary data
- Persistent Cache - Long-term storage that survives app restarts
- Configurable TTL - Time-to-live settings for cache entries
- Automatic Cache Keys - Smart cache key generation based on URL and parameters
- POST Request Caching - Cache support for complex search queries with request bodies
- Complete CRUD Operations:
- Get by ID (GET) - Retrieve single records with optional path parameters
- Paginated Query (GET) - Fetch paginated lists with query parameters
- Paginated Search (POST) - Advanced search with request body and ordering
- Create (POST) - Add new records
- Update (PUT) - Complete record updates
- Partial Update (PATCH) - Partial record updates
- Delete (DELETE) - Remove records
- Flexible Path Management:
- Custom path support with parameter substitution
- Dynamic URL generation
- Support for nested resource paths
- Advanced Pagination:
- Built-in pagination metadata handling
- Configurable page sizes and ordering
- Support for both GET and POST pagination
- Type Safety: Full generic type support for model serialization
Getting started #
Initialize the IwsHttp library:
IwsHttp.setup(
authority: 'mydomain.com',
apiKey: 'my_secret_api_key',
basePath: 'api/',
secureConnection: true,
bearerToken: 'my_secret_token');
Cache Setup (Optional) #
The library supports optional caching using the iws_cache package. Add it to your dependencies:
dependencies:
iws_cache: ^0.0.1
Cache Types #
Memory Cache
Fast in-memory cache for temporary data:
import 'package:iws_cache/iws_cache.dart';
final memoryCache = IwsMemoryCache(
maxSize: 100,
evictionPolicy: EvictionPolicy.lru,
cleanupInterval: Duration(minutes: 5),
);
Persistent Cache
Data survives app restarts using SharedPreferences:
final persistentCache = IwsPersistentCache(
maxSize: 200,
evictionPolicy: EvictionPolicy.lru,
);
Using CacheService (Recommended)
For production apps, use the singleton CacheService:
final cacheService = CacheService.instance;
final cache = cacheService.memoryCache; // or persistentCache, apiCache, userDataCache
Usage #
Instantiate IwsHttpModel #
final myModelProvider = IwsHttpModel<MyModelClass>(
path: 'myModels',
iwsHttp: IwsHttp(),
fromJson: MyModelClass.fromJson,
toJson: MyModelClass.toJson,
resultAttribute: 'data' // Optional, defaults to 'data'
);
Available Methods #
1. Get by ID
Retrieve a single model by its identifier.
// Basic usage
MyModelClass myModel = await myModelProvider.get(120);
// With custom path and parameters
MyModelClass myModel = await myModelProvider.get(
120,
customPath: 'users/{userId}/models',
pathParams: {'userId': 456},
queryParameters: {'include': 'relations'},
auth: true,
apiKey: true
);
// With cache support
MyModelClass myModel = await myModelProvider.get(
120,
cache: cache,
cacheTtl: const Duration(minutes: 10),
auth: true
);
2. Paginated Query (GET)
Retrieve a paginated list of models using GET request.
// Basic usage
PaginatedData<MyModelClass> result = await myModelProvider.fetchPaginated();
// With pagination and query parameters
PaginatedData<MyModelClass> result = await myModelProvider.fetchPaginated(
page: 2,
queryParameters: {
'filter': 'active',
'orderBy': 'name',
'orderType': 'asc'
},
customPath: 'users/{userId}/models',
pathParams: {'userId': 456},
auth: true,
apiKey: true
);
// With cache support
PaginatedData<MyModelClass> result = await myModelProvider.fetchPaginated(
page: 1,
queryParameters: {'filter': 'active'},
cache: cache,
cacheTtl: const Duration(minutes: 15),
auth: true
);
3. Paginated Search (POST)
Retrieve a paginated list of models using POST request with search criteria.
// Basic usage with search body
PaginatedData<MyModelClass> result = await myModelProvider.fetchPaginatedQuery(
body: {
'search': 'search term',
'filters': {'status': 'active'}
}
);
// Advanced usage with all parameters
PaginatedData<MyModelClass> result = await myModelProvider.fetchPaginatedQuery(
page: 1,
orderBy: 'created_at',
orderType: OrderType.desc,
queryParameters: {'include': 'relations'},
customPath: 'search/models',
pathParams: {'category': 'electronics'},
body: {
'search': 'iPhone',
'filters': {
'price_min': 100,
'price_max': 1000
}
},
auth: true,
apiKey: true
);
// With cache support
PaginatedData<MyModelClass> result = await myModelProvider.fetchPaginatedQuery(
page: 1,
orderBy: 'name',
orderType: OrderType.asc,
body: {'search': 'electronics'},
cache: cache,
cacheTtl: const Duration(minutes: 20),
auth: true
);
4. Create (POST)
Add a new model.
// Basic usage
MyModelClass newModel = MyModelClass(name: 'New Item');
MyModelClass createdModel = await myModelProvider.add(newModel);
// With custom path and parameters
MyModelClass createdModel = await myModelProvider.add(
newModel,
customPath: 'users/{userId}/models',
pathParams: {'userId': 456}
);
5. Update (PUT)
Update an existing model completely.
// Basic usage
MyModelClass updatedModel = MyModelClass(id: 120, name: 'Updated Item');
MyModelClass result = await myModelProvider.update(updatedModel, 120);
// With custom path and parameters
MyModelClass result = await myModelProvider.update(
updatedModel,
120,
customPath: 'users/{userId}/models',
pathParams: {'userId': 456}
);
6. Partial Update (PATCH)
Update an existing model partially.
// Basic usage
MyModelClass partialModel = MyModelClass(name: 'Only name updated');
MyModelClass result = await myModelProvider.patchUpdate(partialModel, 120);
// With custom path and parameters
MyModelClass result = await myModelProvider.patchUpdate(
partialModel,
120,
customPath: 'users/{userId}/models',
pathParams: {'userId': 456}
);
7. Delete
Delete a model by ID.
// Basic usage
await myModelProvider.delete(120);
// With custom path and parameters
await myModelProvider.delete(
120,
customPath: 'users/{userId}/models',
pathParams: {'userId': 456}
);
8. Fetch List (GET)
Retrieve a simple list of models without pagination.
// Basic usage
List<MyModelClass> models = await myModelProvider.fetch();
// With query parameters
List<MyModelClass> models = await myModelProvider.fetch(
queryParameters: {
'filter': 'active',
'limit': '50'
},
customPath: 'users/{userId}/models',
pathParams: {'userId': 456},
auth: true,
apiKey: true
);
// With cache support
List<MyModelClass> models = await myModelProvider.fetch(
queryParameters: {'status': 'published'},
cache: cache,
cacheTtl: const Duration(minutes: 5),
auth: true
);
9. Generate URL
Generate the endpoint URL for debugging or custom requests.
// Basic URL generation
String url = myModelProvider.generateURL();
// With custom path, parameters and ID
String url = myModelProvider.generateURL(
customPath: 'users/{userId}/models/{modelId}',
pathParam: {'userId': 456, 'modelId': 120},
id: '120'
);
Working with Paginated Data #
The fetchPaginated and fetchPaginatedQuery methods return a PaginatedData<T> object:
PaginatedData<MyModelClass> result = await myModelProvider.fetchPaginated();
// Access the data
List<MyModelClass> models = result.data;
// Access pagination metadata
ListMetaData meta = result.meta;
int currentPage = meta.currentPage;
int totalPages = meta.lastPage;
int? totalItems = meta.total;
int? itemsPerPage = meta.perPage;
int? fromItem = meta.from;
int? toItem = meta.to;
Working with Cache #
The library provides comprehensive caching support for all read operations (fetch, fetchPaginated, fetchPaginatedQuery, and get) using the CacheConfig class.
CacheConfig Class
All read methods support an optional cacheConfig parameter that encapsulates cache-related settings:
cache: An instance ofIwsCache(memory or persistent)ttl: Time-to-live duration for cache entries (optional, no default)staleWhileRevalidate: If true, returns cached data immediately and updates cache in backgroundforceRefresh: If true, forces fresh data from server, ignoring existing cache
CacheConfig Constructors
// Standard cache behavior (wait for fresh data if cache is expired)
final standardConfig = CacheConfig.standard(
cache: cache,
ttl: const Duration(minutes: 10),
);
// Stale-while-revalidate behavior (return cache immediately, update in background)
final staleConfig = CacheConfig.staleWhileRevalidate(
cache: cache,
ttl: const Duration(minutes: 10),
);
// Force refresh behavior (ignore cache, fetch fresh data, update cache)
final forceRefreshConfig = CacheConfig.forceRefresh(
cache: cache,
ttl: const Duration(minutes: 10),
);
// Custom configuration
final customConfig = CacheConfig(
cache: cache,
ttl: const Duration(hours: 2),
staleWhileRevalidate: true,
forceRefresh: false,
);
Cache Key Generation
The library automatically generates unique cache keys based on:
- Method name: Different prefixes for
fetch,fetchPaginated,fetchPaginatedQuery, andget - URL: Including custom paths and path parameters
- Query parameters: All query string parameters
- Request body: For POST requests (hashed for consistency)
Basic Cache Usage
// Create cache instance
final cache = IwsMemoryCache(maxSize: 100, evictionPolicy: EvictionPolicy.lru);
// Create cache configuration
final cacheConfig = CacheConfig.standard(
cache: cache,
ttl: const Duration(minutes: 10),
);
// Fetch with cache
List<MyModel> models = await provider.fetch(
queryParameters: {'status': 'active'},
cacheConfig: cacheConfig,
);
// Subsequent calls with same parameters will use cache
List<MyModel> cachedModels = await provider.fetch(
queryParameters: {'status': 'active'},
cacheConfig: cacheConfig,
); // This will return cached data if still valid
Cache with Pagination
// Create cache configuration
final cacheConfig = CacheConfig.standard(
cache: cache,
ttl: const Duration(minutes: 15),
);
// Cache paginated results
PaginatedData<MyModel> page1 = await provider.fetchPaginated(
page: 1,
queryParameters: {'category': 'electronics'},
cacheConfig: cacheConfig,
);
// Different pages are cached separately
PaginatedData<MyModel> page2 = await provider.fetchPaginated(
page: 2,
queryParameters: {'category': 'electronics'},
cacheConfig: cacheConfig,
);
Cache with POST Searches
// Create cache configuration
final cacheConfig = CacheConfig.standard(
cache: cache,
ttl: const Duration(hours: 1),
);
// Cache search results
PaginatedData<MyModel> results = await provider.fetchPaginatedQuery(
body: {
'search': 'smartphone',
'filters': {'price_max': 500}
},
cacheConfig: cacheConfig,
);
// Same search parameters will use cache
PaginatedData<MyModel> cachedResults = await provider.fetchPaginatedQuery(
body: {
'search': 'smartphone',
'filters': {'price_max': 500}
},
cacheConfig: cacheConfig,
); // Returns cached data
Cache Management
// Get cache statistics
final stats = await cache.getStats();
print('Cache hit ratio: ${stats.hitRatio}');
print('Items in cache: ${stats.itemCount}');
// Clear specific items (manual cache invalidation)
await cache.remove('specific_cache_key');
// Clear all cache
await cache.clear();
// Close cache when done
await cache.close();
Stale-While-Revalidate Pattern
The CacheConfig.staleWhileRevalidate() constructor implements a performance optimization pattern:
- Immediate response: Returns cached data immediately if available
- Background update: Fetches fresh data in the background to update the cache
- Best of both worlds: Fast response times with eventually consistent data
// Create stale-while-revalidate configuration
final staleConfig = CacheConfig.staleWhileRevalidate(
cache: cache,
ttl: const Duration(minutes: 10),
);
// Enable stale-while-revalidate
List<MyModel> models = await provider.fetch(
queryParameters: {'status': 'active'},
cacheConfig: staleConfig, // Returns cache immediately, updates in background
);
// Works with pagination
PaginatedData<MyModel> page = await provider.fetchPaginated(
page: 1,
queryParameters: {'category': 'books'},
cacheConfig: CacheConfig.staleWhileRevalidate(
cache: cache,
ttl: const Duration(minutes: 15),
),
);
// Works with POST searches
PaginatedData<MyModel> results = await provider.fetchPaginatedQuery(
body: {'search': 'electronics'},
cacheConfig: CacheConfig.staleWhileRevalidate(
cache: cache,
ttl: const Duration(hours: 1),
),
);
// Works with single item fetching
MyModel? item = await provider.get(
123,
cacheConfig: CacheConfig.staleWhileRevalidate(
cache: cache,
ttl: const Duration(minutes: 30),
),
);
When to use stale-while-revalidate:
- High-frequency requests: When the same data is requested multiple times
- User experience priority: When immediate response is more important than absolute freshness
- Background apps: When you want to keep data updated without blocking the UI
- Mobile applications: To reduce perceived loading times and improve responsiveness
Behavior details:
- If no cached data exists, behaves like a normal request (waits for network)
- If cached data exists (even if expired), returns it immediately
- Background update happens asynchronously and doesn't block the response
- Next request will get the updated data from the cache
Force Refresh Strategy #
The forceRefresh option allows you to bypass the cache completely and fetch fresh data directly from the server, while still updating the cache with the new data.
// Create a force refresh configuration
final forceRefreshConfig = CacheConfig.forceRefresh(
cache: cache,
ttl: const Duration(minutes: 10),
);
// Force refresh ignores any existing cache and fetches fresh data
List<MyModel> freshModels = await provider.fetch(
queryParameters: {'status': 'active'},
cacheConfig: forceRefreshConfig, // Ignores cache, fetches fresh, updates cache
);
// Works with all read methods
PaginatedData<MyModel> freshPage = await provider.fetchPaginated(
page: 1,
queryParameters: {'category': 'books'},
cacheConfig: CacheConfig.forceRefresh(cache: cache),
);
// Single item with force refresh
MyModel? freshItem = await provider.get(
123,
cacheConfig: CacheConfig.forceRefresh(cache: cache),
);
When to use forceRefresh:
- After data modifications: When you've created, updated, or deleted data and need fresh results
- User-initiated refresh: When users explicitly request fresh data (pull-to-refresh)
- Critical data updates: When you need to ensure you have the absolute latest data
- Cache invalidation: When you know the cached data is stale and must be refreshed
Behavior details:
- Always fetches data from the server, regardless of cache state
- Cache is ignored during the read operation
- Fresh data is stored in cache after successful fetch
- Subsequent requests (without forceRefresh) will use the updated cache
Practical example - after creating a new item:
// Create a new book
MyModel newBook = await provider.add(myNewBook);
// Now fetch the list with forceRefresh to get updated results including the new book
List<MyModel> updatedList = await provider.fetch(
queryParameters: {'category': 'books'},
cacheConfig: CacheConfig.forceRefresh(
cache: cache,
ttl: const Duration(minutes: 15),
),
);
Cache Cleanup #
// Always close the cache when done
await cache.close();
Cache Best Practices
- Use appropriate TTL: Set longer TTL for stable data, shorter for frequently changing data
- Choose cache type: Use memory cache for temporary data, persistent cache for data that should survive app restarts
- Monitor cache size: Set appropriate
maxSizeto prevent memory issues - Cache invalidation: Clear cache when data is modified via
add,update, ordeleteoperations - Error handling: Cache operations are fail-safe - if cache fails, the HTTP request will still execute
Cache Eviction Policies
Choose the best eviction strategy based on your use case:
// LRU (Least Recently Used) - Default, good for most cases
final cache = IwsMemoryCache(evictionPolicy: EvictionPolicy.lru);
// LFU (Least Frequently Used) - Good for frequently accessed data
final cache = IwsMemoryCache(evictionPolicy: EvictionPolicy.lfu);
// FIFO (First In, First Out) - Simple time-based eviction
final cache = IwsMemoryCache(evictionPolicy: EvictionPolicy.fifo);
// LIFO (Last In, First Out) - Stack-like behavior
final cache = IwsMemoryCache(evictionPolicy: EvictionPolicy.lifo);
// Random - Random eviction
final cache = IwsMemoryCache(evictionPolicy: EvictionPolicy.random);
Order Types #
For search queries, you can specify the order type:
// Ascending order (default)
OrderType.asc
// Descending order
OrderType.desc
Path Parameters #
Use curly braces {} in custom paths to define parameters:
// Define path with parameters
customPath: 'users/{userId}/categories/{categoryId}/items'
// Replace parameters
pathParams: {
'userId': 123,
'categoryId': 'electronics'
}
// Results in: users/123/categories/electronics/items
Data Models #
PaginatedData
Contains paginated results and metadata:
class PaginatedData<T> {
List<T> data; // The actual data items
ListMetaData meta; // Pagination metadata
}
ListMetaData
Pagination information:
class ListMetaData {
int currentPage; // Current page number
int? from; // First item number on current page
int lastPage; // Total number of pages
int? perPage; // Items per page
int? to; // Last item number on current page
int? total; // Total number of items
}
OrderType Enum
Available ordering options:
enum OrderType {
asc, // Ascending order
desc // Descending order
}
JSON Structure for Pagination #
Paging queries expect a specific response format:
{
"data": [
{
"id": 1,
"name": "Item 1"
},
{
"id": 2,
"name": "Item 2"
}
],
"meta": {
"current_page": 1,
"from": 1,
"last_page": 2,
"per_page": 10,
"to": 10,
"total": 15
}
}
For non-paginated responses, you can return just the data array:
[
{
"id": 1,
"name": "Item 1"
}
]
Advanced Usage Examples #
Nested Resource Management
// Managing user payments
final paymentProvider = IwsHttpModel<Payment>(
path: 'users/{userId}/payments',
iwsHttp: IwsHttp(),
fromJson: Payment.fromJson,
toJson: Payment.toJson
);
// Get user's payments
PaginatedData<Payment> payments = await paymentProvider.fetchPaginated(
pathParams: {'userId': 123},
page: 1
);
// Create new payment for user
Payment newPayment = Payment(amount: 100.0, method: 'credit_card');
Payment created = await paymentProvider.add(
newPayment,
pathParams: {'userId': 123}
);
Custom Search with Complex Filters
// Advanced search with multiple filters
PaginatedData<Product> products = await productProvider.fetchPaginatedQuery(
page: 1,
orderBy: 'price',
orderType: OrderType.asc,
body: {
'search': 'smartphone',
'filters': {
'category': 'electronics',
'price_range': {
'min': 200,
'max': 1000
},
'brand': ['Apple', 'Samsung'],
'in_stock': true
},
'date_range': {
'from': '2024-01-01',
'to': '2024-12-31'
}
}
);
Error Handling
try {
MyModelClass model = await myModelProvider.get(999);
} on IwsHttpException catch (e) {
// Handle HTTP errors (404, 500, etc.)
print('HTTP Error: ${e.statusCode} - ${e.message}');
} catch (e) {
// Handle other errors
print('Error: $e');
}
Custom Result Attribute
If your API returns data in a different attribute:
final customProvider = IwsHttpModel<MyModel>(
path: 'models',
iwsHttp: IwsHttp(),
fromJson: MyModel.fromJson,
toJson: MyModel.toJson,
resultAttribute: 'result' // API returns {"result": {...}}
);
Best Practices #
- Reuse IwsHttpModel instances: Create them once and reuse throughout your app
- Use path parameters: For dynamic routes, prefer path parameters over custom paths
- Handle pagination: Always check
meta.lastPagewhen implementing infinite scroll - Error handling: Wrap HTTP calls in try-catch blocks
- Type safety: Define proper model classes with
fromJsonandtoJsonmethods - Cache strategy:
- Use memory cache for frequently accessed, temporary data
- Use persistent cache for data that should survive app restarts
- Set appropriate TTL based on data volatility
- Implement cache invalidation when data is modified
- Cache sizing: Set reasonable
maxSizelimits to prevent memory issues - Performance: Cache read operations (
fetch,get,fetchPaginated) but not write operations
Integration with State Management #
With Provider
class MyModelNotifier extends ChangeNotifier {
final IwsHttpModel<MyModel> _modelProvider;
final IwsCache _cache;
List<MyModel> _models = [];
MyModelNotifier(this._modelProvider, this._cache);
Future<void> loadModels({bool forceRefresh = false}) async {
try {
final result = await _modelProvider.fetchPaginated(
cache: forceRefresh ? null : _cache,
cacheTtl: const Duration(minutes: 10)
);
_models = result.data;
notifyListeners();
} catch (e) {
// Handle error
}
}
Future<void> refreshModels() async {
await _cache.clear(); // Clear cache to force refresh
await loadModels();
}
}
With BLoC
class MyModelBloc extends Bloc<MyModelEvent, MyModelState> {
final IwsHttpModel<MyModel> modelProvider;
final IwsCache cache;
MyModelBloc(this.modelProvider, this.cache) : super(MyModelInitial()) {
on<LoadModels>(_onLoadModels);
on<RefreshModels>(_onRefreshModels);
}
Future<void> _onLoadModels(LoadModels event, Emitter<MyModelState> emit) async {
emit(MyModelLoading());
try {
final result = await modelProvider.fetchPaginated(
page: event.page,
cache: cache,
cacheTtl: const Duration(minutes: 15)
);
emit(MyModelLoaded(result.data, result.meta));
} catch (e) {
emit(MyModelError(e.toString()));
}
}
Future<void> _onRefreshModels(RefreshModels event, Emitter<MyModelState> emit) async {
await cache.clear(); // Force refresh by clearing cache
add(LoadModels(page: 1));
}
}
Request Parameters #
The parameters for paging and ordering are sent in the request route as follows:
https://exampledomain.com/api/models?page=1&orderBy=name&orderType=asc&filter=active
For POST requests with search body:
POST https://exampledomain.com/api/models/search?page=1&orderBy=created_at&orderType=desc
Content-Type: application/json
{
"search": "search term",
"filters": {
"status": "active",
"category": "electronics"
}
}