kickin_network 0.0.2+1
kickin_network: ^0.0.2+1 copied to clipboard
Kickin network provides utilities to handle network efficiently and reliably
kickin_network #
A Flutter package for making HTTP requests cleanly and reliably. It is built on top of Dio, with typed responses, optional caching, and internet connectivity monitoring baked in.
Part of the Kickin toolkit for Flutter.
Installation #
flutter pub add kickin_network
Or add it manually to your pubspec.yaml:
dependencies:
kickin_network: ^0.0.2+1
How it works #
The package gives you three things to work with:
- An API hub (
KRestApiBase): one class per app that holds your base URL, Dio config, and interceptors. - Feature API clients (
KRestApi): one class per area of your app (e.g.UsersApi,ChatsApi). Each client is owned by the hub. - Requests (
KGetRequest,KPostRequest, etc.): declared once on your client, then called with.send()or.catchErrorOnSend()wherever you need them.
Every response comes back as ApiResult<T>, which holds either a typed value or an error ending with no uncaught exceptions.
Quick start #
Step 1 — Create your API hub #
import 'package:kickin_network/kickin_network.dart';
class Api extends KRestApiBase {
Api._();
static final instance = Api._();
// Feature clients live here
late final users = UsersApi(this);
Future<void> init() async {
await super.intialize(
baseUrl: 'https://api.yourapp.com',
);
}
}
Step 2 — Create a feature client #
class UsersApi extends KRestApi {
UsersApi(super.parent);
// Declare your requests once
late final getProfile = KGetRequest(
this,
path: '/users/me',
decoder: (data, _) => UserModel.fromMap(data),
);
}
Step 3 — Call it anywhere #
await Api.instance.init();
final result = await Api.instance.users.getProfile.catchErrorOnSendResult();
if (result.isSuccess) {
print(result.value); // UserModel
} else {
print(result.error); // human-readable error string
}
That's the core loop. Everything else is opt-in.
Sending requests #
Each request type has a few ways to execute it. Pick the one that fits your use case:
| Method | Returns | Throws on error? |
|---|---|---|
.send() |
TDecoded? |
Yes |
.catchErrorOnSend() |
TDecoded? |
No, returns null |
.sendResponse() |
KResponse<Raw, TDecoded> |
Yes |
.catchErrorOnSendResponse() |
KResponse<Raw, TDecoded> |
No |
.sendResult() |
ApiResult<TDecoded> |
Yes |
.catchErrorOnSendResult() |
ApiResult<TDecoded> |
No, recommended |
Recommendation: Use catchErrorOnSendResult() in most cases. It never throws and gives you both value and error in one object.
Request types #
| Class | HTTP method |
|---|---|
KGetRequest |
GET |
KPostRequest |
POST |
KPutRequest |
PUT |
KPatchRequest |
PATCH |
KDeleteRequest |
DELETE |
KDownloadRequest |
GET (file download) |
Cloning requests for dynamic paths #
Use .copyWith() to create a modified copy of any request; useful for dynamic routes:
// Base request defined on the client
late final getUserById = KGetRequest(this, path: '/users/me', ...);
// Clone it with a different path at call time
final result = await getUserById
.copyWith(pathTransform: (p) => '/users/$userId')
.catchErrorOnSendResult();
Sending data (POST / PATCH / PUT) #
late final updateBio = KPatchRequest(
this,
path: '/users/me',
decoder: (_, res) => res.statusCode == 200,
);
// At call time, attach the body
final result = await updateBio
.copyWith(data: {'bio': 'Hello!'}, headers: addAuthHeader())
.catchErrorOnSendResult();
Responses #
ApiResult<T> #
The cleanest response type (check isSuccess to see if it was successful):
final result = await api.users.getProfile.catchErrorOnSendResult();
if (result.isSuccess) {
print(result.value); // your decoded model
} else {
print(result.error); // error message string
}
KResponse<Raw, Formatted> #
Extends Dio's Response, so you can access raw HTTP details too:
final response = await api.users.getProfile.catchErrorOnSendResponse();
print(response.statusCode);
print(response.raw); // raw payload
print(response.value); // decoded model
print(response.error); // error if any
Interceptors & global error handling #
Add global interceptors (e.g. token refresh) on your hub:
Api.instance.primaryInterceptors.add(
TokenRefreshInterceptor(onSessionInvalid: () async { /* logout */ }),
);
Override globalErrorOverride to customise how errors are parsed:
class Api extends KRestApiBase {
@override
String? globalErrorOverride(Response response, Object? error, [StackTrace? st]) {
final result = super.globalErrorOverride(response, error, st);
if (result is List) return result.take(3).join(', ');
return result?.toString();
}
}
The default implementation already handles common HTTP status codes (400, 401, 403, 404, 500, etc.) with readable messages.
Logging #
Control what gets logged during development via LogOptions:
await super.intialize(
baseUrl: '...',
logOptions: LogOptions(
parts: {LogPart.requestBody, LogPart.responseBody, LogPart.errors},
maxLogLength: 2000,
),
);
Preset shortcuts:
| Preset | What it logs |
|---|---|
LogOptions.debugAll() |
Everything |
LogOptions.debugRequest() |
Request only (query, headers, body) |
LogOptions.debugResponse() |
Response only (headers, body, errors) |
LogOptions.normal() |
Query, request headers/body, response body |
LogOptions.none() |
Nothing |
Logging only runs in debug mode.
Optional: Caching — KApiCacheMixin #
Add in-memory caching to your hub with a mixin. Optionally sync it to disk.
class Api extends KRestApiBase with KApiCacheMixin {
static final instance = Api._();
Api._();
}
Enable disk persistence (uses Hive via kickin_storage):
await Api.instance.intialize(
baseUrl: '...',
syncCacheToStorage: true,
cacheBoxName: 'my_app_cache', // optional
);
Use cache inside a feature client:
class UsersApi extends KRestApi<UserModel> {
UsersApi(super.parent);
Future<UserModel?> getCachedProfile() async {
if (cache != null) return cache; // return cached value
final result = await getProfile.catchErrorOnSend();
if (result != null) setCache(result);
return result;
}
}
- Each client gets its own cache slot meaning no conflicts between clients.
- Disk writes are batched with a 300 ms debounce to avoid excessive I/O.
- On app restart, the cache is restored from disk before any request fires.
Clean up:
Api.instance.disposeCache(); // cancel pending writes
Optional: Internet monitoring — KApiMonitorMixin #
Requires platform setup: see Platform setup below.
Add real-time connectivity monitoring to your hub:
class Api extends KRestApiBase with KApiCacheMixin, KApiMonitorMixin {
static final instance = Api._();
Api._();
}
// Start/stop monitoring globally
Api.instance.startMonitoring();
Api.instance.stopMonitoring();
// React to connectivity changes
void _onStatusChange(InternetStatus status) {
if (status == InternetStatus.disconnected) showOfflineBanner();
}
Api.instance.addListener(_onStatusChange);
// Remove when done (e.g. in dispose)
Api.instance.removeListener(_onStatusChange);
- Listeners are paused (not cancelled) when
stopMonitoring()is called so they resume cleanly. - Call
disposeMonitor()to clean up all subscriptions when the app shuts down.
@override
void dispose() {
Api.instance.disposeCache();
Api.instance.disposeMonitor();
}
Platform setup #
Required only when using KApiMonitorMixin.
Android #
Add to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
macOS #
Add to both macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements:
<key>com.apple.security.network.client</key>
<true/>
Full example #
import 'package:kickin_network/kickin_network.dart';
// ── Hub ──────────────────────────────────────────────────────────────
class Api extends KRestApiBase with KApiCacheMixin {
Api._();
static final instance = Api._();
late final users = UsersApi(this);
Future<void> init() async {
await super.intialize(
baseUrl: 'https://api.myapp.com',
syncCacheToStorage: true,
logOptions: LogOptions.debugAll(),
);
primaryInterceptors.add(
TokenRefreshInterceptor(onSessionInvalid: () async {}),
);
}
}
// ── Feature client ───────────────────────────────────────────────────
class UsersApi extends KRestApi<UserModel> {
UsersApi(super.parent);
late final getProfile = KGetRequest(
this,
path: '/users/me',
decoder: (data, _) => UserModel.fromMap(data['data']),
);
late final updateFcmToken = KPatchRequest(
this,
path: '/users/me/fcm-token',
decoder: (_, res) => res.statusCode == 200,
);
}
// ── Extensions (keep call sites clean) ───────────────────────────────
extension UsersApiExt on UsersApi {
Map<String, String> _authHeader() => {'Authorization': 'Bearer $yourToken'};
Future<ApiResult<UserModel?>> fetchProfile() =>
getProfile.copyWith(headers: _authHeader()).catchErrorOnSendResult();
Future<ApiResult<bool?>> pushFcmToken(String token) =>
updateFcmToken
.copyWith(headers: _authHeader(), data: {'fcmToken': token})
.catchErrorOnSendResult();
}
// ── Usage ─────────────────────────────────────────────────────────────
Future<void> main() async {
await Api.instance.init();
final result = await Api.instance.users.fetchProfile();
if (result.isSuccess) {
print('Hello, ${result.value!.name}');
} else {
print('Error: ${result.error}');
}
}
API reference summary #
| Class / Mixin | Purpose |
|---|---|
KRestApiBase |
Base class for your app's API hub |
KApiCacheMixin |
Adds in-memory + optional disk caching to the hub |
KApiMonitorMixin |
Adds internet connectivity monitoring to the hub |
KRestApi<T> |
Base class for feature API clients |
KRestRequest<T> |
Base class for all request wrappers |
KGetRequest |
HTTP GET |
KPostRequest |
HTTP POST |
KPutRequest |
HTTP PUT |
KPatchRequest |
HTTP PATCH |
KDeleteRequest |
HTTP DELETE |
KDownloadRequest |
File download |
KResponse<Raw, Formatted> |
Full Dio response + typed decoded value |
ApiResult<T> |
Lightweight result type: value or error |
LogOptions |
Configure what gets logged and how |
WebSocket and GraphQL support are planned for future releases.