koi_network 0.0.5
koi_network: ^0.0.5 copied to clipboard
Enterprise-grade network library built on Dio with configurable response parsing, request encoding, token refresh, retry, caching, and adapter-based architecture.
简体中文 | English
Koi Network #
A flexible Dio-based networking library for Dart and Flutter projects.
koi_network provides a reusable network layer with adapter-based integration,
configurable response parsing, request execution helpers, token refresh,
retry, caching, and multi-module Dio management.
Why Koi Network #
Many projects need the same network capabilities, but do not want to bind the network layer to a specific UI framework, state management solution, or backend response format.
koi_network solves that by separating infrastructure from project-specific
logic:
- Adapter-based auth, loading, error handling, platform, logging, parsing, dynamic headers, and request encoding
- Works with custom response envelopes such as
{code, msg, data}or other backend formats - Built-in request execution patterns for normal, silent, quick, batch, and retry flows
- Proactive and reactive token refresh support
- Optional retry and cache support through Dio middleware
- Support for both raw Dio responses and already typed API wrappers
Installation #
Add the package to your pubspec.yaml:
dependencies:
koi_network: ^0.0.5
Then install dependencies:
dart pub get
If you use Flutter, flutter pub get also works.
Minimal Setup #
The smallest working setup is:
- Register adapters
- Initialize the network layer
- Get a Dio instance and make requests
import 'package:koi_network/koi_network.dart';
Future<void> setupNetwork() async {
KoiNetworkAdapters.register(
authAdapter: KoiDefaultAuthAdapter(),
errorHandlerAdapter: KoiDefaultErrorHandlerAdapter(),
loadingAdapter: KoiDefaultLoadingAdapter(),
platformAdapter: KoiDefaultPlatformAdapter(),
loggerAdapter: KoiDefaultLoggerAdapter(),
);
await KoiNetworkInitializer.initialize(
baseUrl: 'https://api.example.com',
environment: 'development',
);
}
After initialization:
final dio = KoiNetworkServiceManager.instance.mainDio;
final profile = await KoiRequestExecutor.execute<Map<String, dynamic>>(
request: () => dio.get('/user/profile'),
);
Dynamic Headers #
Use headerBuilders when a project needs to inject request-specific headers
without hard-coding business rules inside the network package:
await KoiNetworkInitializer.initialize(
baseUrl: 'https://api.example.com',
environment: 'production',
headerBuilders: [
(options) async => {'X-Tenant': 'school-a'},
],
);
Custom Adapters #
In real projects, you usually replace the default adapters with application implementations.
TLS Defaults #
KoiNetworkConfig.create() and KoiNetworkConfig.production() validate SSL
certificates by default. Development and testing configs keep certificate
validation disabled for local or self-signed services.
If a migration must temporarily keep the old insecure behavior, opt in explicitly:
await KoiNetworkInitializer.initialize(
baseUrl: 'https://api.example.com',
environment: 'production',
validateCertificate: false,
);
Example auth adapter with JWT support:
import 'package:dio/dio.dart';
import 'package:koi_network/koi_network.dart';
class MyAuthAdapter extends KoiAuthAdapter with KoiJwtTokenMixin {
String? _token;
String? _refreshToken;
@override
String? getToken() => _token;
@override
String? getRefreshToken() => _refreshToken;
@override
Future<bool> refresh() async {
try {
final dio = KoiDioFactory.createTokenDio(null);
final response = await dio.post(
'/auth/refresh',
data: {'refresh_token': getRefreshToken()},
);
final accessToken = response.data['access_token'] as String?;
if (accessToken == null || accessToken.isEmpty) {
return false;
}
await saveToken(accessToken);
return true;
} catch (_) {
return false;
}
}
@override
Future<void> saveToken(String token) async {
_token = token;
}
@override
Future<void> saveRefreshToken(String refreshToken) async {
_refreshToken = refreshToken;
}
@override
Future<void> clearToken() async {
_token = null;
_refreshToken = null;
}
}
class MyErrorHandler extends KoiErrorHandlerAdapter {
@override
void showError(String message) {
print('Error: $message');
}
@override
Future<bool> handleAuthError({int? statusCode, String? message}) async {
// For example: clear session and redirect to login
return true;
}
@override
String formatErrorMessage(DioException error) {
return error.message ?? error.toString();
}
}
Request Execution #
KoiRequestExecutor is the main entry point for standard Dio requests.
Parse JSON into a model #
final user = await KoiRequestExecutor.execute<User>(
request: () => dio.get('/user/profile'),
fromJson: (json) => User.fromJson(json as Map<String, dynamic>),
);
Silent request #
final settings = await KoiRequestExecutor.executeSilent<Map<String, dynamic>>(
request: () => dio.get('/settings'),
);
Quick request #
final notifications = await KoiRequestExecutor.executeQuick<List<dynamic>>(
request: () => dio.get('/notifications'),
);
Batch request #
final results = await KoiRequestExecutor.executeBatch<Map<String, dynamic>>(
[
() => dio.get('/user/profile'),
() => dio.get('/user/permissions'),
() => dio.get('/dashboard'),
],
options: const BatchRequestOptions(
concurrent: true,
showLoading: true,
),
);
Retry at the application layer #
final criticalData = await KoiRequestExecutor.executeWithRetry<MyData>(
request: () => dio.get('/critical-data'),
fromJson: (json) => MyData.fromJson(json as Map<String, dynamic>),
maxRetries: 3,
delay: const Duration(seconds: 2),
);
Use the Mixin #
For controllers or business classes that repeatedly issue requests,
KoiNetworkRequestMixin provides a simpler API.
import 'package:dio/dio.dart';
import 'package:koi_network/koi_network.dart';
class UserController with KoiNetworkRequestMixin {
UserController(this._dio);
final Dio _dio;
Future<void> loadProfile() async {
await universalRequest<Map<String, dynamic>>(
request: () => _dio.get('/user/profile'),
onSuccess: (data) => print('Profile: $data'),
);
}
}
Common helpers:
universalRequestsilentRequestquickRequestbatchRequestretryRequest
Typed Response Support #
If your API layer already returns typed response wrappers, implement
KoiTypedResponse<T> and use KoiTypedRequestExecutor.
class BaseResult<T> implements KoiTypedResponse<T> {
BaseResult({
required this.code,
required this.message,
required this.data,
});
@override
final int? code;
@override
final String? message;
@override
final T? data;
@override
bool get isSuccess => code == 200 || code == 0;
}
final user = await KoiTypedRequestExecutor.execute<User>(
request: () => userApi.getProfile(),
);
Token Refresh #
koi_network supports two refresh paths:
- Proactive refresh before token expiration
- Reactive refresh after authentication failures
Recommended JWT setup:
- implement
KoiAuthAdapter - mix in
KoiJwtTokenMixin - use
KoiDioFactory.createTokenDio(null)insiderefresh() - add login and refresh endpoints to
tokenRefreshWhiteList
Example:
await KoiNetworkInitializer.initialize(
baseUrl: 'https://api.example.com',
enableProactiveTokenRefresh: true,
tokenRefreshWhiteList: ['/auth/login', '/auth/refresh'],
);
Multi-Module Support #
You can initialize more than one backend module in the same app.
await KoiNetworkInitializer.initialize(
baseUrl: 'https://api-common.example.com',
key: 'main',
);
await KoiNetworkInitializer.initialize(
baseUrl: 'https://api-orders.example.com',
key: 'orders',
);
final ordersDio =
KoiNetworkServiceManager.instance.getModuleDio('orders');
Using with Retrofit / Swagger #
koi_network is designed as infrastructure — it does not generate API clients.
For type-safe endpoint definitions, pair it with retrofit
and optionally a Swagger/OpenAPI code generator.
Recommended architecture #
Swagger/OpenAPI doc
↓ (code generation)
┌──────────────────────┐
│ API Client Layer │ ← LoginApi, OrderApi (Retrofit annotations)
│ ApiClient aggregate │ ← groups all APIs into one entry point
├──────────────────────┤
│ koi_network │ ← Dio management, interceptors, token refresh,
│ │ KoiTypedRequestExecutor
├──────────────────────┤
│ Dio │ ← HTTP transport
└──────────────────────┘
Step 1: Define a Retrofit API #
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
part 'user_api.g.dart';
@RestApi()
abstract class UserApi {
factory UserApi(Dio dio, {String? baseUrl}) = _UserApi;
@GET('/api/v1/user/profile')
Future<BaseResult<UserProfile>> getProfile();
@POST('/api/v1/user/update')
Future<BaseResult<bool>> updateProfile(@Body() UpdateProfileRequest req);
}
Step 2: Create an API client aggregate #
class MyApiClient {
MyApiClient(Dio dio)
: user = UserApi(dio),
order = OrderApi(dio);
final UserApi user;
final OrderApi order;
}
Step 3: Wire it up with koi_network #
final dio = KoiNetworkServiceManager.instance.mainDio;
final api = MyApiClient(dio);
// Use KoiTypedRequestExecutor for automatic error handling
final profile = await KoiTypedRequestExecutor.execute<UserProfile>(
request: () => api.user.getProfile(),
);
Swagger / OpenAPI code generation #
If your backend provides a Swagger doc, use a code generator to auto-create the Retrofit API classes and model files:
# Example with swagger_generator_flutter
dart run swagger_generator_flutter generate --all
flutter pub run build_runner build --delete-conflicting-outputs
Generated files typically output to:
lib/api/— Retrofit API interfaceslib/api_models/— Request/response model classes
Tip:
koi_networkstays completely decoupled from the generator. You can switch generators, hand-write APIs, or mix both — the Dio instance and request executors work the same way.
Main Public APIs #
KoiNetworkAdaptersKoiNetworkInitializerKoiNetworkServiceManagerKoiRequestExecutorKoiTypedRequestExecutorKoiNetworkRequestMixinKoiNetworkConfigKoiAuthAdapterKoiResponseParserKoiRequestEncoder
Documentation #
License #
MIT. See LICENSE.