Sixplace
Cross-platform Flutter foundations for responsive layout, networking, feedback, themes, forms, storage abstractions, and Room-inspired SQLite persistence.
Sixplace helps Flutter developers build adaptive mobile, tablet, desktop, and web applications with minimal code while avoiding duplicated widget trees.
Its responsive layout system is inspired by the simplicity of Bootstrap's 12-column grid while remaining designed specifically for Flutter.
Sixplace combines six focused foundations: responsive layout, networking, feedback, theming, forms, and storage. The storage foundation now includes SPRoom, a Room-inspired structured SQLite layer powered by the sqflite ecosystem. Each foundation remains independently adoptable.
Why Sixplace?
Responsive Flutter applications often contain repeated widget trees:
if (isMobile) {
return MobilePage();
}
return DesktopPage();
This approach can duplicate:
- UI structure
- State bindings
- Loading and error states
- Navigation behavior
- Business logic
- Maintenance effort
Sixplace allows developers to define one mobile-first widget tree:
SPRow(
gap: 16,
children: [
SPCol(
md: 6,
child: firstCard,
),
SPCol(
md: 6,
child: secondCard,
),
],
)
The same layout automatically becomes:
- One column on smaller screens
- Two columns from the
mdbreakpoint - Responsive without separate mobile and desktop pages
Device registration: one API on every platform
Use await SPDeviceInfo.getRegistrationId() for registration across Android,
iOS, Windows, macOS, Linux and web. It creates a random UUID once and saves it
to application preferences (browser-origin storage on web). It returns the same
ID across normal restarts until that storage is cleared.
SPDeviceInfo.getDeviceId() is optional OS-specific metadata, not the
recommended registration key. Android ANDROID_ID, Apple vendor ID and desktop
machine IDs have different reset scopes. Browsers return null for OS ID.
SPDeviceInfo.getInfo() provides both deviceId and installationId
plus other configuration details.
final registrationId = await SPDeviceInfo.getRegistrationId();
final info = await SPDeviceInfo.getInfo();
print(registrationId);
print(info.deviceId); // OS-specific, null on web
print(info.installationId); // Registration UUID, if storage is accessible
If browser/app storage is unavailable, getRegistrationId() throws instead
of returning an unpersisted value. Registration UUIDs can reset on uninstall,
data clearing, private-browser sessions or changes of browser profile/origin.
Never use this UUID as an authentication secret or assume it is a permanent
physical-device identifier. No manual Android manifest / iOS plist change is
needed for this identifier.
SPDeviceInfo — Device configuration (no app permission edits)
Import package:sixplace/device_info.dart (also exported by sixplace.dart).
final snapshot = await SPDeviceInfo.getInfo();
print(snapshot.deviceId); // OS-provided ID where available; null on web
print(snapshot.model); // Device model
print(snapshot.name); // OS-provided device name, when available
print(snapshot.osVersion); // OS version, when available
print(snapshot.localIpAddresses); // Interface IPv4 addresses (not public IP)
print(snapshot.batteryLevel); // 0..100 or null
print(snapshot.batteryState); // charging/discharging/full/etc.
print(snapshot.toMap()); // Additional device metadata
Individual live getters: getDeviceId(), getBatteryLevel(),
getBatteryState(), getLocalIpAddress(), getLocalIpAddresses().
Identifier semantics: Android's Settings.Secure.ANDROID_ID (not
Build.ID) is normally a hexadecimal identifier, scoped on Android 8+
to the device + Android user + app signing key, and may change after a
factory reset/signing-key change. iOS uses identifierForVendor, which
can reset after all apps from the vendor are removed. Windows, macOS and
Linux return OS machine identifiers where supported; web returns null.
A permanent universally unique hardware ID, IMEI, MAC, public IP or serial
number is not available to arbitrary apps without restrictions.
All values are best effort; null indicates an unsupported or
unavailable field. No app manifest or Info.plist modifications or runtime
permission prompts are required for this API. On web, local IP addresses
are intentionally unavailable. No remote public-IP lookup is performed.
Never use these identifiers as credentials, and consider user privacy when
sending device details to a server.
Sixplace Foundations
Sixplace is designed around six coordinated application foundations:
| Foundation | Responsibility | Status |
|---|---|---|
SPLayout |
Responsive grids, containers, spacing, visibility, sizing and application shell | Available |
SPNetwork |
HTTP requests, authentication, retry, cancellation, uploads, typed errors and reachability | Available |
SPFeedback |
Transient messages, alerts and blocking/embedded loaders | Available |
SPTheme |
Colours, typography integration, spacing, radii and design tokens | Available |
SPForms |
Inputs, validation, guarded submission and submit-state UI | Available |
SPStorage |
Preference/secure-store abstractions, bounded cache, and SPRoom structured SQLite persistence |
Available |
All six foundations are part of the current public API. SPRoom belongs to the storage foundation and is also independently importable through package:sixplace/room.dart.
AI prompt — copy this into an AI coding agent or chat assistant
You are assisting a Flutter developer who is using Sixplace 1.0.1.
DEVICE REGISTRATION GUIDANCE:
For application/device registration across Android, iOS, Windows, macOS, Linux and web, always prefer await SPDeviceInfo.getRegistrationId(). This creates one persisted app-installation UUID and works on web. SPDeviceInfo.getDeviceId() is optional OS-provided metadata, not the registration identity; web returns null for OS ID. SPDeviceInfo.getInfo() exposes both deviceId and installationId. Clearing storage or reinstalling may rotate the registration ID. Registration IDs are not authentication credentials. Do not confuse these distinct APIs.
Your job is to understand Sixplace as an application-foundation package and use it correctly when:
- starting a new Flutter project,
- introducing Sixplace into an existing Flutter project,
- migrating duplicated responsive layouts to one responsive widget tree,
- migrating direct HTTP calls to SPNetwork,
- standardizing feedback, theme tokens, forms, and storage abstractions,
- reviewing or refactoring code that already uses Sixplace,
- or planning a gradual migration without forcing unrelated architectural changes.
Treat the information below as the working contract for Sixplace 1.0.1. If the installed package source or current official Sixplace documentation conflicts with this prompt, the installed package/API is the source of truth. Do not invent Sixplace APIs that are not documented or present in the package.
PACKAGE IDENTITY
================
Package: sixplace
Stable version: 1.0.1
Flutter package: yes
Dart SDK constraint: >=3.9.0 <4.0.0
Flutter constraint: >=3.35.0
Primary platforms: Android, iOS, Linux, macOS, web, Windows
License: MIT
Repository: https://github.com/innovatenestlabs-inl/sixplace
Runtime dependencies are intentionally small and focused:
- flutter (SDK)
- android_id
- battery_plus
- device_info_plus
- shared_preferences
- uuid
- http
- http_parser
- sqflite
- sqflite_common
Sixplace is not a state-management framework and does not require GetX, Riverpod, Bloc, Provider, or another state manager. It also does not own application routing, business rules, domain models, authentication state, or a platform secure-storage plugin. For structured local relational persistence it provides the optional SPRoom layer; applications still own their entities, schema design, migrations, data-retention rules, and encryption decisions.
PUBLIC IMPORTS
==============
Use the complete API when broad access is appropriate:
import 'package:sixplace/sixplace.dart';
Or import focused foundations:
import 'package:sixplace/layout.dart';
import 'package:sixplace/network.dart';
import 'package:sixplace/device_info.dart';
import 'package:sixplace/feedback.dart';
import 'package:sixplace/theme.dart';
import 'package:sixplace/forms.dart';
import 'package:sixplace/storage.dart';
import 'package:sixplace/room.dart';
Numeric design-scaling helpers are also available through:
import 'package:sixplace/sixplace_numeric.dart';
DEVICE INFORMATION AND REGISTRATION
===================================
Import package:sixplace/device_info.dart or use package:sixplace/sixplace.dart.
Use await SPDeviceInfo.getRegistrationId() as the persisted registration UUID
on Android, iOS, Windows, macOS, Linux and web. This method propagates storage
errors rather than returning an unpersisted replacement ID. Calls made
concurrently share the same pending initialization.
SPDeviceInfo.getDeviceId() is separate, nullable OS-provided metadata. Android
uses ANDROID_ID, iOS uses identifierForVendor, and Windows/macOS/Linux use
platform machine identifiers where available. Web always returns null for
deviceId and does not claim an accessible physical hardware identifier.
SPDeviceInfo.getInfo() returns a snapshot with separate deviceId and
installationId, plus available model/name/manufacturer/OS, battery and local
IP fields. Unsupported fields remain null or empty. No manifest or Info.plist
changes are required. These identifiers are not authentication credentials.
ARCHITECTURAL PRINCIPLE
=======================
Sixplace has six coordinated but independently adoptable foundations:
1. SPLayout - responsive layout, grid, spacing, visibility, adaptive shell
2. SPNetwork - UI-independent HTTP client and transport behavior
3. SPFeedback - messages, dialogs, blocking/embedded loaders
4. SPTheme - Material themes and semantic design tokens
5. SPForms - validators, common form fields, guarded submission
6. SPStorage - preference/secure-store contracts, bounded cache, and SPRoom SQLite persistence
A project does not need to migrate all six foundations at once. Prefer incremental adoption. Do not rewrite working business logic merely to use Sixplace.
============================================================
1. SPLAYOUT
============================================================
PURPOSE
-------
Use SPLayout to build mobile-first responsive Flutter interfaces without maintaining separate mobile/tablet/desktop page trees.
Default breakpoints are Bootstrap-inspired:
- xs: 0+
- sm: 576+
- md: 768+
- lg: 992+
- xl: 1200+
- xxl: 1400+
Responsive values inherit forward from smaller breakpoints until overridden.
Example:
SPCol(
sm: 6,
lg: 4,
child: content,
)
means:
- xs: 12 columns by default
- sm/md: 6 columns
- lg/xl/xxl: 4 columns
KEY LAYOUT TYPES
----------------
- SPBreakpoint
- SPBreakpoints
- SPResponsiveValue<T>
- SPResponsive
- context.sp
- SPContainer
- SPRow
- SPCol
- SPResponsiveSizedBox
- SPPadding
- SPMargin
- SPVisibility
- SPAppBar
- SPScaffold
CORE PATTERN
------------
Prefer one widget tree:
SPRow(
gap: 16,
children: [
SPCol(
md: 6,
child: firstCard,
),
SPCol(
md: 6,
child: secondCard,
),
],
)
Avoid creating a full MobilePage and DesktopPage when only structure or sizing changes. Use responsive spans, values, visibility, ordering, padding, and scaffold behavior instead.
RESOLUTION SEMANTICS
--------------------
This distinction is important:
Viewport-based:
- context.sp
- SPResponsiveValue.resolveFrom(context)
- SPResponsiveSizedBox
- SPPadding
- SPMargin
- SPVisibility
- SPScaffold
Immediate-parent-width based:
- SPContainer
- SPRow
- SPCol inside SPRow
This lets nested grids respond to their actual container width while screen-level decisions still use viewport width.
SPResponsiveValue<T> example:
const spacing = SPResponsiveValue<double>(
base: 8,
md: 16,
xl: 24,
);
Missing values inherit from the nearest smaller configured breakpoint.
VIEWPORT INFORMATION
--------------------
Use:
final responsive = context.sp;
Common properties include:
- width
- height
- breakpoint
- isMobile (xs/sm)
- isTablet (md)
- isDesktop (lg+)
- isLargeDesktop (xxl+)
- atLeast(...)
- below(...)
APPLICATION SHELL
-----------------
SPScaffold can coordinate:
- normal mobile drawer,
- permanent desktop drawer,
- tablet navigation rail,
- responsive bottom navigation visibility,
- SPAppBar.
Use SPAppBar for a platform-adaptive app bar with iOS-aware presentation, automatic back handling, dark-mode-aware colors, and optional drawer action behavior.
CUSTOM BREAKPOINTS
------------------
SPBreakpoints can be customized. Keep breakpoints strictly increasing. When migrating an existing app with established breakpoints, prefer configuring Sixplace to match the existing design system rather than arbitrarily changing layout thresholds.
============================================================
NUMERIC DESIGN SCALING (RELATED CORE API)
============================================================
Sixplace also provides numeric design-draft helpers:
- value.w -> width scaling
- value.h -> height scaling
- value.sp -> font scaling with system text scaling
- value.rem -> root-font-relative scaling
- value.em(context) -> inherited/parent-font-relative scaling
These helpers require SixPlace metrics initialization.
Default design size: 390 x 844
Default root font size: 16
Initialize from a valid MediaQuery context using SixPlace.init(...), or wrap the relevant subtree with SixPlaceScope.
Example concept:
SixPlaceScope(
designSize: const Size(390, 844),
rootFontSize: 16,
child: appContent,
)
Do not confuse numeric design scaling with breakpoint-responsive layout. Use breakpoints for structural decisions; use numeric scaling only where design-draft proportional sizing is genuinely desired.
SPNetwork does not require SixPlace.init or SixPlaceScope.
============================================================
2. SPNETWORK
============================================================
PURPOSE
-------
SPNetwork is a persistent, UI-independent HTTP layer. It wraps package:http behavior while keeping storage, navigation, UI feedback, and business rules owned by the application.
SHARED INITIALIZATION
---------------------
Initialize a shared client once, normally during application startup:
SPNetwork.init(
config: SPNetworkConfig(
baseUrl: 'https://api.example.com/api/v1/',
timeout: const Duration(seconds: 30),
retryPolicy: SPRetryPolicy(maxAttempts: 3),
),
);
Then reuse:
final api = SPNetwork.instance;
SPNetwork.init throws if called again while the shared client is still active. Closing the shared client allows future initialization.
INDEPENDENT CLIENTS
-------------------
For multiple backends create separate clients:
final reportingApi = SPNetwork(
config: SPNetworkConfig(
baseUrl: 'https://reports.example.com/api/',
),
);
Reuse clients. Do not create one SPNetwork instance per request. Call close() only when the owner is permanently disposed.
CONFIGURATION
-------------
SPNetworkConfig supports concepts including:
- validated absolute HTTP(S) baseUrl,
- default headers,
- per-attempt timeout,
- SPRetryPolicy,
- tokenProvider,
- onUnauthorized callback,
- metadata-only logger,
- maxResponseBytes.
The base URL must not contain credentials, query parameters, or a fragment. Sixplace normalizes it as a directory-style base URL so relative endpoints preserve the configured path prefix.
REQUEST METHODS
---------------
SPNetwork supports:
- get
- post
- put
- patch
- delete
- head
- request
- upload
- isDeviceOnline
JSON request bodies are encoded by Sixplace. Pass Dart JSON-compatible objects. Do not jsonEncode the request body before passing it to SPNetwork.
Example:
final response = await SPNetwork.instance.post<Map<String, dynamic>>(
'customers',
body: {
'name': 'Example Customer',
},
);
TYPED DECODING
--------------
Use a decoder when mapping a successful decoded response to a model:
final response = await SPNetwork.instance.get<Customer>(
'customers/42',
decoder: (body) => Customer.fromJson(
body! as Map<String, dynamic>,
),
);
SPNetworkResponse<T> exposes the decoded data plus statusCode, headers, bodyBytes, URI, and attempt count.
PER-REQUEST OPTIONS
-------------------
SPRequestOptions can override:
- headers,
- authenticated behavior,
- bearer token,
- timeout,
- retry policy,
- response type,
- cancellation token.
SPResponseType supports JSON, text, and raw bytes.
AUTHENTICATION
--------------
Prefer tokenProvider for current bearer-token lookup. The provider is evaluated at call time, so newly stored tokens are used without recreating the client.
For public/login requests use authenticated: false. This removes Authorization handling for that request and skips the authenticated 401 callback behavior.
The application owns token persistence and refresh/login policy. Sixplace does not decide where tokens are stored.
UNAUTHORIZED HANDLING
---------------------
An authenticated 401 can trigger the configured onUnauthorized callback once for the current auth session. After a successful login, call resetUnauthorizedState() so a future authenticated 401 can trigger the callback again.
The application owns navigation, session cleanup, user messaging, and re-authentication.
RETRIES
-------
Sixplace automatically retries only GET and HEAD according to SPRetryPolicy.
Writes are intentionally not automatically replayed by Sixplace:
- POST: one attempt
- PUT: one attempt
- PATCH: one attempt
- DELETE: one attempt
- multipart write: one attempt
This protects transactional operations from accidental duplicate writes.
A custom injected HTTP client may have its own replay policy; if one is injected, review that transport independently.
SPRetryPolicy.maxAttempts includes the first attempt. Default retryable statuses include 408, 429, 502, 503, and 504. 401 and 403 are not retry statuses.
For critical write operations such as orders, payments, invoices, or POS sales, pair client-side duplicate-submit protection with backend idempotency.
CANCELLATION
------------
Use SPCancelToken to cancel one request or multiple requests sharing the token. Cancellation is permanent; create a new token for a new operation.
UPLOADS
-------
SPUploadFile is byte-backed and works across native and web without requiring dart:io File. Read/pick bytes in application code and pass them to SPUploadFile. Upload supports POST, PUT, and PATCH.
Large streaming uploads/downloads are outside the buffered initial API. Respect maxResponseBytes when downloading files.
ERRORS
------
Catch SPNetworkException and branch on SPNetworkErrorType rather than parsing text.
Error types include:
- http
- authentication
- timeout
- connection
- cancelled
- decoding
- responseTooLarge
Potentially sensitive fields such as URI, headers, body, and underlying cause are available on the exception for controlled handling, but SPNetworkException.toString intentionally avoids exposing those values. Do not blindly log sensitive exception fields.
REACHABILITY
------------
isDeviceOnline() is a backend reachability probe, not a guarantee that the backend is healthy.
It performs an unauthenticated single-attempt HEAD request with no automatic retry. Any received HTTP response, including 401/404/500/503, proves the network path/server answered and therefore counts as reachable. DNS, socket, TLS/transport failure, or timeout returns false.
Do not call isDeviceOnline() before every write. A preflight request introduces a race condition and extra traffic. Normally send the real request and handle its typed result.
REQUEST LIFECYCLE GUIDANCE
--------------------------
Do not start network calls directly in build(). Cache futures where FutureBuilder is used. Reuse SPNetwork clients. Guard submit buttons. Keep token providers lightweight.
============================================================
3. SPFEEDBACK
============================================================
PURPOSE
-------
SPFeedback provides framework-native user feedback without requiring a router or state manager.
Key APIs:
- SPFeedback.showToast
- SPFeedback.showMessage
- SPFeedback.showAlert
- SPFeedback.showLoading
- SPBlockingLoader
- SPFeedbackLoaderHandle
- SPFeedbackType
- SPFeedbackStyle
Semantic feedback types:
- info
- success
- warning
- error
Messages use ScaffoldMessenger, so call them from a context that has the required Flutter ancestors.
Example:
SPFeedback.showMessage(
context,
'Saved successfully.',
type: SPFeedbackType.success,
);
Alerts return a nullable bool so the caller can distinguish confirm, explicit cancel, and external dismissal.
Blocking loader example:
final loader = SPFeedback.showLoading(
context,
message: 'Saving...',
);
try {
await save();
} finally {
loader.close();
}
The loader handle closes only the route it created. It must not be replaced with a generic Navigator.pop that could close an unrelated route.
Use SPBlockingLoader when an inline/embedded loading surface is better than a modal route.
============================================================
4. SPTHEME
============================================================
PURPOSE
-------
SPTheme creates Material themes and installs Sixplace semantic tokens through Flutter ThemeExtension.
Key APIs:
- SPTheme.light
- SPTheme.dark
- SPTheme.withTokens
- SPThemeTokens
- SPSpacingTokens
- SPRadiusTokens
- SPTypographyTokens
- context.spTheme
- context.spSpacing
- context.spRadius
- context.spTypography
Example:
MaterialApp(
theme: SPTheme.light(
seedColor: const Color(0xFF5B5BD6),
),
darkTheme: SPTheme.dark(
seedColor: const Color(0xFF5B5BD6),
),
home: const HomePage(),
)
Use SPTheme.withTokens(existingTheme) when the app already has a carefully configured ThemeData and only Sixplace tokens should be installed. This is preferable during migration because it avoids replacing unrelated theme configuration.
Tokens include semantic colors plus spacing, radius, and typography scales. Theme accessors have a ColorScheme-based fallback if tokens are not explicitly installed, but installing them intentionally is preferred for a consistent design system.
============================================================
5. SPFORMS
============================================================
PURPOSE
-------
SPForms provides common validators, Material form inputs, and guarded asynchronous submission. It does not impose a state-management framework.
Key APIs:
- SPValidators
- SPFormController
- SPTextFormField
- SPDropdownFormField<T>
- SPCheckboxFormField
- SPSubmitButton
Built-in validators include:
- required
- email
- minLength
- maxLength
- pattern
- minNumber
- maxNumber
- compose
Typical pattern:
final formKey = GlobalKey<FormState>();
final formController = SPFormController();
Future<void> save() async {
await formController.submit<void>(
formKey: formKey,
context: context,
action: () async {
await SPNetwork.instance.post(
'customers',
body: buildPayload(),
);
},
);
}
SPSubmitButton can observe the controller and prevent repeated user submission while the operation is running.
Dispose SPFormController when it is owned by a State object.
Guarding the UI is not a substitute for server-side idempotency on transactions that must never execute twice.
============================================================
6. SPSTORAGE + SPROOM
============================================================
PURPOSE
-------
SPStorage defines stable preference/secure-store interfaces without forcing a key/value or secure-storage plugin on all consumers. The same storage foundation also includes SPRoom for structured relational SQLite data.
Key APIs:
- SPPreferencesStore
- SPSecureStore
- SPMemoryPreferencesStore
- SPMemorySecureStore
- SPStorageCache
- SPStorage
Applications should adapt their chosen persistence libraries to:
SPPreferencesStore
- read
- write
- containsKey
- remove
- clear
SPSecureStore
- read
- write
- containsKey
- remove
- clear
Preference values are intentionally restricted to common primitive preference types:
- String
- bool
- int
- double
- List<String>
SPMemoryPreferencesStore and SPMemorySecureStore are useful for tests, previews, and ephemeral sessions. SPMemorySecureStore is not encrypted persistent secure storage and must never be described as equivalent to an OS-backed secure store.
SPStorageCache is a bounded in-process LRU-style cache with optional TTL expiration. It does not run a background polling timer. Expiration is handled lazily during cache access, and least-recently-used entries are evicted when capacity is exceeded.
SPROOM
------
SPRoom is Sixplace's Room-inspired SQLite persistence layer. It is intentionally modeled after the mental model used by AndroidX Room in Kotlin projects:
- database class / central database access point,
- entities/tables,
- DAOs,
- explicit schema versions,
- explicit migrations,
- transactions,
- conflict strategies,
- typed entity adapters,
- reactive query invalidation.
SPRoom uses the sqflite ecosystem underneath. On Android, iOS, and macOS the default factory comes from package:sqflite. On Linux, Windows, and web, inject a compatible DatabaseFactory from the sqflite ecosystem (for example sqflite_common_ffi on desktop or a compatible web factory) instead of assuming the mobile plugin exists.
Focused import:
import 'package:sixplace/room.dart';
Core SPRoom types:
- SPRoom
- SPRoomDatabaseBuilder<T>
- SPRoomDatabase
- SPRoomEntitySchema
- SPRoomColumn
- SPRoomIndex
- SPRoomForeignKey
- SPRoomEntityAdapter<T>
- SPRoomDao<T>
- SPRoomMigration
- SPRoomExecutor
- SPRoomInvalidationTracker
- SPRoomConflictStrategy
- SPRoomTypeConverter<T, D>
Typical architecture:
class AppDatabase extends SPRoomDatabase {
late final UserDao users = UserDao(this);
}
class UserDao extends SPRoomDao<User> {
UserDao(SPRoomDatabase database) : super(database, UserAdapter());
}
final db = await SPRoom.databaseBuilder<AppDatabase>(
name: 'app.db',
version: 1,
create: AppDatabase.new,
entities: const [userSchema],
).build();
Entity schemas are explicit rather than annotation-generated. Flutter/Dart does not provide Kotlin KSP semantics at runtime, so do NOT invent @Entity/@Dao annotations or claim compile-time SQL verification exists. SPRoom aims for Room-like runtime behavior and architecture while keeping the Dart API explicit, tree-shakeable, and predictable.
Entity adapter responsibilities:
- schema: declares the table schema,
- toRow(entity): maps a Dart model to SQLite-compatible values,
- fromRow(row): maps a database row back to the Dart model,
- primaryKeyOf(entity): defaults to reading declared PK fields from toRow and can be overridden.
SPRoomDao provides:
- insert / insertAll,
- upsert / upsertAll using SQLite ON CONFLICT DO UPDATE,
- update / updateAll,
- delete / deleteAll / deleteWhere,
- getAll / query,
- findById / findByPrimaryKey,
- count,
- typed rawQuery,
- rawWrite with invalidation,
- watch / watchRawQuery reactive streams.
Transaction rule:
await db.withTransaction(() async {
await db.users.insert(user);
await db.audit.insert(entry);
});
DAO calls inside `writeTransaction`, `withTransaction`, or `runInTransaction` automatically reuse the active sqflite transaction. Do not manually use the outer database connection from inside a transaction callback.
Migrations are explicit:
SPRoomMigration(1, 2, (db) async {
await db.execute('ALTER TABLE users ADD COLUMN nickname TEXT');
});
Register them through addMigrations(...). If a path is missing, opening fails by default. fallbackToDestructiveMigration() is opt-in because it destroys managed data.
Schema validation is enabled by default. SPRoom checks managed table structure and explicit indexes after creation/migration/open and keeps a schema identity marker. A schema change must therefore be paired with a version bump and migration. Do not silently edit the schema while keeping the same version.
Reactive queries:
final stream = db.users.watch(orderBy: 'name');
Writes made through SPRoomDao invalidate the relevant table automatically. If custom SQL writes bypass a DAO, use db.rawWrite(...) or db.notifyTablesChanged(...) so watchers refresh. Writes from completely separate SQLite connections cannot be automatically observed by this in-process tracker.
Security and durability:
- SPRoom is SQLite persistence, not encrypted storage.
- Do not store secrets merely because the database is local.
- Use an encryption strategy appropriate to the application when at-rest confidentiality is required.
- Keep migrations tested with production-like data.
- Prefer one long-lived database instance per app process, matching Room/sqflite guidance.
- Close databases intentionally in tests/tools; normal app lifecycles usually keep the singleton open.
Do not claim SPRoom is the AndroidX Room implementation. It is an independent Dart/Flutter API inspired by Room's architecture and powered by sqflite. Room's compile-time KSP query verification and generated DAO implementations are not reproduced by Sixplace 1.0.1.
SECURITY BOUNDARIES
===================
Do not claim Sixplace provides security properties it does not own.
Sixplace does NOT automatically provide:
- encrypted persistent storage,
- OAuth/OIDC implementation,
- token refresh flows,
- certificate pinning,
- application authorization rules,
- database encryption,
- route guards,
- backend idempotency,
- connectivity guarantees,
- automatic safe replay of write requests.
Use SPStorage abstractions with an appropriate secure-storage adapter where secrets must persist. Keep authentication and authorization decisions in application/backend code.
NEW PROJECT GUIDANCE
====================
When starting a fresh Flutter project with Sixplace:
1. Add sixplace to pubspec.
2. Choose focused imports or package:sixplace/sixplace.dart.
3. Establish the app ThemeData using SPTheme or install tokens into the existing theme.
4. Decide whether numeric design scaling is actually needed; if yes, initialize SixPlace metrics from a MediaQuery context.
5. Build pages mobile-first with SPContainer, SPRow, SPCol, SPResponsiveValue, and SPScaffold.
6. Initialize SPNetwork once per backend, with application-owned token/session callbacks.
7. Use SPForms for common input/submit patterns where helpful.
8. Use SPFeedback for user-facing transient messages, alerts, and loaders.
9. Provide real persistence adapters to SPStorage when key/value data must survive restarts; use memory stores only where ephemeral behavior is intended.
10. Use SPRoom when the app needs structured relational SQLite data, and define versioned migrations before shipping schema changes.
11. Keep business/domain logic independent from presentation helpers.
EXISTING PROJECT MIGRATION GUIDANCE
===================================
When migrating an existing app, first audit before editing:
A. Responsive UI
- Find duplicated mobile/tablet/desktop widget trees.
- Identify existing breakpoints and parent-width constraints.
- Migrate page sections incrementally to SPRow/SPCol/SPResponsiveValue.
- Preserve state, controllers, and business logic while replacing only layout duplication.
- Use custom SPBreakpoints if the existing product already has established thresholds.
B. Networking
- Inventory direct http calls and any current client wrapper/interceptors.
- Identify token lookup, 401 handling, retry behavior, timeout behavior, multipart behavior, logging, and response decoding.
- Do not blindly replace a mature networking layer. Map each existing behavior to SPNetwork and keep app-specific requirements outside the package.
- Never introduce automatic replay of writes during migration.
- Use independent SPNetwork clients for genuinely different backends/origins.
C. Theme
- Preserve existing ThemeData and install Sixplace tokens with SPTheme.withTokens when a full theme replacement is unnecessary.
D. Forms
- Replace repeated validator/submit boilerplate where it improves consistency, without rewriting domain validation that belongs elsewhere.
E. Storage
- Wrap existing SharedPreferences/secure-storage implementations with Sixplace interfaces rather than replacing proven key/value persistence mechanisms solely for package conformity.
- If an existing app already uses SQLite/sqflite, migrate incrementally: model the existing tables with SPRoomEntitySchema, create adapters/DAOs around the current schema, keep the existing database version, then add explicit SPRoomMigration steps for future changes. Never drop or recreate production tables simply to adopt SPRoom.
F. Feedback
- Migrate messages/dialogs/loaders where consistent cross-platform Flutter-native behavior is desired. Preserve app-specific navigation and error policy.
MIGRATION SAFETY RULES
======================
- Do not change business behavior just to reduce line count.
- Do not introduce duplicate widget trees when responsive primitives can solve the layout.
- Do not start API calls in build().
- Do not create an SPNetwork client per request.
- Do not pre-jsonEncode request bodies for SPNetwork JSON calls.
- Do not automatically retry transactional writes.
- Do not treat isDeviceOnline() as proof that the backend is healthy.
- Do not log tokens, authorization headers, private bodies, or sensitive exception details.
- Do not represent SPMemorySecureStore as encrypted persistent storage.
- Do not force all six foundations into a project if only some solve a real problem.
- Prefer small, reviewable migration steps with tests after each step.
TESTING EXPECTATIONS
====================
When modifying a project that uses Sixplace:
- preserve existing tests,
- add tests for changed responsive behavior,
- test network success and typed failures,
- test guarded submission for transactional UI,
- test custom storage adapters,
- verify mobile/tablet/desktop breakpoints,
- verify web behavior when network or file-upload code changes.
For the Sixplace package itself, expected validation includes:
- dart format with no changes required,
- flutter analyze with no issues,
- flutter test passing,
- dependency freshness reviewed,
- pub publish dry-run with no unexpected files/warnings,
- pana review before a public release.
HOW TO ANSWER THE DEVELOPER
===========================
When the developer asks for help:
1. Identify whether the task belongs to layout, network, feedback, theme, forms, storage, numeric scaling, or application-specific code.
2. Prefer the narrowest Sixplace foundation that solves the problem.
3. Explain what Sixplace owns and what the consuming app must still own.
4. Produce compile-oriented Flutter/Dart examples using real Sixplace 1.0.1 APIs only.
5. For migration requests, first describe the current-to-target mapping and compatibility risks, then provide the implementation steps/code.
6. For architecture requests, preserve existing state management unless the developer explicitly asks to change it.
7. For network writes, prioritize duplicate-write safety and backend idempotency.
8. For responsive work, prefer one mobile-first widget tree and use parent-width vs viewport-width semantics correctly.
9. If the request depends on an API not described here, inspect the installed Sixplace source/docs before proposing code; do not guess method names.
10. If Sixplace is not the right layer for a requirement, say so and keep that responsibility in the application or backend.
REFERENCE EXAMPLE: RESPONSIVE PAGE
==================================
SPContainer.fluid(
responsivePadding: const SPResponsiveValue<EdgeInsetsGeometry>(
base: EdgeInsets.all(8),
md: EdgeInsets.all(12),
lg: EdgeInsets.all(16),
),
child: SPRow(
responsiveGap: const SPResponsiveValue<double>(
base: 8,
lg: 16,
),
children: [
SPCol(
span: 12,
lg: 8,
child: mainContent,
),
SPCol(
span: 12,
lg: 4,
child: sideContent,
),
],
),
)
REFERENCE EXAMPLE: NETWORK + FORM + FEEDBACK
============================================
final formController = SPFormController();
Future<void> submit(BuildContext context) async {
try {
final saved = await formController.submit<bool>(
formKey: formKey,
context: context,
action: () async {
await SPNetwork.instance.post<Map<String, dynamic>>(
'orders',
body: buildOrderPayload(),
);
return true;
},
);
if (saved == true && context.mounted) {
SPFeedback.showMessage(
context,
'Order saved.',
type: SPFeedbackType.success,
);
}
} on SPNetworkException catch (error) {
if (context.mounted) {
SPFeedback.showMessage(
context,
'Unable to save the order (${error.type.name}).',
type: SPFeedbackType.error,
);
}
}
}
FINAL MENTAL MODEL
==================
Sixplace should reduce repeated application plumbing while leaving application ownership clear:
- SPLayout structures responsive UI.
- SPNetwork transports requests safely and predictably.
- SPFeedback presents framework-native feedback.
- SPTheme provides consistent Material themes and tokens.
- SPForms reduces repeated validation/submission boilerplate.
- SPStorage supplies persistence contracts and a bounded cache.
- SPRoom supplies Room-inspired SQLite entities, DAOs, migrations, transactions, and reactive invalidation within the storage foundation.
Use Sixplace as a foundation, not as a replacement for domain architecture.
Installation
Add Sixplace to your Flutter project:
flutter pub add sixplace
When using FVM:
fvm flutter pub add sixplace
Or add it manually:
dependencies:
sixplace: ^1.0.0
For local package development:
dependencies:
sixplace:
path: ../sixplace
Import the complete public API:
import 'package:sixplace/sixplace.dart';
Or import only the layout API:
import 'package:sixplace/layout.dart';
Or import a single foundation:
import 'package:sixplace/network.dart';
import 'package:sixplace/feedback.dart';
import 'package:sixplace/theme.dart';
import 'package:sixplace/forms.dart';
import 'package:sixplace/storage.dart';
import 'package:sixplace/room.dart';
SPNetwork
SPNetwork is Sixplace's UI-independent networking foundation.
Applications can use Sixplace instead of directly calling http.get, http.post, and related transport APIs throughout the application.
Internally, Sixplace uses package:http for native/browser transport and http_parser for MIME types and HTTP date handling.
SPNetwork does not depend on:
BuildContext- GetX
- Riverpod
- Bloc
- Provider
- Secure-storage packages
- Navigation packages
- Toast packages
Your application remains responsible for storage, navigation, UI messages, business rules, and state management.
Initialize Once
Initialize the shared client before runApp():
import 'package:flutter/material.dart';
import 'package:sixplace/network.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SPNetwork.init(
config: SPNetworkConfig(
baseUrl: 'https://your-api.example/api/v2/',
timeout: const Duration(seconds: 30),
retryPolicy: SPRetryPolicy(
maxAttempts: 3,
),
),
);
runApp(const MyApp());
}
Replace the example URL with your real API.
If your tokenProvider depends on plugin-backed storage, initialize that storage before calling SPNetwork.init().
SPNetwork is independent from SixPlace.init() and SixPlaceScope.
No layout initialization is required for networking.
Shared Client
After initialization:
final network = SPNetwork.instance;
SPNetwork.instance throws if SPNetwork.init() has not been called.
Calling SPNetwork.init() a second time while the shared instance is active also throws instead of silently replacing the existing client.
This prevents accidental creation of multiple shared network clients.
Independent Clients
For applications communicating with more than one backend:
final primaryApi = SPNetwork(
config: SPNetworkConfig(
baseUrl: 'https://api.example.com/v1/',
),
);
final reportingApi = SPNetwork(
config: SPNetworkConfig(
baseUrl: 'https://reports.example.com/api/',
),
);
Reuse each client instead of constructing a new client for every request.
When an independently owned client is no longer required:
primaryApi.close();
Client Lifecycle
Sixplace creates and reuses one http.Client per SPNetwork instance by default.
That allows the underlying transport to reuse persistent HTTP connections efficiently.
Do not create a new SPNetwork instance for every API request.
Recommended:
final api = SPNetwork.instance;
await api.get('products');
await api.get('customers');
await api.post(
'sales',
body: sale,
);
Avoid:
await SPNetwork(
config: config,
).get('products');
await SPNetwork(
config: config,
).get('customers');
Call close() only when the owner of the client is being permanently disposed.
api.close();
Closing the shared instance also allows a future SPNetwork.init() call to create a new shared client.
JSON Requests
GET
final response =
await SPNetwork.instance.get<Map<String, dynamic>>(
'products',
queryParameters: {
'page': 1,
'search': 'LED lamp',
},
);
final data = response.data;
final statusCode = response.statusCode;
POST
final response =
await SPNetwork.instance.post<Map<String, dynamic>>(
'products',
body: {
'name': 'LED lamp',
'quantity': 10,
},
);
Pass JSON-compatible objects directly.
Do not call jsonEncode() yourself:
// Correct
body: {
'name': 'LED lamp',
}
// Avoid
body: jsonEncode({
'name': 'LED lamp',
})
Sixplace performs JSON encoding once.
PUT
await SPNetwork.instance.put<Map<String, dynamic>>(
'products/100',
body: {
'name': 'Updated LED lamp',
},
);
PATCH
await SPNetwork.instance.patch<Map<String, dynamic>>(
'products/100',
body: {
'quantity': 20,
},
);
DELETE
await SPNetwork.instance.delete<Object?>(
'products/100',
);
An optional JSON body is also supported.
HEAD
final response = await SPNetwork.instance.head(
'health',
);
print(response.statusCode);
print(response.headers);
HEAD responses always return null data.
Backend Reachability
Sixplace provides a lightweight reachability check:
final bool online =
await SPNetwork.instance.isDeviceOnline();
Or specify a dedicated health endpoint:
final bool online =
await SPNetwork.instance.isDeviceOnline(
endpoint: 'health',
);
The default timeout is three seconds:
final bool online =
await SPNetwork.instance.isDeviceOnline(
endpoint: 'health',
timeout: const Duration(seconds: 5),
);
What isDeviceOnline() Means
isDeviceOnline() checks whether the configured backend can currently be reached.
It performs:
- An unauthenticated
HEADrequest - One transport attempt
- No automatic retry
A received HTTP response means the backend/network path was reachable.
Therefore responses such as:
200 -> true
204 -> true
401 -> true
404 -> true
500 -> true
503 -> true
still return true.
Those responses prove that an HTTP server answered.
Failures such as:
- DNS failure
- Socket connection failure
- TLS/transport failure
- Request timeout
return:
false
This method is intentionally a reachability check, not a backend-health guarantee.
For example:
HTTP 500
means:
Backend reachable: yes
Backend healthy: not necessarily
Recommended Health Endpoint
Production applications should preferably expose a lightweight endpoint such as:
HEAD /api/health
or:
GET /api/health
Then:
final reachable =
await SPNetwork.instance.isDeviceOnline(
endpoint: 'health',
);
Using a dedicated endpoint avoids relying on whether the backend supports HEAD on its root URL.
Important POS and Transactional Usage
isDeviceOnline() should not normally be called before every POST request.
Avoid:
if (await SPNetwork.instance.isDeviceOnline(
endpoint: 'health',
)) {
await SPNetwork.instance.post(
'sales',
body: sale.toJson(),
);
}
That intentionally creates two HTTP requests:
HEAD /health
POST /sales
For normal transactional operations, send the actual request and handle connection failures:
try {
await SPNetwork.instance.post<Object?>(
'sales',
body: sale.toJson(),
);
} on SPNetworkException catch (error) {
if (error.type == SPNetworkErrorType.connection ||
error.type == SPNetworkErrorType.timeout) {
await saveSaleOffline(sale);
return;
}
rethrow;
}
Use isDeviceOnline() for scenarios such as:
- Showing an online/offline indicator
- User-initiated connectivity checks
- Deciding whether to start offline synchronization
- Checking connectivity when an application resumes
- Checking before a large synchronization operation
- Retrying an offline queue
Remember that connectivity can change immediately after any reachability check.
Typed Models
Sixplace can decode API responses directly into application models.
class Product {
const Product(
this.id,
this.name,
);
final int id;
final String name;
factory Product.fromJson(
Map<String, dynamic> json,
) {
return Product(
json['id'] as int,
json['name'] as String,
);
}
}
Use a decoder:
Future<Product?> loadProduct(int id) async {
final response =
await SPNetwork.instance.get<Product>(
'products/$id',
decoder: (json) {
return Product.fromJson(
json as Map<String, dynamic>,
);
},
);
return response.data;
}
Sixplace does not require a specific backend response envelope.
If your server returns:
{
"data": {
"id": 1,
"name": "LED lamp"
}
}
unwrap it inside the decoder:
decoder: (json) {
final map = json as Map<String, dynamic>;
return Product.fromJson(
map['data'] as Map<String, dynamic>,
);
},
Business-level failures returned using HTTP 200 remain application-level business logic.
Endpoint Resolution
Given:
SPNetworkConfig(
baseUrl: 'https://api.example.com/api/v2/',
)
both:
'products'
and:
'/products'
resolve inside:
https://api.example.com/api/v2/
Absolute endpoint URLs are rejected.
For example, this is not allowed:
await SPNetwork.instance.get(
'https://other.example.com/products',
);
Path traversal is also rejected.
This prevents request endpoints from escaping the configured backend.
Query Parameters
await SPNetwork.instance.get(
'products',
queryParameters: {
'page': 1,
'search': 'LED',
'categoryIds': [1, 2, 3],
'optional': null,
},
);
Null query values are omitted.
Iterable values are supported.
For dynamic URL path segments, use:
Uri.encodeComponent(value)
where appropriate.
Authentication
Bearer Token Provider
Applications can supply a token provider:
SPNetwork.init(
config: SPNetworkConfig(
baseUrl: 'https://api.example.com/api/',
tokenProvider: () async {
return readAccessToken();
},
),
);
The provider is evaluated for authenticated requests.
The token is not permanently captured when SPNetwork is initialized.
Keep Token Providers Lightweight
A token provider should normally retrieve an already stored credential.
Recommended:
tokenProvider: () async {
return secureStorage.read(
key: 'access_token',
);
},
Avoid implementing recursive network calls inside tokenProvider.
For example, do not make the token provider call the same authenticated API client in order to obtain its own token.
Authentication refresh workflows should be managed explicitly by the consuming application.
Public Requests
Login, registration, password-reset and similar public requests can disable authentication:
final response =
await SPNetwork.instance.post<Map<String, dynamic>>(
'auth/login',
body: {
'username': username,
'password': password,
},
options: const SPRequestOptions(
authenticated: false,
),
);
After successful login and token storage:
SPNetwork.instance.resetUnauthorizedState();
Unauthorized Session Handling
Configure an unauthorized callback:
SPNetwork.init(
config: SPNetworkConfig(
baseUrl: 'https://api.example.com/api/',
tokenProvider: readAccessToken,
onUnauthorized: (error) async {
await clearAuthSession();
showLogin();
},
),
);
Your application controls:
- Secure storage
- Session removal
- Login navigation
- Toasts/messages
- State reset
Sixplace does not import a navigation or state-management framework.
GetX Example
For example:
void configureApi({
required Future<String?> Function() readAccessToken,
required Future<void> Function() clearAuthSession,
}) {
SPNetwork.init(
config: SPNetworkConfig(
baseUrl: 'https://api.example.com/api/',
tokenProvider: readAccessToken,
onUnauthorized: (error) async {
await clearAuthSession();
Get.offAllNamed(
AppRoutes.authLogin,
);
},
),
);
}
The Sixplace package itself does not depend on GetX.
Authentication Rules
authenticated: falsedoes not call the token provider.authenticated: falseremoves the Authorization header.authenticated: falseskips the unauthorized-session callback.options.tokenoverrides the configured token provider.- An empty token removes bearer authentication.
- An explicitly supplied Authorization header can override provider behavior when no option token is supplied.
- HTTP
401triggersonUnauthorizedonly when Authorization was actually sent. - HTTP
403is treated as forbidden, not automatically as an expired login. - Concurrent
401responses do not repeatedly execute the unauthorized callback. resetUnauthorizedState()re-arms the callback after successful login or an externally completed refresh.- A failed request still throws its original typed exception.
- Sixplace does not automatically refresh a token and replay an authenticated write.
Typed Errors
Sixplace exposes failures through SPNetworkException.
try {
await SPNetwork.instance.get<Object?>(
'profile',
);
} on SPNetworkException catch (error) {
switch (error.type) {
case SPNetworkErrorType.connection:
// Unable to reach the backend.
break;
case SPNetworkErrorType.timeout:
// Request deadline exceeded.
break;
case SPNetworkErrorType.http:
// Server returned a non-2xx status.
break;
case SPNetworkErrorType.cancelled:
// Caller cancelled the request.
break;
default:
break;
}
}
Available error categories:
| Type | Meaning |
|---|---|
http |
Backend returned a non-2xx HTTP status |
authentication |
Token provider failed or did not finish within its deadline |
connection |
Transport failure such as DNS, TLS, socket, CORS or connection loss |
timeout |
Request/send/body-read deadline expired |
cancelled |
Request was cancelled or the network client was closed |
decoding |
Successful response could not be decoded |
responseTooLarge |
Buffered response exceeded the configured maximum |
For HTTP failures, Sixplace retains information such as:
- Status code
- Response headers
- Decoded JSON error body when valid
- Plain-text error body when JSON is unavailable
Example:
try {
await SPNetwork.instance.post<Object?>(
'orders',
body: order,
);
} on SPNetworkException catch (error) {
if (error.type == SPNetworkErrorType.http &&
error.statusCode == 422) {
final validation = error.body;
// Map backend validation to your UI.
}
}
Malformed HTML/plain-text error pages are not incorrectly reported as JSON decoding errors.
Retry Policy
By default, only GET and HEAD requests are eligible for automatic Sixplace retries.
| Setting | Default behavior |
|---|---|
| Eligible methods | GET and HEAD |
| Total attempts | 3 |
| Additional retries | Maximum 2 |
| Retryable transport failures | Connection failure and timeout |
| Retryable HTTP statuses | 408, 429, 502, 503, 504 |
| Backoff | 400 ms, then 800 ms |
| Maximum backoff | 5 seconds |
Retry-After |
Supported for seconds and HTTP dates |
| Timeout | 30 seconds per attempt by default |
| POST retry | Never by Sixplace |
| PUT retry | Never by Sixplace |
| PATCH retry | Never by Sixplace |
| DELETE retry | Never by Sixplace |
| Upload retry | Never by Sixplace |
The retry count is bounded.
Sixplace does not contain a background retry loop.
Disable Retries Globally
SPNetwork.init(
config: SPNetworkConfig(
baseUrl: 'https://api.example.com/',
retryPolicy: SPRetryPolicy.none,
),
);
Disable Retries for One Request
await SPNetwork.instance.get<Object?>(
'report',
options: const SPRequestOptions(
retryPolicy: SPRetryPolicy.none,
),
);
Write Request Safety
Sixplace itself does not automatically retry or replay:
- POST
- PUT
- PATCH
- DELETE
- Multipart uploads
For these methods, one Sixplace API invocation creates one Sixplace transport send attempt.
Example:
await SPNetwork.instance.post(
'sales',
body: sale,
);
Sixplace itself does not transform that call into:
POST
POST
POST
POST
...
Important: Injected HTTP Clients
Sixplace allows advanced users to inject a custom http.Client:
final api = SPNetwork(
config: config,
client: customClient,
);
Sixplace cannot control additional retry/replay behavior implemented internally by that custom client.
For example, a retrying transport can potentially resend a request after Sixplace has handed the request to it.
Therefore:
Sixplace itself never retries POST, PUT, PATCH, DELETE, or multipart uploads. If you inject a custom
http.Client, verify that the injected transport does not independently retry or replay transactional write requests.
For strict transactional behavior, prefer the default Sixplace transport unless you fully understand the behavior of the custom transport being injected.
Avoid Accidental Repeated API Calls
A networking package can send only the requests that the application asks it to send.
Repeated application lifecycle calls can therefore create repeated backend requests even when the networking client contains no automatic polling.
Do Not Start Requests in build()
Avoid:
@override
Widget build(BuildContext context) {
loadProducts();
return const ProductPage();
}
Flutter can call build() many times.
That can unintentionally trigger repeated API traffic.
Instead, start work from an appropriate lifecycle method, controller, event, command, state notifier, or explicitly cached future.
FutureBuilder
Avoid constructing the network Future directly during every rebuild:
FutureBuilder(
future: SPNetwork.instance.get(
'products',
),
builder: ...
)
Prefer storing the Future:
late Future<SPNetworkResponse<Object?>> productsFuture;
@override
void initState() {
super.initState();
productsFuture =
SPNetwork.instance.get<Object?>(
'products',
);
}
Then:
FutureBuilder(
future: productsFuture,
builder: ...
)
The same principle applies to:
- GetX reactive builders
- Bloc builders
- Riverpod providers
- Provider consumers
- Repeated lifecycle callbacks
Do not trigger transactional requests merely because a widget rebuilds.
Prevent Duplicate User Submission
Applications should disable or guard transactional actions while the current operation is in progress.
Example:
bool submitting = false;
Future<void> submitSale() async {
if (submitting) {
return;
}
submitting = true;
try {
await SPNetwork.instance.post<Object?>(
'sales',
body: sale.toJson(),
);
} finally {
submitting = false;
}
}
This prevents repeated taps from creating multiple independent application calls.
This behavior belongs to application/business state, not the network transport.
Transactional Idempotency
Network failures create an important ambiguity.
Consider:
POS SERVER
POST /sale ---------->
Sale committed
connection lost
POS receives timeout
The client cannot always know whether the server committed the transaction before the connection disappeared.
A manual retry could therefore create a duplicate transaction.
For important operations such as:
- POS sales
- Payments
- Invoices
- Stock movements
- Order placement
- Money transfers
- Ledger entries
use backend-supported idempotency.
Example:
await SPNetwork.instance.post<Object?>(
'sales',
body: sale.toJson(),
options: SPRequestOptions(
headers: {
'Idempotency-Key':
sale.localTransactionId,
},
),
);
The backend must actually implement the idempotency contract.
Another common approach is to include a unique client transaction identifier:
{
"clientTransactionId": "SALE-DEVICE01-20260928-000001",
"amount": 1250
}
and enforce uniqueness on the server/database.
Client-side "no automatic retries" alone cannot guarantee exactly-once business execution after an ambiguous network failure.
Timeout Behavior
Default timeout:
const Duration(seconds: 30)
The timeout applies per transport attempt.
Because GET/HEAD can retry, total wall-clock duration can exceed one attempt timeout.
Example:
GET attempt 1
timeout
backoff
GET attempt 2
timeout
backoff
GET attempt 3
Configure a custom timeout globally:
SPNetworkConfig(
baseUrl: 'https://api.example.com/',
timeout: const Duration(seconds: 15),
)
Or per request:
await SPNetwork.instance.get(
'large-report',
options: const SPRequestOptions(
timeout: Duration(seconds: 60),
),
);
A timed-out write request may already have reached and completed on the server.
Do not assume:
timeout == transaction did not happen
Use idempotency or query the backend transaction state when necessary.
Cancellation
Create a cancellation token:
final cancellation = SPCancelToken();
Attach it to a request:
final request =
SPNetwork.instance.get<Object?>(
'report',
options: SPRequestOptions(
cancelToken: cancellation,
),
);
Cancel it:
cancellation.cancel();
Handle cancellation normally:
try {
await request;
} on SPNetworkException catch (error) {
if (error.type ==
SPNetworkErrorType.cancelled) {
return;
}
rethrow;
}
Cancellation interrupts:
- Retry waits
- Request processing
- Response-body reads
Sixplace uses abortable requests where supported by the underlying package:http transport.
Cancellation cannot undo server-side work that has already completed.
Multipart Uploads
await SPNetwork.instance.upload<Map<String, dynamic>>(
'attachments',
fields: {
'description': 'Invoice receipt',
},
files: [
SPUploadFile(
field: 'file',
filename: 'receipt.png',
bytes: selectedImageBytes,
contentType: 'image/png',
),
],
);
Supported upload methods:
- POST
- PUT
- PATCH
Sixplace creates fresh multipart parts for each upload request.
The multipart boundary is generated by the HTTP transport.
Uploads are not automatically retried by Sixplace.
Text Responses
final response =
await SPNetwork.instance.get<String>(
'health',
options: const SPRequestOptions(
responseType: SPResponseType.text,
),
);
print(response.data);
Binary Responses
final response =
await SPNetwork.instance.get<List<int>>(
'report.pdf',
options: const SPRequestOptions(
responseType: SPResponseType.bytes,
),
);
final bytes = response.data;
The current networking implementation buffers response data in memory.
For very large downloads, use a specialized streaming solution until streaming support is added to Sixplace.
Response Size Protection
Sixplace limits buffered response size.
The default maximum response size is:
20 MiB
This limit helps prevent unexpectedly large responses from consuming excessive client memory.
It can be configured using the network configuration when your application requires a different maximum.
Redirect Behavior
Automatic redirects are intentionally disabled.
Use the backend's canonical URL, including the correct trailing slash where required.
This prevents Authorization credentials from being unexpectedly forwarded to another location.
On native transports, redirect responses normally surface as HTTP errors.
Browser behavior can differ because browser security policies are also involved.
Logging
Network logging is disabled unless a logger is configured.
Example:
SPNetworkConfig(
baseUrl: 'https://api.example.com/',
logger: (event) {
debugPrint('$event');
},
)
Sixplace diagnostic events contain only metadata such as:
- HTTP method
- Lifecycle phase
- Attempt number
- Status code
- Error category
Sixplace does not intentionally include:
- Access tokens
- Authorization headers
- Request bodies
- Response bodies
- Query values
- Full request URLs
in normal diagnostic logs.
Application code should still avoid blindly logging exception fields because backend error bodies may contain private business information.
Security
For production applications:
- Use HTTPS.
- Do not disable certificate validation.
- Store credentials using an appropriate secure-storage solution.
- Do not log bearer tokens.
- Do not blindly log API response bodies.
- Keep API authorization rules enforced on the server.
- Use backend-side validation.
- Use idempotency for important transactional operations.
- Use short-lived or otherwise appropriately managed authentication credentials.
- Configure CORS correctly for Flutter Web.
Sixplace does not provide certificate bypass logic.
Platform Notes
Android
Applications using networking need Internet permission.
Most Flutter Android applications already include the appropriate network permission configuration, but verify your final Android manifest.
Development HTTP endpoints may also require cleartext-network configuration.
Production APIs should use HTTPS.
iOS
Use HTTPS unless your application has a justified and correctly configured App Transport Security exception.
macOS
Network-client entitlement may be required depending on application sandbox configuration.
Web
Flutter Web networking remains subject to browser security rules such as:
- CORS
- Mixed-content restrictions
- Forbidden headers
- Browser cookie policies
Sixplace cannot bypass CORS.
Configure CORS on the backend.
Diagnostics and Server Load
Sixplace contains no automatic background polling loop.
The networking package does not automatically issue repeated POST requests.
However, server load can still increase when:
- Application code repeatedly calls an API
- Requests are triggered from widget rebuilds
- Users repeatedly tap a submit action
- A consuming application injects a retrying custom transport
- GET/HEAD retries occur during backend overload
- Many devices retry simultaneously
- Application code implements its own polling
For high-scale deployments, monitor:
- Request rate
- Concurrent requests
- Error rate
- 429 responses
- 503 responses
- Database connection utilization
- API latency
- Memory consumption
- CPU consumption
Backends under load should return suitable HTTP statuses and, where appropriate:
Retry-After
Sixplace respects bounded retry behavior for eligible GET/HEAD requests.
SPNetwork Feature Summary
SPNetwork currently provides:
- Shared or independent HTTP clients
- GET
- POST
- PUT
- PATCH
- DELETE
- HEAD
- JSON request encoding
- JSON response decoding
- Custom model decoders
- Query parameters
- Bearer authentication
- Optional public/unauthenticated requests
- Unauthorized callback
- Bounded GET/HEAD retries
Retry-Afterhandling- Request timeout
- Cancellation
- Multipart upload
- Text responses
- Binary responses
- Typed network exceptions
- Response-size protection
- Backend reachability through
isDeviceOnline() - Safe base-URL resolution
- Redirect rejection
- Minimal diagnostic logging
- Native and browser transport support
Sixplace currently does not provide:
- Automatic token refresh
- Automatic write retry
- Background polling
- HTTP caching
- WebSockets
- Large-file streaming
- Upload progress callbacks
- Download progress callbacks
- General interceptor chains
- Persistent secure token storage implementation
- Application routing
- Feedback inside the network layer (use
SPFeedbackseparately)
These responsibilities remain outside SPNetwork. Use the relevant Sixplace foundation where available, or keep application-specific policy in your app.
SPFeedback
SPFeedback provides framework-native messages, dialogs and loading surfaces without requiring a routing or state-management package.
Toast-like messages
SPFeedback.showToast(
context,
'Connected.',
type: SPFeedbackType.info,
);
showToast uses Flutter's own ScaffoldMessenger rather than a platform-specific native toast, keeping behavior consistent on mobile, web and desktop. For actions or longer messages, use showMessage.
Transient messages
SPFeedback.showMessage(
context,
'Sale saved successfully.',
type: SPFeedbackType.success,
);
Available semantic types are info, success, warning, and error. Messages use ScaffoldMessenger, so they work consistently across Flutter platforms.
Actions are optional:
SPFeedback.showMessage(
context,
'Draft deleted.',
type: SPFeedbackType.warning,
actionLabel: 'Undo',
onAction: restoreDraft,
);
Alerts
final confirmed = await SPFeedback.showAlert(
context,
title: 'Delete invoice?',
message: 'This action cannot be undone.',
confirmLabel: 'Delete',
cancelLabel: 'Cancel',
type: SPFeedbackType.error,
);
if (confirmed == true) {
await deleteInvoice();
}
Blocking loader
final loader = SPFeedback.showLoading(
context,
message: 'Submitting invoice…',
);
try {
await submitInvoice();
} finally {
loader.close();
}
The loader handle removes only the route it created, avoiding accidental pops of unrelated screens. For inline workflows, use SPBlockingLoader directly in the widget tree.
SPTheme
SPTheme builds Material themes and attaches Sixplace semantic design tokens through Flutter's ThemeExtension mechanism.
MaterialApp(
theme: SPTheme.light(
seedColor: const Color(0xFF5B5BD6),
),
darkTheme: SPTheme.dark(
seedColor: const Color(0xFF5B5BD6),
),
home: const HomePage(),
)
Read the current tokens from context:
final tokens = context.spTheme;
final successColor = tokens.success;
final pagePadding = context.spSpacing.md;
final cardRadius = context.spRadius.lg;
final bodySize = context.spTypography.body;
Customize token scales:
final theme = SPTheme.light(
spacing: const SPSpacingTokens(
xs: 4,
sm: 8,
md: 18,
lg: 28,
xl: 36,
xxl: 52,
),
radius: const SPRadiusTokens(
sm: 6,
md: 12,
lg: 18,
xl: 28,
pill: 999,
),
typography: const SPTypographyTokens(
display: 34,
headline: 26,
title: 20,
body: 16,
label: 14,
caption: 12,
),
);
To add Sixplace tokens to an existing ThemeData without replacing other theme configuration:
final theme = SPTheme.withTokens(existingTheme);
SPTheme.light and SPTheme.dark apply the configured SPTypographyTokens to Flutter's TextTheme while preserving each base style's remaining properties. Pass a complete custom textTheme when the application needs full typography control. SPTheme.withTokens preserves an existing TextTheme by default; set applyTypography: true to apply the token sizes.
SPForms
SPForms provides validators, common Material form inputs and guarded asynchronous submission. It does not require GetX, Bloc, Riverpod, Provider, or another state manager.
Validators
final emailValidator = SPValidators.compose<String>([
SPValidators.required(),
SPValidators.email(),
]);
Other built-in validators include:
minLengthmaxLengthpatternminNumbermaxNumbercompose
Inputs
final formKey = GlobalKey<FormState>();
Form(
key: formKey,
child: Column(
children: [
SPTextFormField(
label: 'Email',
keyboardType: TextInputType.emailAddress,
validator: SPValidators.compose([
SPValidators.required(),
SPValidators.email(),
]),
),
SPDropdownFormField<int>(
label: 'Customer type',
items: const [
DropdownMenuItem(value: 1, child: Text('Retail')),
DropdownMenuItem(value: 2, child: Text('Dealer')),
],
onChanged: (value) {},
validator: (value) => value == null ? 'Choose a type.' : null,
),
SPCheckboxFormField(
title: 'I confirm the information is correct',
validator: (value) => value == true ? null : 'Confirmation is required.',
),
],
),
)
Guarded submission
SPFormController.submit validates the form and rejects concurrent submissions, helping prevent duplicate actions caused by repeated taps.
final formController = SPFormController();
Future<void> save() async {
await formController.submit<void>(
formKey: formKey,
context: context,
action: () async {
await SPNetwork.instance.post(
'customers',
body: buildCustomerPayload(),
);
},
);
}
Use a submission-aware button:
SPSubmitButton(
controller: formController,
label: 'Save',
busyLabel: 'Saving…',
onPressed: save,
)
Dispose controllers owned by a State object:
@override
void dispose() {
formController.dispose();
super.dispose();
}
For transaction safety, UI duplicate-submit protection should still be paired with backend idempotency where business operations must not be duplicated.
SPStorage
SPStorage defines stable storage interfaces without forcing a particular persistence or secure-storage plugin on every application. This keeps Sixplace platform-neutral and lets apps choose providers that match their deployment and security requirements.
Storage facade
final storage = SPStorage(
preferences: MyPreferencesAdapter(),
secure: MySecureStorageAdapter(),
);
await storage.writePreference('pageSize', 25);
final pageSize = await storage.readPreference<int>('pageSize');
await storage.writeSecure('accessToken', token);
final savedToken = await storage.readSecure('accessToken');
Implement these interfaces around your preferred persistence packages:
abstract interface class SPPreferencesStore {
Future<Object?> read(String key);
Future<void> write(String key, Object value);
Future<bool> containsKey(String key);
Future<void> remove(String key);
Future<void> clear();
}
abstract interface class SPSecureStore {
Future<String?> read(String key);
Future<void> write(String key, String value);
Future<bool> containsKey(String key);
Future<void> remove(String key);
Future<void> clear();
}
Preference values are intentionally limited to String, bool, int, double, and List<String> so adapters can map cleanly to common preference stores.
In-memory providers
Sixplace includes:
SPMemoryPreferencesStore();
SPMemorySecureStore();
These are useful for tests, previews and ephemeral sessions. SPMemorySecureStore does not provide encrypted persistent storage and should not be represented as an OS-backed secure store.
Bounded cache
SPStorageCache is an in-process LRU-style cache with optional TTL expiration. It creates no background polling timer.
final cache = SPStorageCache(maxEntries: 200);
cache.put(
'dashboard',
dashboardModel,
ttl: const Duration(minutes: 5),
);
final cached = cache.get<DashboardModel>('dashboard');
Expired values are removed lazily during cache access. When the capacity is exceeded, the least recently used entry is evicted.
SPRoom
SPRoom is Sixplace's Room-inspired SQLite layer for structured local data. It follows the familiar Database → Entity → DAO → Migration → Transaction model used by AndroidX Room while using the sqflite ecosystem underneath.
SPRoom is part of Sixplace's storage foundation, but it has a focused import:
import 'package:sixplace/room.dart';
What SPRoom provides
- Declarative table schemas through
SPRoomEntitySchema - Typed model mapping through
SPRoomEntityAdapter<T> - Generic/subclassable
SPRoomDao<T>CRUD APIs insert,insertAll,update,delete,upsert, query and count helpers- Composite and auto-increment primary keys
- Explicit indexes and foreign keys
- Explicit versioned migrations
- Missing-migration failure by default
- Opt-in destructive migration fallback
- Schema validation after create/migrate/open
- Schema identity tracking
- Transaction-aware DAO calls
- Reactive
watch()andwatchRawQuery()streams - Raw SQL escape hatches with explicit invalidation
- In-memory database builders for tests
- sqflite-compatible factory injection for desktop/web/testing
Entity + adapter
class User {
const User({this.id, required this.name});
final int? id;
final String name;
}
const userSchema = SPRoomEntitySchema(
tableName: 'users',
columns: <SPRoomColumn>[
SPRoomColumn(
name: 'id',
type: SPRoomSqlType.integer,
primaryKey: true,
autoIncrement: true,
),
SPRoomColumn(name: 'name', type: SPRoomSqlType.text),
],
indices: <SPRoomIndex>[
SPRoomIndex(name: 'idx_users_name', columns: <String>['name']),
],
);
class UserAdapter extends SPRoomEntityAdapter<User> {
@override
SPRoomEntitySchema get schema => userSchema;
@override
Map<String, Object?> toRow(User user) => <String, Object?>{
'id': user.id,
'name': user.name,
};
@override
User fromRow(Map<String, Object?> row) => User(
id: (row['id'] as num?)?.toInt(),
name: row['name']! as String,
);
}
DAO + database
class UserDao extends SPRoomDao<User> {
UserDao(SPRoomDatabase database) : super(database, UserAdapter());
Future<List<User>> findByName(String text) => query(
where: 'name LIKE ?',
whereArgs: <Object?>['%$text%'],
orderBy: 'name',
);
}
class AppDatabase extends SPRoomDatabase {
late final UserDao users = UserDao(this);
}
Open it once and reuse the instance:
final db = await SPRoom.databaseBuilder<AppDatabase>(
name: 'app.db',
version: 1,
create: AppDatabase.new,
entities: const <SPRoomEntitySchema>[userSchema],
).build();
final id = await db.users.insert(const User(name: 'Mehedi'));
final user = await db.users.findById(id);
Reactive queries
final users = db.users.watch(orderBy: 'name');
StreamBuilder<List<User>>(
stream: users,
builder: (context, snapshot) {
// rebuilds after writes performed through SPRoom
return const SizedBox.shrink();
},
);
SPRoomDao writes automatically invalidate their table. For custom SQL writes, use db.rawWrite(...) or call db.notifyTablesChanged(...) after the write.
Transactions
await db.writeTransaction(() async {
await db.users.insert(firstUser);
await db.users.insert(secondUser);
});
DAO operations automatically reuse the active sqflite transaction. withTransaction() and runInTransaction() are aliases for the same transaction behavior. This prevents the common sqflite deadlock mistake of using the outer Database object from inside a transaction callback.
Migrations
final migration1To2 = SPRoomMigration(1, 2, (db) async {
await db.execute(
'ALTER TABLE users ADD COLUMN email TEXT',
);
});
final db = await SPRoom.databaseBuilder<AppDatabase>(
name: 'app.db',
version: 2,
create: AppDatabase.new,
entities: const <SPRoomEntitySchema>[userSchemaV2],
).addMigrations(<SPRoomMigration>[
migration1To2,
]).build();
If a migration path is missing, SPRoom throws SPRoomMissingMigrationException. fallbackToDestructiveMigration() is intentionally opt-in because it deletes managed data.
Platform database factories
Android, iOS, and macOS use package:sqflite automatically.
For Linux/Windows unit tests or desktop applications, add a compatible sqflite factory such as sqflite_common_ffi in the consuming app and inject it:
sqfliteFfiInit();
final db = await SPRoom.databaseBuilder<AppDatabase>(
name: 'app.db',
version: 1,
create: AppDatabase.new,
entities: const <SPRoomEntitySchema>[userSchema],
factory: databaseFactoryFfi,
).build();
For web, inject a compatible sqflite web factory and complete that factory's required web/wasm setup. Sixplace intentionally does not force desktop/web SQLite binaries into every consuming application.
pub.dev's automatic platform detection can omit Windows and Linux because the
default factory imports the mobile/macOS sqflite plugin. Desktop SPRoom usage
requires an injected factory; its database tests run with sqflite_common_ffi.
The layout, networking, feedback, theme, forms and key/value storage entrypoints
do not require a native SQLite factory.
Important parity note
SPRoom deliberately adopts the architecture and runtime behavior patterns developers know from AndroidX Room, but it is not a port of AndroidX Room and does not claim Kotlin/KSP compile-time code generation. Sixplace 1.0.1 uses explicit Dart schemas/adapters instead of fake annotations or runtime mirrors. SQL inside custom DAO methods is therefore the application's responsibility to test.
SPLayout
The SPLayout foundation provides mobile-first responsive UI primitives for Flutter.
Current layout features include:
- Mobile-first responsive resolution
- Bootstrap-inspired 12-column grid
- Six default breakpoints
- Custom breakpoint support
- Responsive value inheritance
- Viewport information through
context.sp - Automatic responsive width and height
- Fixed and fluid containers
- Responsive container padding
- Wrapping rows with static or responsive gaps
- Responsive column spans
- Responsive offsets
- Responsive visual ordering
- Responsive padding and margin
- Breakpoint-based visibility
- Platform-adaptive application bar
- Responsive application scaffold
- Permanent desktop drawers
- Tablet navigation rails
- Responsive bottom-navigation visibility
- No duplicate responsive widget trees
- No required third-party state manager
Breakpoints
Sixplace uses the following mobile-first breakpoints:
| Breakpoint | Minimum width | Effective range |
|---|---|---|
xs |
0 px | 0–575.99 px |
sm |
576 px | 576–767.99 px |
md |
768 px | 768–991.99 px |
lg |
992 px | 992–1199.99 px |
xl |
1200 px | 1200–1399.99 px |
xxl |
1400 px | 1400 px and above |
Responsive values inherit forward until overridden.
Example:
SPCol(
sm: 6,
lg: 4,
child: content,
)
This produces:
- 12 columns on
xs - 6 columns on
smandmd - 4 columns on
lg,xl, andxxl
Responsive Two-Column Layout
import 'package:flutter/material.dart';
import 'package:sixplace/sixplace.dart';
class DashboardPage extends StatelessWidget {
const DashboardPage({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const SPAppBar(
title: 'Dashboard',
),
body: SingleChildScrollView(
child: SPContainer.fluid(
padding: const EdgeInsets.all(16),
child: const SPRow(
gap: 16,
children: [
SPCol(
md: 6,
child: Card(
child: Padding(
padding: EdgeInsets.all(20),
child: Text('First card'),
),
),
),
SPCol(
md: 6,
child: Card(
child: Padding(
padding: EdgeInsets.all(20),
child: Text('Second card'),
),
),
),
],
),
),
),
);
}
}
Conceptually:
<div class="row">
<div class="col-12 col-md-6">
First card
</div>
<div class="col-12 col-md-6">
Second card
</div>
</div>
Three-Column Responsive Layout
SPRow(
gap: 16,
children: [
SPCol(
sm: 6,
lg: 4,
child: firstCard,
),
SPCol(
sm: 6,
lg: 4,
child: secondCard,
),
SPCol(
sm: 6,
lg: 4,
child: thirdCard,
),
],
)
Result:
- One column on extra-small screens
- Two columns from
sm - Three columns from
lg
Responsive Dashboard Cards
SPRow(
responsiveGap: const SPResponsiveValue<double>(
base: 8,
md: 12,
lg: 16,
),
children: [
SPCol(
span: 12,
sm: 6,
xl: 3,
child: firstCard,
),
SPCol(
span: 12,
sm: 6,
xl: 3,
child: secondCard,
),
SPCol(
span: 12,
sm: 6,
xl: 3,
child: thirdCard,
),
SPCol(
span: 12,
sm: 6,
xl: 3,
child: fourthCard,
),
],
)
Responsive Values
SPResponsiveValue<T> stores one value that changes across breakpoints.
const spacing =
SPResponsiveValue<double>(
base: 8,
md: 16,
xl: 24,
);
This resolves to:
8onxsandsm16onmdandlg24onxlandxxl
Missing breakpoint values inherit from the nearest configured smaller breakpoint.
Resolve from Context
final borderRadius =
const SPResponsiveValue<double>(
base: 8,
md: 12,
lg: 16,
).resolveFrom(context);
This avoids application-level MediaQuery boilerplate for many responsive properties.
Responsive Width and Height
SPResponsiveSizedBox(
height: const SPResponsiveValue<double?>(
base: null,
lg: 350,
),
child: StockOverviewChart(),
)
Below lg, natural child height is used.
From lg, the height becomes 350.
Width and height can be combined:
SPResponsiveSizedBox(
width: const SPResponsiveValue<double?>(
base: double.infinity,
lg: 480,
),
height: const SPResponsiveValue<double?>(
base: 240,
lg: 350,
),
child: content,
)
Viewport Information
Use:
final responsive = context.sp;
Available information includes:
responsive.width;
responsive.height;
responsive.breakpoint;
responsive.isMobile;
responsive.isTablet;
responsive.isDesktop;
responsive.isLargeDesktop;
Device-category behavior:
isMobile:xsandsmisTablet:mdisDesktop:lgand aboveisLargeDesktop:xxland above
isDesktop remains true on larger desktop breakpoints.
Explicit breakpoint checks are also supported:
if (context.sp.atLeast(
SPBreakpoint.lg,
)) {
// Desktop-specific behavior.
}
if (context.sp.below(
SPBreakpoint.md,
)) {
// Small-screen behavior.
}
Prefer layout widgets such as SPRow, SPCol, SPVisibility, and SPScaffold for structural layout changes instead of rebuilding complete mobile and desktop pages.
Responsive Container Padding
SPContainer.fluid(
responsivePadding:
const SPResponsiveValue<EdgeInsetsGeometry>(
base: EdgeInsets.all(8),
md: EdgeInsets.all(12),
lg: EdgeInsets.all(16),
),
child: content,
)
Static padding remains supported:
SPContainer.fluid(
padding: const EdgeInsets.all(16),
child: content,
)
When responsivePadding is provided, it takes precedence over static padding.
Responsive Row Gaps
Static gap:
SPRow(
gap: 16,
children: columns,
)
Responsive gap:
SPRow(
responsiveGap:
const SPResponsiveValue<double>(
base: 8,
md: 12,
lg: 16,
),
children: columns,
)
Explicit horizontal or vertical values can override the responsive gap for that axis.
SPRow(
horizontalGap: 20,
responsiveGap:
const SPResponsiveValue<double>(
base: 8,
lg: 16,
),
children: columns,
)
In this example:
- Horizontal gap remains
20 - Vertical gap changes responsively
Responsive Offset
SPRow(
children: [
SPCol(
span: 12,
md: 8,
offsetMd: 2,
child: centeredContent,
),
],
)
From md, the content occupies eight columns with two empty columns before it.
Responsive Visual Order
SPRow(
gap: 16,
children: [
SPCol(
span: 12,
lg: 8,
order: 2,
orderLg: 1,
child: mainContent,
),
SPCol(
span: 12,
lg: 4,
order: 1,
orderLg: 2,
child: sidebar,
),
],
)
The sidebar appears first on smaller screens and after the main content from lg.
Responsive Visibility
SPVisibility(
hiddenXs: true,
hiddenSm: true,
child: desktopAction,
)
Use replacements where necessary:
SPVisibility(
hiddenLg: true,
hiddenXl: true,
hiddenXxl: true,
replacement:
const Text('Desktop navigation'),
child:
const Text('Mobile navigation'),
)
Avoid using visibility to maintain two complete copies of the same page.
Responsive Padding
SPPadding(
base: const EdgeInsets.all(8),
md: const EdgeInsets.all(16),
lg: const EdgeInsets.all(24),
child: content,
)
Responsive Margin
SPMargin(
base: const EdgeInsets.only(
bottom: 8,
),
md: const EdgeInsets.only(
bottom: 16,
),
child: content,
)
Platform-Adaptive App Bar
Scaffold(
appBar: const SPAppBar(
title: 'Sixplace',
),
body: const YourPageContent(),
)
SPAppBar provides:
- Material styling on Android, web and desktop
- iOS-style presentation on iOS
- Dark-mode-aware colors
- Automatic back-navigation handling
- Optional drawer-button handling
Override title alignment when required:
SPAppBar(
title: 'Sixplace',
centerTitle: false,
)
Show a drawer button:
SPAppBar(
title: 'Dashboard',
showDrawerIcon: true,
onDrawerPressed: openDrawer,
)
Responsive Application Shell
SPScaffold(
appBar: const SPAppBar(
title: 'Dashboard',
),
drawer: const AppDrawer(),
permanentDrawerBreakpoint:
SPBreakpoint.lg,
navigationRail:
const AppNavigationRail(),
navigationRailBreakpoint:
SPBreakpoint.md,
bottomNavigationBar:
const AppBottomNavigation(),
hideBottomNavBreakpoint:
SPBreakpoint.md,
body: const DashboardContent(),
)
Typical behavior:
- Mobile: fly-out drawer and bottom navigation
- Tablet: navigation rail
- Desktop: permanent drawer
- Navigation rail hides when the permanent drawer becomes active
- Bottom navigation hides from the configured breakpoint
Real-World Chart Layout
SPContainer.fluid(
responsivePadding:
const SPResponsiveValue<EdgeInsetsGeometry>(
base: EdgeInsets.all(8),
lg: EdgeInsets.all(16),
),
child: SPRow(
responsiveGap:
const SPResponsiveValue<double>(
base: 12,
lg: 16,
),
children: [
SPCol(
span: 12,
lg: 6,
child: SPResponsiveSizedBox(
height:
const SPResponsiveValue<double?>(
base: null,
lg: 350,
),
child:
InventoryCompositionChart(),
),
),
SPCol(
span: 12,
lg: 6,
child: SPResponsiveSizedBox(
height:
const SPResponsiveValue<double?>(
base: null,
lg: 350,
),
child:
StockOverviewChart(),
),
),
],
),
)
This provides:
- One chart per row below
lg - Two charts per row from
lg - Responsive page padding
- Responsive grid gaps
- Automatic chart-height resolution
- No duplicate desktop/mobile widget trees
Custom Breakpoints
const customBreakpoints =
SPBreakpoints(
sm: 500,
md: 700,
lg: 900,
xl: 1100,
xxl: 1300,
);
Apply to a grid:
SPRow(
breakpoints: customBreakpoints,
children: [
SPCol(
md: 6,
child: content,
),
],
)
Apply to viewport information:
final responsive =
SPResponsive.of(
context,
breakpoints: customBreakpoints,
);
Apply to responsive sizing:
SPResponsiveSizedBox(
breakpoints: customBreakpoints,
height:
const SPResponsiveValue<double?>(
base: null,
lg: 350,
),
child: content,
)
Keep related widgets on the same breakpoint configuration for predictable behavior.
Numeric Design Scaling
Sixplace also includes numeric design-scaling helpers.
The current implementation uses uncapped design scaling.
Width
n.w = n × (logicalScreenWidth / designWidth)
Height
n.h = n × (logicalScreenHeight / designHeight)
These calculations use logical Flutter pixels rather than physical device pixels.
Initialize:
SixPlace.init(
context,
designSize:
const Size(390, 844),
rootFontSize: 16,
);
Use:
final cardWidth = 100.w;
final heroHeight = 48.h;
Use the responsive grid for structural desktop/web layouts rather than relying only on design scaling.
Typography
The numeric typography API includes accessibility scaling.
Conceptually:
n.sp =
textScaler.scale(
n × fontScale
)
and:
n.rem =
textScaler.scale(
n × rootFontSize × fontScale
)
Example:
Text(
'Large heading',
style: TextStyle(
fontSize: 16.sp,
),
textScaler:
TextScaler.noScaling,
)
The returned numeric value already contains the accessibility scaling calculated by Sixplace.
Using TextScaler.noScaling with a font size produced by .sp or .rem prevents Flutter from applying an additional scale to that already scaled value.
Sixplace does not globally disable system text scaling.
Parent-Relative em
DefaultTextStyle(
style: const TextStyle(
fontSize: 18,
),
child: Builder(
builder: (textContext) {
return Text(
'Inherited size',
style: TextStyle(
fontSize:
1.5.em(textContext),
),
textScaler:
TextScaler.noScaling,
);
},
),
)
If no inherited font size is available, Sixplace falls back to the configured root font size.
Public API Overview
| API | Responsibility |
|---|---|
SPBreakpoint |
Active responsive tier |
SPBreakpoints |
Breakpoint boundaries and resolution |
SPResponsiveValue<T> |
Breakpoint-dependent values |
SPResponsive |
Viewport and device-category information |
context.sp |
Convenient responsive context access |
SPContainer |
Fixed/fluid responsive containers |
SPRow |
Wrapping 12-column responsive row |
SPCol |
Responsive span, offset and order |
SPResponsiveSizedBox |
Responsive width/height |
SPPadding |
Responsive internal spacing |
SPMargin |
Responsive external spacing |
SPVisibility |
Breakpoint-based visibility |
SPAppBar |
Platform-adaptive application bar |
SPScaffold |
Responsive application shell |
SPNetwork |
Network client |
SPNetworkConfig |
Network configuration |
SPRequestOptions |
Per-request options |
SPNetworkResponse<T> |
Successful response model |
SPNetworkException |
Typed network failure |
SPRetryPolicy |
Bounded GET/HEAD retry policy |
SPCancelToken |
Request cancellation |
SPUploadFile |
Multipart file model |
isDeviceOnline() |
Backend reachability check |
SPFeedback |
Messages, alerts and blocking loaders |
SPFeedbackLoaderHandle |
Route-specific loader lifecycle |
SPTheme |
Light/dark theme factories and token installation |
SPThemeTokens |
Semantic colors, spacing and radius tokens |
SPSpacingTokens |
Spacing design-token scale |
SPRadiusTokens |
Radius design-token scale |
SPTypographyTokens |
Typography design-token scale |
SPValidators |
Reusable form validators |
SPFormController |
Validation and guarded async submission |
SPTextFormField |
Text input wrapper |
SPDropdownFormField<T> |
Dropdown form input |
SPCheckboxFormField |
Boolean form input |
SPSubmitButton |
Submission-aware Material button |
SPStorage |
Preference, secure-store and cache facade |
SPPreferencesStore |
Preference persistence abstraction |
SPSecureStore |
Secure string persistence abstraction |
SPStorageCache |
Bounded in-memory TTL/LRU cache |
SPRoom |
Room-inspired SQLite database builder |
SPRoomDatabase |
Base database lifecycle and transaction owner |
SPRoomEntitySchema |
Declarative SQLite table schema |
SPRoomEntityAdapter<T> |
Typed entity/row mapper |
SPRoomDao<T> |
CRUD, query, upsert and reactive DAO base |
SPRoomMigration |
Explicit schema-version migration |
SPRoomInvalidationTracker |
Reactive table-change stream |
Resolution Behavior
Sixplace intentionally distinguishes viewport-responsive and parent-responsive behavior.
| API | Resolution basis |
|---|---|
context.sp |
Viewport width |
SPResponsiveValue.resolveFrom(context) |
Viewport width |
SPResponsiveSizedBox |
Viewport width |
SPPadding |
Viewport width |
SPMargin |
Viewport width |
SPVisibility |
Viewport width |
SPScaffold |
Viewport width |
SPContainer |
Immediate parent width |
SPRow |
Immediate parent width |
SPCol inside SPRow |
SPRow parent width |
This allows nested grids to adapt to actual available space while screen-level decisions remain based on the viewport.
Performance
Sixplace uses Flutter-native primitives such as:
LayoutBuilderwhere immediate parent width is requiredMediaQuery.sizeOfwhere viewport information is required- Immutable responsive configuration objects
- Breakpoint inheritance
- Standard Flutter widgets
- Persistent
http.Clientreuse inSPNetwork
Recommended practices:
- Declare responsive values
constwhere possible. - Avoid unnecessary deeply nested responsive wrappers.
- Use lazy lists such as
ListView.builder. - Avoid maintaining duplicate full-page widget trees.
- Avoid starting API requests from
build(). - Reuse
SPNetworkclients. - Guard write buttons against duplicate submission.
- Use backend idempotency for critical transactional writes.
Runnable Example and Tests
Run the application example:
cd example
fvm flutter pub get
fvm flutter run -d chrome
If your checkout includes the dedicated networking demo:
cd example
fvm flutter run \
-t lib/network_main.dart \
-d chrome
The networking example can use an injected mock transport and therefore does not require a real backend, API key, or production account.
Run the feedback/theme/forms/storage demo:
cd example
fvm flutter run \
-t lib/foundations_main.dart \
-d chrome
The storage portion uses only the included in-memory adapters, so the demo needs no platform-specific storage plugin configuration.
Run the dedicated SPRoom example on Android, iOS, or macOS (where sqflite is automatic):
cd example
fvm flutter run -t lib/room_main.dart -d <device>
For Linux/Windows/web, inject a compatible sqflite database factory as described in the SPRoom platform section.
Testing
From the package root:
fvm flutter pub get
fvm dart format .
fvm flutter analyze
fvm flutter test
Network-only tests:
fvm flutter test test/network
SPRoom tests use a sqflite-compatible FFI factory and cover CRUD, transactions, reactive invalidation, and migration/schema validation.
The networking tests should cover areas including:
- GET requests
- POST requests
- PUT requests
- PATCH requests
- DELETE requests
- HEAD requests
- JSON encoding
- JSON decoding
- Typed models
- Authentication
- Unauthorized handling
- URL resolution
- Query parameters
- Retries
- No replay of writes
- Cancellation
- Timeout behavior
- Multipart uploads
- Response-size limits
isDeviceOnline()- Single-attempt reachability behavior
Before Publishing to pub.dev
pub.dev's scoring model evolves over time, so do not optimize for a hard-coded point total. Validate the categories pub.dev currently evaluates: package file conventions and secure metadata URLs, API/example documentation, detected platform support (including modern web/Wasm readiness where applicable), static analysis, and compatibility with current SDKs/dependencies.
For the 1.0.0 release, run the complete validation sequence from the package
root:
fvm flutter pub get
fvm dart format --set-exit-if-changed .
fvm flutter analyze
fvm flutter test
fvm flutter pub outdated
fvm dart pub publish --dry-run
Carefully review the dry-run file list. A library package should not accidentally publish local build output, editor caches, credentials, generated temporary files, or other development-only artifacts.
pub.dev calculates pub points with pana. Run the latest pana against a
copy of the package because the analyzer may modify the directory it checks:
fvm dart pub global activate pana
fvm dart pub global run pana <path-to-a-copy-of-sixplace>
Before publishing, verify all of these release gates:
version -> pubspec.yaml and CHANGELOG.md agree on 1.0.0
format -> pass with no generated diff
analyze -> no errors, warnings, or unexpected lints
tests -> all pass
outdated -> reviewed; supported dependency constraints are current
platforms -> verify Sixplace core remains Android/iOS/Linux/macOS/web/Windows compatible; verify SPRoom default/injected factory notes are accurate
docs -> README example + public API documentation remain available
metadata -> LICENSE present; homepage/repository/issue URLs use valid HTTPS
pana -> no unexpected pub-point findings
dry-run -> no warnings and only intended publish files
Only after those checks pass, publish the immutable 1.0.0 release:
fvm dart pub publish
Migration
Existing Sixplace layout code remains compatible with the current layout API.
For example:
SPContainer.fluid(
padding:
const EdgeInsets.all(16),
child: SPRow(
gap: 16,
children: [
SPCol(
md: 6,
child: firstCard,
),
SPCol(
md: 6,
child: secondCard,
),
],
),
)
New APIs are additive and can be adopted gradually.
Networking is also independently importable:
import 'package:sixplace/network.dart';
Existing layout-only applications do not need to initialize SPNetwork or SPRoom. Existing sqflite applications can adopt SPRoom incrementally by modeling the current schema first and introducing explicit migrations only for future schema versions.
Design Principles
Sixplace follows these principles:
- Mobile-first responsive behavior
- One widget tree for every screen size
- Minimal and readable public APIs
- Predictable breakpoint inheritance
- Flutter-first implementation
- Avoid unnecessary runtime dependencies
- Tested public behavior
- Backward-compatible APIs whenever practical
- Clear viewport/parent-width semantics
- UI-independent networking
- Bounded and predictable retry behavior
- No automatic replay of transactional writes by Sixplace
- Separation of transport, application state and business logic
- Reduced cognitive complexity instead of line-count optimization
- Versioned, explicit relational schema evolution for SPRoom
- No destructive database migration unless the application opts in
Development
Format and validate before submitting changes:
fvm dart format .
fvm flutter analyze
fvm flutter test
Run the visual example:
cd example
fvm flutter pub get
fvm flutter run -d chrome
Resize the browser window to verify responsive transitions.
Contributing
Contributions are welcome.
You can contribute by:
- Reporting bugs
- Suggesting focused features
- Improving documentation
- Adding tests
- Fixing reproducible issues
- Improving performance
- Improving accessibility
- Submitting carefully scoped pull requests
Recommended workflow:
git checkout -b feature/short-description
fvm dart format .
fvm flutter analyze
fvm flutter test
Then submit a pull request with a clear description of the change.
Large features or architectural changes should preferably be discussed before implementation.
Pull requests should be evaluated for:
- Correctness
- Test coverage
- Public API consistency
- Maintainability
- Backward compatibility
- Security
- Predictable runtime behavior
- Alignment with the Sixplace architecture
By contributing, you agree that your contribution may be distributed under the project's MIT License.
Acknowledgements
SPRoom is an independent Sixplace implementation inspired by the architecture and developer experience of AndroidX Room (Kotlin/Android Room). Special thanks to the Room team and Kotlin/Android Room ecosystem for the clear Database / Entity / DAO / Migration / Transaction model that informed SPRoom's design.
SPRoom is powered by the sqflite ecosystem for SQLite access in Flutter/Dart. Special thanks to the sqflite maintainers and contributors for the stable database API, transaction support, version-management hooks, and cross-platform factory ecosystem that make this layer possible.
- AndroidX Room: https://developer.android.com/training/data-storage/room
- sqflite: https://pub.dev/packages/sqflite
Sixplace is not affiliated with, sponsored by, or endorsed by Google/AndroidX or the sqflite maintainers.
Bootstrap Inspiration
Sixplace is inspired by responsive-grid concepts popularized by Bootstrap.
Sixplace is an independent Flutter implementation and is not affiliated with, sponsored by, or endorsed by Bootstrap or its maintainers.
The implementation is written independently in Dart and Flutter.
License
Sixplace is available under the MIT License.
Maintainer
Innovate Nest Labs
Digital Solutions, Bangladesh
Libraries
- device_info
- Permission-free device details and runtime status.
- feedback
- Sixplace's cross-platform feedback foundation.
- forms
- Sixplace's cross-platform forms foundation.
- layout
- network
- Sixplace's UI-independent HTTP networking foundation.
- room
- Sixplace's Room-inspired SQLite persistence layer.
- sixplace
- Complete Sixplace public API.
- sixplace_numeric
- storage
- Sixplace's cross-platform storage and cache abstraction foundation.
- theme
- Sixplace's cross-platform theme and design-token foundation.