A powerful network package for Flutter built on top of DIO, designed to streamline API development with features like caching, logging, token authentication, and local data storage management.
- DIO Wrapper: Simplifies HTTP requests and responses.
- Caching: Efficiently cache API responses to improve performance and reduce network usage.
- Logging: Generate cURL commands for easy debugging and log network requests using
Log.dandLog.i. - Token Authentication: Manage access and refresh tokens seamlessly.
- Retry Policies: Automatically retry failed requests based on customizable policies.
- Base Models: Utilize abstract base models for request and response handling.
- Local Storage Management: Manage app-specific data storage.
- Dependency Injection: Uses GetIt for easy dependency management.
- Customizable Headers: Add or update headers for requests.
- Security Context: Support for custom security context and allowed hosts.
Add the package to your pubspec.yaml:
dependencies:
network_plus: ^0.0.2Then run:
flutter pub getRegister and configure the core dependencies using GetIt:
import 'package:retail_core/retail_core.dart';
coreDILocator.registerLazySingleton<CoreConfiguration>(() => CoreConfiguration(
baseUrl: "https://api.example.com",
timeout: 120000,
connectTimeout: 120000,
cachePolicy: CachePolicy.request,
refreshTokenUrl: "https://api.example.com/auth/refresh",
refreshTokenKey: "refresh_token",
accessTokenKey: "access_token",
securityContext: null, // Optionally provide SecurityContextConfig
additionalHeaders: {
"locale": "en",
"unique-reference-code": "GUID",
},
storageProviderForToken: StorageProvider.sharedPref,
loggerConfig: const LoggerConfig(
shouldShowLogs: true,
logLevel: LogsLevel.trace,
lineLength: 1000,
),
));
// Call setup to register network-related dependencies
coreDILocator<CoreConfiguration>().setup();Extend the BaseRepository to create your own repository for API calls:
class MyCustomRepository extends BaseRepository<MyResponseClass> {
MyCustomRepository(NetworkExecutor networkExecutor) : super(networkExecutor);
Future<Result<MyUiModel>> fetchData() async {
return await execute<MyMapper, MyResponseClass, MyUiModel>(
urlPath: '/api/data',
method: METHOD_TYPE.GET,
params: EmptyRequest(),
mapper: MyMapper(),
responseType: MyResponseClass(),
cachePolicy: CachePolicy.cacheFirst,
retryPolicy: RetryPolicy(retrialCount: 3, retryDelay: Duration(seconds: 2)),
);
}
}You can manage local data storage by creating your own storage class:
coreDILocator.registerLazySingleton<LocalStorageService>(() {
final storageManager = LocalStorageManager();
storageManager.addService(SharedPrefStorageProvider());
storageManager.init();
return storageManager;
});
coreDILocator.registerLazySingleton<AppDataStorage>(
() => AppDataStorage(coreDILocator<LocalStorageService>(), StorageProvider.sharedPref),
);
class AppDataStorage {
final LocalStorageService localStorage;
final StorageProvider provider;
AppDataStorage(this.localStorage, this.provider);
Future<void> saveMyAppData(String key, dynamic value) async {
await localStorage.saveData(StorageData({key: value}, provider));
}
Future<T?> getValueByKey<T>(String key) async {
return await localStorage.getData<T>(key, provider);
}
Future<void> clearValue(String key) async {
await localStorage.removeData(key, provider);
}
}Below is a real-world example of how to implement a repository using retail_core for network operations in a Flutter app. This pattern allows you to easily manage API calls, handle mock data, and map responses to your domain models.
import 'package:retail_core/retail_core.dart';
import 'package:mns_retail_app/data/repositories/checkout/request/billing/update_billing_address_request.dart';
import 'package:mns_retail_app/domain/entity/checkout/checkout_model.dart';
import 'package:mns_retail_app/domain/repositories/checkout/checkout_mapper.dart';
class CheckoutRepositoryImpl extends BaseRepository<CheckoutResponse> {
final checkoutMapper = coreDILocator<CheckoutMapper>();
CheckoutRepositoryImpl(NetworkExecutor networkExecutor) : super(networkExecutor);
// Fetch checkout details
Future<Result<CheckoutModel>> fetchCheckout(String cartId) async {
return await execute(
urlPath: "checkout/guest/v1/orders?cartId=$cartId",
method: METHOD_TYPE.POST,
params: EmptyRequest(),
mapper: checkoutMapper,
responseType: CheckoutResponse(),
);
}
// Update billing address
Future<Result<CheckoutModel>> updateBillingAddress(String orderId, UpdateCheckoutAddressModel updateBillingAddressModel) async {
return await execute(
urlPath: "checkout/guest/v1/orders/$orderId/billinginfo",
method: METHOD_TYPE.PUT,
params: UpdateAddressRequest(addAddressModel: updateBillingAddressModel),
isJsonEncode: true,
mapper: checkoutMapper,
responseType: CheckoutResponse(),
);
}
}final checkoutRepo = CheckoutRepositoryImpl(coreDILocator<NetworkExecutor>());
// Fetch checkout
final result = await checkoutRepo.fetchCheckout('your-cart-id');
result.when(
success: (data) => print('Checkout data: $data'),
error: (error) => print('Error: $error'),
);
// Update billing address
final updateResult = await checkoutRepo.updateBillingAddress('order-id', yourUpdateModel);
updateResult.when(
success: (data) => print('Updated checkout: $data'),
error: (error) => print('Update error: $error'),
);This approach leverages retail_core's execute method for all network operations, ensuring consistent error handling, response mapping, and support for features like caching and retry policies.
Mappers are responsible for converting raw API response data (usually JSON) into your app’s domain models. This separation ensures that your UI and business logic work with clean, predictable data structures, regardless of how the backend formats its responses.
A typical mapper implements a method like mapFrom, which takes a response object and returns a domain model:
class CheckoutMapper {
CheckoutModel mapFrom(CheckoutResponse response) {
// Convert API response to domain model
return CheckoutModel(
id: response.id,
items: response.items.map((item) => ItemModel.fromJson(item)).toList(),
// ...other fields
);
}
}When using retail_core, you pass your mapper to the execute method. The package will automatically use it to transform the API response before returning it to your repository or UI layer.
The Result class is a wrapper that represents the outcome of an operation—either a success with data, or a failure with an error. This pattern makes error handling explicit and consistent throughout your codebase.
A typical Result usage looks like this:
final result = await checkoutRepo.fetchCheckout('cart-id');
result.when(
success: (data) {
// Handle the successful data
print('Checkout: $data');
},
error: (error) {
// Handle the error
print('Error: $error');
},
);This approach avoids exceptions bubbling up and makes it easy to handle both success and failure cases in a unified way.
Summary:
- Mappers convert API responses to your app’s models.
- Result wraps the outcome of operations, making error handling simple and robust.
For more details, see the example repository and usage patterns above.
- API Caching: Specify cache behavior with
CachePolicywhen callingexecute. - Updating Headers: Customize request headers via
additionalHeadersor directly on theDioinstance. - Retry Policies: Use
RetryPolicyto define retry logic for requests. - Logging: Configure logging with
LoggerConfigand useLog.d,Log.i,Log.efor messages. - Security Context: Restrict allowed hosts and provide custom security context for requests.
Contributions are welcome! Please submit a pull request or open an issue if you encounter any problems or have suggestions for improvement.