vps_utilities 1.3.1
vps_utilities: ^1.3.1 copied to clipboard
A multiplatform utilities package for VPS Citizen Mobile application. Supports iOS, Android, Web, Windows, macOS, and Linux. Provides network layer, secure storage, token management, registry, form en [...]
vps_utilities #
A multiplatform utilities package for VPS Citizen Mobile application. It runs on all Flutter-supported platforms from a single codebase.
Supported platforms: iOS, Android, Web, Windows, macOS, Linux.
Author: Levis Nyingi
License: Private / proprietary. This software is for use only in ISE Intelligent Security Ecosystem software. See LICENSE.
Provides a network layer (HTTP client with Dio), base URL config, secure token storage, flexible secure key-value storage (drop-in replacement for an app-level secure storage data source), Cubit state management for tokens, registry (shared models and data source for officer/citizen registry, stations, and officers), form engine (configurable forms with paging, validation, optional AI narrative, and context.pushForm), cache, device info, and responsive layout plus device type detection for platform-aware UI, using Clean Architecture and Kiwi for dependency injection. Storage uses flutter_secure_storage (encrypted where supported, persists across restarts). No Material UI—works on mobile, desktop, and web.
Full documentation #
Complete documentation for every area of the package is in the docs folder:
| Topic | Doc |
|---|---|
| Initialization, core, DI | docs/initialization.md |
| Network layer, API errors, guardedApiCall | docs/network.md |
| Token API, device token, TokenCubit | docs/token.md |
| Secure storage (flexible key-value) | docs/secure_storage.md |
| Cache (CacheManager) | docs/cache.md |
| Device info (DeviceInfoProvider) | docs/device_info.md |
| Responsive layout, device type | docs/responsive.md |
| Registry (shared models, RegistryDataSource) | docs/registry.md |
| Form engine (VpsFormEngine, pushForm) | docs/form_engine.md |
| Narrative (AI narrative service) | docs/narrative.md |
→ docs/README.md – Index of all documentation.
Installation #
From a local path (same repo) #
Add this package to your pubspec.yaml:
dependencies:
vps_utilities:
path: flutter packages/vps_utilities
From GitHub #
If the package is stored in GitHub, depend on it via git:
dependencies:
vps_utilities:
git:
url: https://github.com/YOUR_ORG_OR_USER/vps_utilities.git
ref: main # or master, or a tag e.g. ref: v1.1.0
For a private repo, use SSH or a personal access token in the URL. Then run:
flutter pub get
Platform notes #
The package is written in Dart/Flutter and does not depend on platform-specific APIs directly. Secure token storage uses flutter_secure_storage, which has platform-specific implementations (e.g. Keychain on iOS, Keystore on Android, in-memory or alternatives on Web). The same API works on all platforms; behavior and backing store may differ per platform.
Initialization #
Important: You must initialize the package with a base URL before using any functionality.
Initialize the package in your main() function before running your app:
import 'package:flutter/material.dart';
import 'package:vps_utilities/vps_utilities.dart';
void main() {
VpsUtilities.initialize(
baseUrl: 'https://api.example.com/api/v1/',
);
runApp(MyApp());
}
Network layer #
Resolve and use the HTTP client via Kiwi:
final networkService = VpsUtilities.resolve<NetworkService>();
// GET
final response = await networkService.getHttp('/users', tokenRequired: true);
// POST
final response = await networkService.postHttp(
'/login',
body: {'email': email, 'password': password},
tokenRequired: false,
);
Methods: getHttp, postHttp, putHttp, deleteHttp. Each accepts endpoint, optional params, headers, body (where applicable), tokenRequired, and optional per-request token.
Response shape: On success the map has data. On failure it has error (an [ApiErrors] value) and message. Use response['error'] as ApiErrors for handling (e.g. 401 → unauthenticated, 404 → notFound).
Token API #
Set #
Store the auth token securely (e.g. after login). Pass null to clear (e.g. on logout). Stored with flutter_secure_storage (encrypted, persists across restarts).
await VpsUtilities.setAuthToken(response['data']['token']);
// Clear on logout
await VpsUtilities.setAuthToken(null);
Device token (Device-Token header) #
In addition to the auth token, the package supports a device registration token that is sent as a Device-Token HTTP header on all requests when present. This is intended for device onboarding flows shared across apps (e.g. Officer, Citizen, others).
Set / clear device token
// After successful device registration
await VpsUtilities.setDeviceToken(deviceTokenFromBackend);
// Clear when needed (e.g. logout, reset)
await VpsUtilities.setDeviceToken(null);
Read device token (optional)
final deviceToken = await VpsUtilities.getDeviceToken();
How it is used in requests
The internal RestClient reads the stored device token from secure storage and, if non-empty, adds:
Device-Token: <device_registration_token>
to every outgoing request. If no token is stored, the header is omitted and behavior is unchanged.
Use #
When tokenRequired: true, the package adds the Authorization: Bearer <token> header automatically. It uses a per-request token if you pass one; otherwise it uses the stored token.
Per-request token #
Each network method accepts an optional token parameter. If provided, that token is used for that request only (even if no token was set globally). Use this for one-off or login requests.
final networkService = VpsUtilities.resolve<NetworkService>();
// One-off request with a specific token
await networkService.getHttp(
'/profile',
tokenRequired: true,
token: someToken,
);
See #
Read the stored auth token when needed. Sensitive: use only when necessary (e.g. debug or passing to another layer). Do not log or display in production.
final token = await VpsUtilities.getAuthToken();
Secure storage (flexible key-value) #
The package provides flexible secure storage for any key-value data (tokens, user data, etc.). It uses the same encrypted backend as the auth token (Android EncryptedSharedPreferences, iOS Keychain, etc.) and can replace an app-level secure storage data source.
Requirements: Call VpsUtilities.initialize(baseUrl: ...) first.
API (static) #
| Method | Description |
|---|---|
VpsUtilities.setSecureData(key, value) |
Store value under key. Pass null or empty to remove the key. |
VpsUtilities.getSecureData(key) |
Returns Future<String?> — the value for key, or null if not found. |
VpsUtilities.removeSecureData(key) |
Removes the value for key. |
VpsUtilities.clearAllSecureData() |
Removes all keys. Use with care (e.g. on logout). |
VpsUtilities.containsSecureKey(key) |
Returns Future<bool> — whether key exists. |
Examples #
// Store sensitive data
await VpsUtilities.setSecureData('user', jsonEncode(userJson));
// Read
final userJson = await VpsUtilities.getSecureData('user');
// Check existence
final hasUser = await VpsUtilities.containsSecureKey('user');
// Remove one key
await VpsUtilities.removeSecureData('user');
// Clear all (e.g. logout)
await VpsUtilities.clearAllSecureData();
Resolving SecureStorage #
For dependency injection or multiple operations, resolve the [SecureStorage] instance:
final storage = VpsUtilities.resolve<SecureStorage>();
await storage.write('key', 'value');
final value = await storage.read('key');
await storage.delete('key');
await storage.deleteAll();
final exists = await storage.containsKey('key');
Replacing an app-level secure storage #
If your app has a SecureStorageDataSource (or similar) that uses flutter_secure_storage with storeSecureData / getSecureData / removeSecureData / clearAllSecureData / containsKey, you can route those calls to VpsUtilities.setSecureData, VpsUtilities.getSecureData, etc., after initializing the package. The auth token is already handled via Token API; use the flexible API for other sensitive keys (e.g. user profile, IPRS data).
Token usage examples #
After login:
await VpsUtilities.setAuthToken(response['data']['token']);
One-off request with a different token:
final networkService = VpsUtilities.resolve<NetworkService>();
await networkService.getHttp(
url,
tokenRequired: true,
token: oneOffToken,
);
Accessing the base URL:
String baseUrl = VpsUtilities.baseUrl;
// Or via domain contract
BaseUrlConfigRepository config = VpsUtilities.resolve<BaseUrlConfigRepository>();
String baseUrl = config.baseUrl;
State management (Cubit) #
The package provides [TokenCubit] for token state. It syncs with the secure token store and emits [TokenState] (e.g. hasToken, optional token). Use [BlocProvider] and [BlocBuilder] / [BlocListener] in your app.
Provide the cubit:
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:vps_utilities/vps_utilities.dart';
// After VpsUtilities.initialize(...)
BlocProvider<TokenCubit>(
create: (_) => VpsUtilities.resolve<TokenCubit>(),
child: MyApp(),
)
React to token state:
BlocBuilder<TokenCubit, TokenState>(
builder: (context, state) {
if (state.hasToken) {
return HomeScreen();
}
return LoginScreen();
},
)
Update token (login / logout):
// After login
context.read<TokenCubit>().setToken(response['data']['token']);
// Logout
await context.read<TokenCubit>().clearToken();
Calling [VpsUtilities.setAuthToken] also updates [TokenCubit] state so the UI stays in sync.
Responsive layout and device type #
The package provides Tailwind-style breakpoints, device/platform detection, and layout helpers so you can build responsive, platform-aware UI in one place.
Device type (no BuildContext) #
Use [VpsResponsive] for quick checks. It uses the registered [DeviceTypeProvider] when [VpsUtilities] is initialized; otherwise it falls back to [DeviceTypeProviderImpl], so device type works with or without DI.
import 'package:vps_utilities/vps_utilities.dart';
// Quick checks (no initialization required for fallback)
if (VpsResponsive.isWeb) { /* web-only UI */ }
if (VpsResponsive.isAndroid) { /* Android-specific */ }
if (VpsResponsive.isIOS) { /* iOS-specific */ }
if (VpsResponsive.isMobileOS) { /* Android or iOS */ }
// Full enum (e.g. for switch or logging)
DeviceType type = VpsResponsive.deviceType;
Resolve via DI (after [VpsUtilities.initialize]):
final provider = VpsUtilities.resolve<DeviceTypeProvider>();
DeviceType type = provider.current;
Responsive layout (with BuildContext) #
Use [ResponsiveLayout] with [MediaQuery] and Tailwind breakpoints (sm 640, md 768, lg 1024, xl 1280, 2xl 1536). All methods take [BuildContext].
Breakpoints (mobile-first, “at least”):
if (ResponsiveLayout.isSm(context)) { /* width >= 640 */ }
if (ResponsiveLayout.isMd(context)) { /* width >= 768 */ }
if (ResponsiveLayout.isLg(context)) { /* width >= 1024 */ }
if (ResponsiveLayout.isXl(context)) { /* width >= 1280 */ }
if (ResponsiveLayout.is2xl(context)) { /* width >= 1536 */ }
Orientation:
if (ResponsiveLayout.isPortrait(context)) { /* portrait */ }
if (ResponsiveLayout.isLandscape(context)) { /* landscape */ }
Layout helpers:
// Responsive padding (16 below md, 24 at md–lg, 32 at lg+)
padding: ResponsiveLayout.getResponsivePadding(context),
// Max content width for centered layouts (infinity / 900 / 1200 by breakpoint)
maxWidth: ResponsiveLayout.getMaxContentWidth(context),
// Center content with max width when at least md
ResponsiveLayout.centerContent(
context: context,
child: myWidget,
maxWidth: 1200, // optional; defaults to getMaxContentWidth(context)
)
Breakpoint constants (for custom logic): [TailwindBreakpoints.sm], [TailwindBreakpoints.md], [TailwindBreakpoints.lg], [TailwindBreakpoints.xl], [TailwindBreakpoints.xxl].
Pure width-based checks (no [BuildContext]): use [BreakpointResolver] with a width value (e.g. from [MediaQuery.sizeOf].width):
double width = MediaQuery.sizeOf(context).width;
if (BreakpointResolver.isMd(width)) { /* width >= 768 */ }
When to use what #
| Need | Use |
|---|---|
| “Am I on web / Android / iOS?” | [VpsResponsive.isWeb], [VpsResponsive.isAndroid], [VpsResponsive.isIOS], or [VpsResponsive.deviceType] |
| “Is viewport at least sm/md/lg?” | [ResponsiveLayout.isSm]/[isMd]/isLg |
| “Portrait or landscape?” | [ResponsiveLayout.isPortrait]/isLandscape |
| “Padding / max width by breakpoint?” | [ResponsiveLayout.getResponsivePadding], [ResponsiveLayout.getMaxContentWidth] |
| “Center content with max width?” | [ResponsiveLayout.centerContent] |
| “Width >= breakpoint without context?” | [BreakpointResolver.isSm]/[isMd]/…(width) |
Registry #
The package provides a shared registry utility for VPS Officer and VPS Citizen apps: shared models (registry entries, stations, officers) and a [RegistryDataSource] that uses the package [NetworkService] and [guardedApiCall] to fetch registry data. No app-specific network layer is required; the registry lives in the package and reuses the package network. The form engine uses [SubModule] and [Field] from the registry. See Registry for full documentation.
Purpose #
- Officer app: Fetch registry list (microservices/modules) via a single endpoint (e.g.
officer_registry), typically without auth. - Citizen app: Fetch registry plus police stations and officers in one response (endpoint e.g.
registry), with auth; optional fields such asinitial_iprsanddocumentsare supported in the shared models.
Models use optional fields so one canonical type works for both apps: e.g. [RegistryModel] has optional initial_iprs and documents (citizen uses them; officer receives null when the API omits them). [RegistryDatum] has optional initial_iprs. [StationResponseModel] and [StationOfficer] are used when the citizen app’s registry response includes stations and officers.
Resolving and using RegistryDataSource #
After Initialization, resolve the data source and call the appropriate method:
final registryDataSource = VpsUtilities.resolve<RegistryDataSource>();
// Officer-style: registry list only, no token
final registryList = await registryDataSource.getRegistry(tokenRequired: false);
// Citizen-style: registry + stations + officers in one call (with token)
final result = await registryDataSource.getRegistryWithStationsAndOfficers(
tokenRequired: true,
);
// result.data -> List<RegistryModel>
// result.stations -> List<StationResponseModel>? (may be null if API omits)
// result.officers -> List<StationOfficer>? (may be null if API omits)
On API or network failure, both methods throw [GuardedApiException]; apps can catch it and map to their own domain errors or UI messages.
Endpoint and token #
The default registration in the package uses endpoint registry and defaultTokenRequired: false. For the officer app (endpoint officer_registry, no token), register your own [RegistryDataSourceImpl] in app DI after setup, e.g.:
// In app DI, after VpsUtilitiesInjector.setup(baseUrl):
container.registerFactory<RegistryDataSource>(
(c) => RegistryDataSourceImpl(
c.resolve<NetworkService>(),
registryEndpoint: 'officer_registry',
defaultTokenRequired: false,
),
);
Alternatively, the package may be extended later to accept optional registryEndpoint and registryTokenRequired from VpsUtilities.initialize.
Exported types #
- Contracts: [RegistryDataSource]
- Result: [RegistryResult] (data, stations, officers)
- Implementation: [RegistryDataSourceImpl]
- Models: [RegistryModel], [RegistryDatum], [SubModule], [Field], [Conditions], [Dependency], [InputField], [StationResponseModel], [StationOfficer]
Full documentation: Registry.
Form engine #
The package provides a configurable form engine with paging, validation, optional AI narrative enhancement (Q&A and review dialogs), and a single-call API so the app can open a form with one line.
Quick start #
- Initialize with base URL and optional AI narrative URL (see Initialization).
- Register the form engine config after initialization:
VpsUtilities.formEngineConfig = VpsFormEngineConfig(
pageConfig: MyFormPageConfig(), // implements VpsFormPageConfig
theme: VpsFormTheme(...), // optional
fieldBuilderRegistry: myRegistry, // optional; custom field builders
);
- Open a form from anywhere with a
BuildContext. For per-module base URLs (e.g. from registry), set your page config’s module URL before callingpushForm(e.g.pageConfig.moduleBaseUrl = registryModel.url). See Form engine.
context.pushForm(subModule); // subModule is the package's SubModule (from registry)
When the user taps Submit on the last page, the engine validates, runs the AI narrative flow (if enabled), then optionally runs a pre-confirm flow (e.g. occurrence location and police station selection) when your page config returns true for shouldRunPreConfirmFlow, then pushes the default confirm page (or your custom [buildConfirmPage]). The default confirm page matches vps_citizen: step indicator (dynamic steps; optional show/hide and numbered/unnumbered), details card with module name and field rows, Edit and Submit buttons. When the user taps Submit on the confirm page, the engine submits to sub_module_data and pushes the default success or fail page. See Form engine for pre-confirm flow (OB flow) details.
Main types #
- [VpsFormPageConfig] – App implements this for baseUrl, optional mapsApiKey (for location fields), and optional custom narrative/success/fail dialogs. For per-module base URLs (e.g. from registry), add a settable
moduleBaseUrland havebaseUrlreturnmoduleBaseUrl ?? VpsUtilities.baseUrl; setmoduleBaseUrlbefore eachpushForm. - [VpsFormEngineConfig] – Bundles
pageConfig, optionaltheme, optionalfieldBuilderRegistry, optionalgetAuthToken, and optional step indicator overrides:initialStep,showStepIndicator,showStepNumbers. See Form engine. - [FieldBuilderRegistry] – Register custom builders by field type (e.g.
location,mugshot) or by field name. - [VpsFormEngine] – Root form widget (Scaffold, AppBar, step indicator, form content).
- context.pushForm(SubModule) – Extension to open the form using the registered config.
Full documentation: Form engine.
Architecture #
The package follows Clean Architecture and is organized by utility so multiple utilities are easy to add and maintain:
lib/src/
core/ # Shared: config + DI (used by other utilities)
domain/ # BaseUrlConfigRepository
data/ # BaseUrlConfigRepositoryImpl
di/ # VpsUtilitiesInjector (Kiwi container)
network/ # Network utility
domain/ # NetworkService, ApiErrors, guardedApiCall
data/ # NetworkServiceImpl, RestClient, ErrorSanitizer
registry/ # Registry utility (uses network)
domain/ # RegistryDataSource, RegistryResult, models
data/ # RegistryDataSourceImpl
form/ # Form engine (uses core, narrative)
domain/ # Ports, NarrativeKeysHelper
data/ # FormDependencyResolverImpl, FormValidationServiceImpl
presentation/ # VpsFormCubit, VpsFormEngine, config, field builders, pushForm
narrative/ # AI narrative enhancement (uses core)
domain/ # NarrativeServicePort
data/ # NarrativeServiceImpl, NoOpNarrativeService
secure_storage/ # Flexible secure key-value storage
secure_storage_impl.dart # SecureStorage (flutter_secure_storage)
token/ # Token utility (uses SecureStorage)
data/ # TokenStore
presentation/cubit/ # TokenCubit, TokenState
responsive/ # Responsive layout and device type
domain/ # DeviceType, TailwindBreakpoints, DeviceTypeProvider
data/ # DeviceTypeProviderImpl, BreakpointResolver
presentation/ # ResponsiveLayout, VpsResponsive
vps_utilities_init.dart # Package init and public API
- core: Base URL config and Kiwi DI; wires all utilities. Initialize with
VpsUtilities.initialize(baseUrl: ...). - network: HTTP client (domain contracts + data implementations; uses core for base URL and token utility for Bearer token).
- registry: Shared registry, station, and officer models plus [RegistryDataSource] for fetching registry (and optionally stations/officers) via the package [NetworkService] and [guardedApiCall]. See Registry.
- form: Configurable form engine with paging, validation, optional AI narrative flow, [FieldBuilderRegistry], and [context.pushForm]. See Form engine and Form engine.
- narrative: AI narrative enhancement service ([NarrativeServicePort]); real impl when
aiNarrativeBaseUrlis set, no-op otherwise. Used by the form engine. - secure_storage: Flexible secure key-value storage ([SecureStorage]); same backend as token (Android EncryptedSharedPreferences, iOS Keychain, etc.). Use Secure storage (flexible key-value) for API.
- token: Secure token storage and TokenCubit/TokenState for UI; uses [SecureStorage] with a fixed key.
- responsive: Device type ([DeviceType], [DeviceTypeProvider]) and Tailwind-style layout ([TailwindBreakpoints], [ResponsiveLayout], [BreakpointResolver]). Use [VpsResponsive] for device checks and Responsive layout and device type for API.
To add a new utility (e.g. analytics, logging): add a folder under src/ with domain/, data/, and optional presentation/, then register it in core/di/vps_utilities_injector.dart.
Dependencies are resolved via Kiwi. After initialization, use VpsUtilities.resolve<T>() to obtain registered types (e.g. NetworkService, BaseUrlConfigRepository, TokenCubit).
Regenerating Kiwi code #
If you add or change @Register in the injector, regenerate the .g.dart file:
cd packages/vps_utilities
dart run build_runner build --delete-conflicting-outputs