FlutterGuard
Production infrastructure for Flutter applications.
FlutterGuard is an architecture-agnostic SDK for networking, authentication refresh, caching, offline work, diagnostics, feature flags, and a debug inspector. It does not require GetX, Bloc, Riverpod, Provider, or any other state-management package.
Overview
Use FlutterGuard when you want production client infrastructure without adopting a full application framework.
The package is one library with replaceable ports:
GuardTransportfor HTTPTokenStorefor tokensGuardKeyValueStorefor cache and the offline queueConnectivityAdapterfor online/offlineFeatureFlagProviderfor a future remote source
V1 ships a local feature-flag provider. Remote providers are an extension point, not an implementation.
Why FlutterGuard?
Most Flutter apps rebuild the same production machinery: retries, 401
refresh, cache policies, offline writes, and a way to see what the client
just did. FlutterGuard implements that machinery once, keeps it typed, and
demonstrates every option in example/.
Features
- HTTP client with typed responses and interceptors
- Exception hierarchy mapped from HTTP statuses
- Retry with backoff and jitter
- Single-flight token refresh
- In-flight request deduplication
- Request cancellation
- Memory and persistent cache, including stale-while-revalidate
- Offline queue, sync, and conflict resolution
- Page, offset, and cursor pagination
- Connectivity versus reachability
- Structured logging with redaction
- Diagnostics and performance metrics
- In-app inspector
- Local feature flags
- Environment presets
- App lifecycle history
- Debouncer, throttler, and
Result
Architecture
Application (setState, Riverpod, Bloc, GetX, …)
│
▼
FlutterGuard
│
├── GuardNetwork ── GuardTransport (package:http by default)
├── GuardAuth ──── TokenStore
├── GuardCache ─── GuardKeyValueStore
├── GuardOffline ─ queue + GuardSync
├── FeatureFlags ─ FeatureFlagProvider
└── GuardInspector (optional UI)
Implementation details live under lib/src/. Import
package:flutter_guard_sdk/flutter_guard_sdk.dart.
Installation
The pub.dev name is flutter_guard_sdk. The product API is still
FlutterGuard. flutter_guard could not be used because pub.dev considers
it too similar to the existing flutter_guards package.
dependencies:
flutter_guard_sdk: ^0.1.4
flutter pub get
Requirements
| Tool | Minimum |
|---|---|
| Flutter | 3.44.0 |
| Dart | 3.12.0 |
Platforms: Android, iOS, web, macOS, Windows, and Linux. The default
transport is package:http. Persistence is in-memory unless you inject a
store.
Quick Start
import 'package:flutter/widgets.dart';
import 'package:flutter_guard_sdk/flutter_guard_sdk.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlutterGuard.initialize(
FlutterGuardConfig(
baseUrl: 'https://api.example.com',
),
);
runApp(const MyApp());
}
Future<void> loadUser() async {
final response = await FlutterGuard.instance.network.get<Map<String, Object?>>(
'/users/123',
parser: (json) => Map<String, Object?>.from(json! as Map),
);
debugPrint('${response.data}');
}
Configuration
await FlutterGuard.initialize(
FlutterGuardConfig(
baseUrl: 'https://api.example.com',
network: NetworkConfig(
connectTimeout: Duration(seconds: 15),
receiveTimeout: Duration(seconds: 30),
retry: RetryConfig(
maxAttempts: 3,
initialDelay: Duration(milliseconds: 500),
maxDelay: Duration(seconds: 10),
exponentialBackoff: true,
jitter: true,
),
),
auth: AuthConfig(
accessTokenProvider: () async => readAccessToken(),
refreshToken: () async => refreshAccessToken(),
),
cache: CacheConfig(
enabled: true,
defaultTtl: Duration(minutes: 10),
),
offline: OfflineConfig(
enabled: true,
persistentQueue: true,
autoSync: true,
),
diagnostics: DiagnosticsConfig(enabled: true),
inspector: InspectorConfig(enabled: true),
),
);
EnvironmentConfig.applyPresets (default true) adjusts logging and the
inspector when you pass the default DiagnosticsConfig / InspectorConfig:
| Environment | Log level | Inspector |
|---|---|---|
| development | debug | enabled |
| staging | info | enabled |
| production | error | disabled |
Configuration reference
Every public option is also listed in ConfigCatalog and shown in the
example Configuration Playground.
| Option | Type | Default | Description |
|---|---|---|---|
| FlutterGuardConfig.baseUrl | String? | null | Root URL for relative paths |
| FlutterGuardConfig.network | NetworkConfig | defaults | HTTP settings |
| FlutterGuardConfig.auth | AuthConfig | defaults | Token attachment and refresh |
| FlutterGuardConfig.cache | CacheConfig | defaults | Cache settings |
| FlutterGuardConfig.offline | OfflineConfig | defaults | Queue and sync |
| FlutterGuardConfig.diagnostics | DiagnosticsConfig | preset | Logs and metrics |
| FlutterGuardConfig.inspector | InspectorConfig | preset | Debug inspector |
| FlutterGuardConfig.featureFlags | FeatureFlagConfig | defaults | Local flags |
| FlutterGuardConfig.environment | EnvironmentConfig | development | Presets and targeting |
| FlutterGuardConfig.transport | GuardTransport? | HttpGuardTransport | HTTP adapter |
| FlutterGuardConfig.store | GuardKeyValueStore? | MemoryKeyValueStore | Persistence port |
| FlutterGuardConfig.connectivityAdapter | ConnectivityAdapter? | ManualConnectivityAdapter | Connectivity source |
| NetworkConfig.connectTimeout | Duration | 15s | Connection timeout |
| NetworkConfig.receiveTimeout | Duration | 30s | Response timeout |
| NetworkConfig.sendTimeout | Duration | 30s | Upload timeout |
| NetworkConfig.retry | RetryConfig | defaults | Default retry policy |
| NetworkConfig.defaultHeaders | Map<String, String> | {} | Headers on every request |
| NetworkConfig.followRedirects | bool | true | Follow HTTP redirects |
| NetworkConfig.maxRedirects | int | 5 | Redirect cap |
| NetworkConfig.enableDefaultDeduplication | bool | false | Global in-flight sharing |
| NetworkConfig.userAgent | String | flutter_guard_sdk/0.1.4 | Default User-Agent |
| RetryConfig.maxAttempts | int | 3 | Attempts including the first |
| RetryConfig.initialDelay | Duration | 500ms | Base retry delay |
| RetryConfig.maxDelay | Duration | 10s | Backoff cap |
| RetryConfig.exponentialBackoff | bool | true | Double the delay |
| RetryConfig.jitter | bool | true | Randomize the delay |
| RetryConfig.retryableStatusCodes | Set | {408,429,500,502,503,504} | Retryable statuses |
| RetryConfig.retryNonIdempotent | bool | false | Allow POST/PATCH retries |
| AuthConfig.accessTokenProvider | Function? | null | Current access token |
| AuthConfig.refreshToken | Function? | null | Single-flight refresh |
| AuthConfig.tokenStore | TokenStore? | MemoryTokenStore | Token persistence |
| AuthConfig.refreshStatusCodes | Set | {401} | Statuses that refresh |
| AuthConfig.attachAuthorization | bool | true | Attach the token header |
| AuthConfig.authorizationHeader | String | Authorization | Header name |
| AuthConfig.authorizationPrefix | String | Bearer | Token prefix |
| AuthConfig.maxRefreshAttempts | int | 1 | Refresh attempts per request |
| CacheConfig.enabled | bool | true | Cache master switch |
| CacheConfig.defaultTtl | Duration | 10 minutes | Freshness window |
| CacheConfig.defaultPolicy | CachePolicy | networkFirst | Default policy |
| CacheConfig.maxMemoryEntries | int | 256 | LRU size |
| CacheConfig.persistent | bool | false | Write through the store |
| CacheConfig.persistKeyPrefix | String | flutter_guard.cache | Store prefix |
| OfflineConfig.enabled | bool | true | Queue master switch |
| OfflineConfig.persistentQueue | bool | true | Persist the queue |
| OfflineConfig.autoSync | bool | true | Sync when back online |
| OfflineConfig.maxQueueSize | int | 500 | Queue cap |
| OfflineConfig.defaultPriority | int | 0 | Default priority |
| OfflineConfig.queueMutationsByDefault | bool | true | Queue writes while offline |
| OfflineConfig.persistKey | String | flutter_guard.offline.queue | Store key |
| DiagnosticsConfig.enabled | bool | true | Collect logs/metrics |
| DiagnosticsConfig.logLevel | LogLevel | info (debug in dev) | Minimum severity |
| DiagnosticsConfig.maxLogEntries | int | 500 | Ring-buffer size |
| DiagnosticsConfig.redactSensitiveFields | bool | true | Redact secrets |
| DiagnosticsConfig.extraRedactedKeys | List | [] | Extra secret keys |
| DiagnosticsConfig.collectMetrics | bool | true | Record timings |
| DiagnosticsConfig.slowRequestThreshold | Duration | 1000ms | Slow-request bar |
| InspectorConfig.enabled | bool | true (false in prod) | Allow inspector UI |
| InspectorConfig.allowInRelease | bool | false | Allow release inspector |
| FeatureFlagConfig.enabled | bool | true | Evaluate flags |
| FeatureFlagConfig.flags | Map<String, FeatureFlag> | {} | Seed flags |
| FeatureFlagConfig.userId | String? | null | Rollout identity |
| FeatureFlagConfig.appVersion | String? | null | minVersion targeting |
| FeatureFlagConfig.provider | FeatureFlagProvider? | local | Flag source |
| EnvironmentConfig.environment | GuardEnvironment | development | Current environment |
| EnvironmentConfig.applyPresets | bool | true | Apply logging/inspector presets |
Dartdoc on each field also records purpose, allowed values, a production note, and common mistakes.
Network
Basic
final response = await guard.network.get<User>(
'/users/123',
parser: User.fromJson,
);
Advanced
final response = await guard.network.send<Order>(
'POST',
'/orders',
data: order.toJson(),
headers: {'x-request-source': 'checkout'},
parser: Order.fromJson,
timeout: const Duration(seconds: 20),
);
HTTP methods
get, post, put, patch, delete, head, send, upload, and
download.
Typed responses
Pass parser to map JSON to your type. ApiResponse<T> includes data,
statusCode, headers, requestId, duration, fromCache, isStale,
retryCount, timestamp, url, method, and responseSize.
Configuration
See NetworkConfig above. Per-request overrides: timeout, headers,
query, deduplicate, cachePolicy, retry, cancelToken.
Expected behavior
Statuses >= 400 throw a FlutterGuardException subclass. Successful
bodies are parsed and returned.
Common mistake
Putting Authorization in NetworkConfig.defaultHeaders instead of
AuthConfig.
Example application
example/lib/features/network/network_page.dart
Error Handling
FlutterGuardException
├── NetworkException
├── RequestTimeoutException
├── UnauthorizedException
├── ForbiddenException
├── NotFoundException
├── ValidationException
├── RateLimitException
├── ServerException
├── CancelledException
├── CacheException
├── OfflineException
└── UnknownException
RequestTimeoutException is named to avoid colliding with dart:async
TimeoutException.
Each type documents when it occurs, its properties, how to handle it, and
whether FlutterGuard retries it. See
lib/src/core/exceptions/exceptions.dart.
The example Network screen triggers 401, 403, 404, 422, 429, and 500 through the local demo transport.
Retry
Basic
RetryConfig(maxAttempts: 3)
Advanced
RetryConfig(
maxAttempts: 3,
initialDelay: Duration(milliseconds: 500),
maxDelay: Duration(seconds: 10),
exponentialBackoff: true,
jitter: true,
retryableStatusCodes: {503, 504},
)
GET, HEAD, PUT, and DELETE may retry. POST and PATCH do not, unless
retryNonIdempotent: true or the call passes retry: true.
Example application
example/lib/features/retry/retry_page.dart
Authentication
Basic
AuthConfig(
accessTokenProvider: () async => storage.readAccess(),
refreshToken: () async => api.refresh(),
)
Token refresh
If 100 in-flight requests receive 401, FlutterGuard starts one refresh. When it succeeds, the waiting requests retry once with the new token.
Do not store a captured bearer token on the offline queue. Authorization is applied again at send time.
Common mistake
Adding 403 to refreshStatusCodes. 403 is authorization, not expiry.
Example application
example/lib/features/authentication/auth_page.dart
Request Deduplication
await guard.network.get('/dashboard', deduplicate: true);
Identical in-flight calls share one transport request. Authorization, Cookie, and request-id headers are ignored when building the key.
Example application
example/lib/features/deduplication/dedup_page.dart
Request Cancellation
final token = CancellationToken();
final future = guard.network.get('/reports', cancelToken: token);
token.cancel();
A cancelled request throws CancelledException and is not retried.
Example application
example/lib/features/cancellation/cancel_page.dart
Caching
Policies: networkOnly, cacheOnly, cacheFirst, networkFirst,
staleWhileRevalidate.
Stale while revalidate
get returns the cached value immediately, including a stale entry, and
refreshes in the background. Use onRevalidate to rebuild UI with the
fresh ApiResponse.
final first = await guard.network.get<Catalog>(
'/catalog',
cachePolicy: CachePolicy.staleWhileRevalidate,
parser: Catalog.fromJson,
onRevalidate: (fresh) => setState(() => catalog = fresh.data),
);
Cache invalidation
await guard.cache.invalidate('/products');
await guard.cache.invalidateTag('products');
await guard.cache.clear();
Common mistake
Using cacheOnly as the global default. The first launch will miss.
Example application
example/lib/features/cache/cache_page.dart
Connectivity
ConnectivityStatus is unknown, offline, or online.
This is not internet reachability. A device can be online on a captive
portal and still fail to reach your API. Call
guard.connectivity.checkReachability(uri: ...) for a real probe.
FlutterGuard does not invent radio quality. The inspector can show measured request latency after samples exist.
Example application
example/lib/features/connectivity/connectivity_page.dart
Offline Queue
Offline POST/PUT/PATCH/DELETE are queued when
OfflineConfig.queueMutationsByDefault is true. GET is not queued unless
you pass queueIfOffline: true.
OfflineException.queued is true when the request was stored. Sync drains
pending entries when connectivity returns if autoSync is true.
Conflict resolution
On HTTP 409 the sync engine applies ConflictStrategy:
serverWins— keep the server representationclientWins— resend the client payloadlastWriteWins— compare timestampscustom— call yourConflictResolvermanual— leaveneedsResolution
The example conflict screen executes the resolver. It does not print a fake success message.
Example application
example/lib/features/offline/
Pagination
final paginator = Paginator<Item>.page(
fetch: (page, pageSize) => api.page(page, pageSize),
);
await paginator.load();
await paginator.loadMore(); // ignored if a loadMore is already running
Also Paginator.offset and Paginator.cursor.
Example application
example/lib/features/pagination/pagination_page.dart
Diagnostics
guard.diagnostics exposes logger and metrics.
diagnostics.reset() clears both. Intended for debug builds.
Logging
guard.logger.info('Order submitted', metadata: {'orderId': order.id});
Levels: debug, info, warning, error, critical.
Log redaction
Values for Authorization, Cookie, password, token, accessToken,
refreshToken, creditCard, secret, apiKey, and common snake_case
aliases are replaced with ******** before they are stored.
Redaction exists because logs and the inspector are easy to screenshot, share, and persist. Never disable it to "debug auth" in a shared build.
Performance metrics
Recorded when available: request duration, retry count, cache hit/miss, queue depth, and response size. Unsupported metrics are not reported.
Example application
example/lib/features/diagnostics/diagnostics_page.dart
Debug Inspector
await GuardInspector.show(context);
Sections: Overview, Network, Requests, Errors, Logs, Cache, Offline Queue, Performance, Feature Flags, Environment, Device Information.
The inspector is a no-op when disabled or when the build is release and
allowInRelease is false.
Example application
Open it from the dashboard action or the diagnostics screen.
Feature Flags
if (guard.flags.isEnabled('new_checkout')) {
return const NewCheckout();
}
Types: boolean, string, integer, double, JSON. Targeting: rollout percentage, platform, app version, environment. The V1 provider is local.
Example application
example/lib/features/feature_flags/flags_page.dart
Environment Configuration
FlutterGuardConfig(
environment: EnvironmentConfig(
environment: GuardEnvironment.production,
),
);
The example Environment screen shows the live process and the development versus production presets.
App Lifecycle
guard.lifecycle records resumed, inactive, paused, detached, and
hidden when a widgets binding exists.
Example application
example/lib/features/lifecycle/lifecycle_page.dart
Debouncer
final debouncer = Debouncer(duration: Duration(milliseconds: 300));
onChanged: (text) {
debouncer.run(() {
unawaited(guard.network.get('/search', query: {'q': text}));
});
};
Example application
example/lib/features/utils/debounce_page.dart
Throttler
Leading-edge: the first call in a window runs, later calls are ignored.
Example application
example/lib/features/utils/throttle_page.dart
Result Type
final result = await guard.network.getResult<User>(
'/users/123',
parser: User.fromJson,
);
result.when(
success: (response) {},
failure: (error) {},
);
Use Result when both outcomes are routine at an application boundary. Use
exceptions when the caller is not expected to recover locally. get throws.
getResult does not throw FlutterGuardException.
Complete Example Application
cd example
flutter pub get
flutter run
The example uses DemoTransport. It does not need a production API.
Testing
flutter test
flutter analyze
cd example && flutter test
Critical automated cases:
- 100 requests + expired token → 1 refresh, 100 successes
- 100 identical deduplicated requests → 1 transport call
- offline, queue 10, restore, sync → 10 sends
Production Configuration
FlutterGuardConfig(
baseUrl: const String.fromEnvironment('API_BASE_URL'),
environment: const EnvironmentConfig(
environment: GuardEnvironment.production,
),
auth: AuthConfig(
accessTokenProvider: secureStore.readAccess,
refreshToken: refreshSession,
tokenStore: SecureTokenStore(),
),
inspector: const InspectorConfig(enabled: false),
diagnostics: const DiagnosticsConfig(
logLevel: LogLevel.error,
redactSensitiveFields: true,
),
);
Never put production secrets in source.
Security
- Tokens belong in a
TokenStoreyou control, not in source. - Logs redact known secret keys.
- The inspector is off in production presets.
- Cached and queued bodies can contain personal data. Inject an encrypted store if you persist them.
- API keys should come from
--dart-defineor a secrets manager.
Performance
Debug collectors are bounded by maxLogEntries. The inspector is built
only when opened. Dedup entries are removed in whenComplete. Cache is
LRU. Retry timers stop when a request is cancelled.
Troubleshooting
401 after token refresh
Cause: The refresh handler returned the same expired token, or
accessTokenProvider still reads the old value.
Solution: Persist the new token before the refresh callback returns.
Make accessTokenProvider read that store.
Debug: Inspect guard.auth.refreshCount and the Authorization header
in the inspector (it will be redacted, but you can see that it changed
length only if you log a hash yourself).
Infinite retry loop
Cause: maxAttempts is high and retryableStatusCodes includes a
permanent failure, or retryNonIdempotent is true on a failing POST.
Solution: Keep maxAttempts at 2–3. Do not retry 4xx other than 408
and 429.
Offline queue not syncing
Cause: autoSync is false, or the adapter still reports offline.
Solution: Call guard.offline.sync() or set the adapter to online.
Confirm with await guard.connectivity.refresh().
Cache not updating
Cause: cacheFirst is serving a fresh TTL, or enabled is false.
Solution: Invalidate the URL or tag, or use networkFirst /
staleWhileRevalidate.
Inspector not opening
Cause: Production preset, or release mode without allowInRelease.
Solution: Use a development environment or pass
InspectorConfig(enabled: true) explicitly in a debug build.
Request cancellation
Cause: The transport ignored the token.
Solution: Custom GuardTransport implementations must honor
request.cancelToken.
Connectivity says online but the API fails
Cause: Connectivity is not reachability.
Solution: Call checkReachability and inspect the real HTTP error.
Duplicate requests
Cause: Deduplication is opt-in.
Solution: Pass deduplicate: true or enable
enableDefaultDeduplication for safe GETs only.
Persistent storage errors
Cause: The injected store threw, or persisted JSON is corrupt.
Solution: FlutterGuard deletes corrupt cache rows. Replace the store if writes fail.
Platform-specific issues
Cause: File stores and dart:io are not available on web.
Solution: Inject a web-safe GuardKeyValueStore. Memory works everywhere.
Release build behavior
Cause: Inspector and debug logging are intentionally reduced.
Solution: This is expected. Do not enable allowInRelease to inspect
production users.
FAQ
Does FlutterGuard require Dio? No. It uses package:http and a
GuardTransport port.
Does FlutterGuard require GetX or Bloc? No.
Can I use Riverpod or Provider? Yes. FlutterGuard is not a state-management library.
Can I use my own HTTP client? Yes. Implement GuardTransport.
Can I disable caching, the offline queue, or diagnostics? Yes. Set
enabled: false on the corresponding config.
Can I use FlutterGuard in production? Yes. Use the production environment preset, a secure token store, and keep redaction enabled.
Does it support Web, iOS, and Android? Yes.
How does token refresh work? One shared refresh, then one retry of the original request.
How is sensitive data protected? Default redaction, memory token store, inspector disabled in production presets.
Migration Guide
See MIGRATION.md. This is the initial release.
API Reference
Import package:flutter_guard_sdk/flutter_guard_sdk.dart. Dartdoc is the source of
truth for parameters, returns, and exceptions.
Architecture Extension
Future packages can implement the existing ports:
flutter_guard_formsflutter_guard_firebaseflutter_guard_sentryflutter_guard_cloudflutter_guard_devtools
They are not implemented in this repository.
Roadmap
- Optional secure-storage adapter package
- Optional
connectivity_plusadapter package - Remote feature-flag provider package
Documentation
Contributing
See CONTRIBUTING.md.
Security Policy
See SECURITY.md.
License
MIT. See LICENSE.
Support
If FlutterGuard saved you time, you can buy me a chai.
Phones open a UPI app. Desktops show a QR to scan.
Developer
Kishan Dobariya
- Phone: +91 90232 56218
- Email: flutterdeveloper2206@gmail.com
- LinkedIn: kishan-dobariya-99b005217
- GitHub: Kishandobariya76
Example Application Index
Example Application
├── Network example/lib/features/network/network_page.dart
├── Authentication example/lib/features/authentication/auth_page.dart
├── Retry example/lib/features/retry/retry_page.dart
├── Deduplication example/lib/features/deduplication/dedup_page.dart
├── Cancellation example/lib/features/cancellation/cancel_page.dart
├── Cache example/lib/features/cache/cache_page.dart
├── Offline Queue example/lib/features/offline/offline_queue_page.dart
├── Synchronization example/lib/features/offline/sync_page.dart
├── Conflict Resolution example/lib/features/offline/conflict_page.dart
├── Pagination example/lib/features/pagination/pagination_page.dart
├── Connectivity example/lib/features/connectivity/connectivity_page.dart
├── Diagnostics example/lib/features/diagnostics/diagnostics_page.dart
├── Logging example/lib/features/diagnostics/diagnostics_page.dart
├── Performance inspector Performance section
├── Inspector GuardInspector.show
├── Feature Flags example/lib/features/feature_flags/flags_page.dart
├── Environment example/lib/features/environment/environment_page.dart
├── Lifecycle example/lib/features/lifecycle/lifecycle_page.dart
├── Debounce example/lib/features/utils/debounce_page.dart
├── Throttle example/lib/features/utils/throttle_page.dart
└── Configuration example/lib/features/playground/playground_page.dart
Libraries
- flutter_guard_sdk
- Production infrastructure for Flutter applications.