flutter_web_storage 1.0.1
flutter_web_storage: ^1.0.1 copied to clipboard
A production-grade, WASM-ready Flutter storage package with synchronous hydration, reactive streams, route preservation, and reload-safe UI controllers.
flutter_web_storage #
A production-grade, WASM-Ready, and Multiplatform-Safe Flutter package for accessing localStorage and sessionStorage with zero UI flicker. Features reactive streams, deep link route preservation on browser refresh (F5), and drop-in reload-safe UI controllers.
Showcase #

Core Architecture and Design Philosophy #
In the Flutter ecosystem, different storage packages are designed for different optimal use cases. Understanding their trade-offs helps select the right tool for the web platform:
- Standard Asynchronous Storage (e.g. SharedPreferences): Designed for simple key-value settings. On mobile, it writes to disk asynchronously, which is ideal for native threads. On web, the asynchronous initialization can cause the UI to build before the state is recovered, resulting in a visible layout flicker.
- Encrypted Key-Value Storage (e.g. Secure Storage): Designed for sensitive tokens. It binds to native device keychains (like iOS Keychain and Android Keystore). On web, since there is no native hardware security keychain, it falls back to unencrypted storage or WebCrypto, which operates asynchronously and cannot prevent access from malicious scripts running on the same origin.
- Relational and Object Databases (e.g. SQLite, Isar, ObjectBox): Designed for complex querying, massive datasets, and relations. On web, they run on IndexedDB or custom WASM compilations. While excellent for offline-first data, they carry substantial bundle size overhead and are complex for simple tab-scoped session caching.
- flutter_web_storage: Optimized specifically for instant, tab-isolated, zero-flicker key-value state hydration on the web using direct synchronous JS-Interop.
Technical Comparison Matrix #
| Feature / Metric | Standard Async Storage | Encrypted Key-Value | Relational & Object DBs | flutter_web_storage |
|---|---|---|---|---|
| Primary Use Case | Basic key-value settings | Secure credentials | Heavy relational datasets | Web state persistence |
| API Synchronicity | Asynchronous (Future) | Asynchronous (Future) | Asynchronous (Future/Stream) | Synchronous (JS-Interop) |
| Startup UI Flicker | High | High | High | Zero (Instant hydration) |
| Tab Isolation | No | No | No | Yes (sessionStorage) |
| WASM Performance | Standard bridge | Crypto overhead | Heavy WASM engine | Native interop (zero cost) |
| Native Portability | Disk serialization | Keychain integration | Native binary engine | In-memory fallback stub |
Technical Trade-Off Explanations #
1. The Startup UI Flicker Problem
When a user refreshes a web page (F5), standard Flutter code initializes state variables to their defaults. Because standard asynchronous storage utilities rely on asynchronous read operations, the UI is drawn with default values before the stored value is retrieved. This latency results in a visible UI flash or flicker.
flutter_web_storage solves this by using synchronous Dart-JS interop bindings. Since read operations map directly to synchronous browser API calls (window.localStorage.getItem), the state hydrates instantly inside the constructor or initState, bypassing asynchronous microtasks.
2. Over-engineering and Setup Overhead
Standard relational/object databases are powerful database engines but introduce complexity for basic key-value storage. They require asynchronous path registration, database initialization, and code-generation configurations for custom models.
flutter_web_storage provides lightweight, codegen-free JSON serialization, allowing direct storage of maps and lists without setup routines.
3. Session Isolation
Standard shared storage libraries only write to permanent browser storage. flutter_web_storage natively separates data between localStorage (permanent across browser restarts) and sessionStorage (isolated per tab, survives F5 reload, but perishes once the tab is closed).
Architectural Flow #
Data Hydration Flow (Zero-Flicker) #
[Flutter Widget (UI)] --(1. Hydrate state)--> [FlutterWebStorage Singleton]
|
(2. Synchronous read)
v
[Browser Storage Engine] <--(3. Direct JS-Interop)--> [Storage Driver]
Multiplatform Target Resolution #
[Compilation Target]
|
+----------------+----------------+
| |
v v
[Web Platform] [Native Platforms]
| |
(Loads storage_web.dart) (Loads storage_stub.dart)
| |
v v
[Browser Storage Engine] [In-Memory Map Fallback]
Browser Storage Security and Web Permissions #
Standard browser storage APIs (localStorage and sessionStorage) operate inside the browser's sandboxed security model.
- Permissions: Unlike camera, microphone, or location APIs, browser storage does not require user permission prompts. It is granted automatically to every website.
- Origin Sandbox: Storage is bound to the origin (Protocol + Domain + Port). Code running on https://example.com cannot read storage written by https://another-domain.com.
- Storage Quota: Browsers typically limit storage to 5MB to 10MB per origin. Storing massive data payloads may trigger a quota exceeded exception.
Installation #
Add the dependency to your pubspec.yaml:
dependencies:
flutter_web_storage: ^1.0.0
Detailed Function and API Reference #
All functions are accessed via the FlutterWebStorage.instance singleton. You can specify StorageArea.local (localStorage) or StorageArea.session (sessionStorage).
1. Primitive Read/Write Methods #
getString / setString
Retrieves or stores a raw text value.
void setString(String key, String value, {StorageArea area = StorageArea.session});
String? getString(String key, {StorageArea area = StorageArea.session});
Example:
final storage = FlutterWebStorage.instance;
storage.setString('username', 'Alex', area: StorageArea.local);
String? name = storage.getString('username', area: StorageArea.local);
getInt / setInt
Retrieves or stores an integer. Performs string conversion under the hood.
void setInt(String key, int value, {StorageArea area = StorageArea.session});
int? getInt(String key, {StorageArea area = StorageArea.session});
Example:
storage.setInt('app_counter', 10, area: StorageArea.local);
int? counter = storage.getInt('app_counter', area: StorageArea.local);
getDouble / setDouble
Retrieves or stores a floating-point number.
void setDouble(String key, double value, {StorageArea area = StorageArea.session});
double? getDouble(String key, {StorageArea area = StorageArea.session});
Example:
storage.setDouble('app_rating', 4.5);
double? rating = storage.getDouble('app_rating');
getBool / setBool
Retrieves or stores a boolean value.
void setBool(String key, bool value, {StorageArea area = StorageArea.session});
bool? getBool(String key, {StorageArea area = StorageArea.session});
Example:
storage.setBool('theme_dark', true);
bool? isDark = storage.getBool('theme_dark');
2. Lists & Arrays #
getStringList / setStringList
Serializes and deserializes a list of strings using JSON encoding.
void setStringList(String key, List<String> value, {StorageArea area = StorageArea.session});
List<String>? getStringList(String key, {StorageArea area = StorageArea.session});
Example:
storage.setStringList('user_tags', ['Flutter', 'WebAssembly']);
List<String>? tags = storage.getStringList('user_tags');
getObjectList / setObjectList
Persists a list of custom objects by serializing each object through a toJson map callback, and reconstructing them via a fromJson constructor.
void setObjectList<T>(
String key,
List<T> list,
Map<String, dynamic> Function(T item) toJson, {
StorageArea area = StorageArea.session,
});
List<T>? getObjectList<T>(
String key,
T Function(Map<String, dynamic> json) fromJson, {
StorageArea area = StorageArea.session,
});
Example:
class Task {
final String id;
final String title;
Task(this.id, this.title);
Map<String, dynamic> toJson() => {'id': id, 'title': title};
factory Task.fromJson(Map<String, dynamic> json) => Task(json['id'], json['title']);
}
// Write object list
final tasks = [Task('1', 'Fix bugs'), Task('2', 'Review code')];
storage.setObjectList<Task>('todo_list', tasks, (t) => t.toJson());
// Read object list
List<Task>? retrievedTasks = storage.getObjectList<Task>('todo_list', Task.fromJson);
3. JSON & Custom Models #
getJson / setJson
Stores or retrieves a raw JSON map structure.
void setJson(String key, Map<String, dynamic> value, {StorageArea area = StorageArea.session});
Map<String, dynamic>? getJson(String key, {StorageArea area = StorageArea.session});
Example:
final profile = {'username': 'Alex', 'role': 'Admin'};
storage.setJson('user_profile', profile);
Map<String, dynamic>? data = storage.getJson('user_profile');
getObject / setObject
Saves or loads a single custom model object.
void setObject<T>(
String key,
T object,
Map<String, dynamic> Function(T item) toJson, {
StorageArea area = StorageArea.session,
});
T? getObject<T>(
String key,
T Function(Map<String, dynamic> json) fromJson, {
StorageArea area = StorageArea.session,
});
Example:
final user = User(id: '101', name: 'Frank');
storage.setObject<User>('current_user', user, (u) => u.toJson());
User? activeUser = storage.getObject<User>('current_user', User.fromJson);
4. Reactive Streams #
watchString
Returns a stream emitting value updates for a specific key. It triggers both when updates occur in the current tab and when updates sync from other open browser tabs.
Stream<String?> watchString(String key, {StorageArea area = StorageArea.session});
Example:
StreamBuilder<String?>(
stream: storage.watchString('live_status'),
builder: (context, snapshot) {
return Text('Status: ${snapshot.data ?? ""}');
},
);
watchJson
Listens to map values reactively.
Stream<Map<String, dynamic>?> watchJson(String key, {StorageArea area = StorageArea.session});
watch<T>
Listens to custom model states reactively, reconstructing the type via the optional decoder callback.
Stream<T?> watch<T>(
String key, {
T Function(dynamic raw)? decoder,
StorageArea area = StorageArea.session,
});
5. Utility Functions #
containsKey
Checks if a key exists in the storage area.
bool containsKey(String key, {StorageArea area = StorageArea.session});
remove
Deletes a specific key.
void remove(String key, {StorageArea area = StorageArea.session});
clear
Deletes all values in the targeted area.
void clear({StorageArea area = StorageArea.session});
getKeys
Retrieves a set of all keys present in the specified area.
Set<String> getKeys({StorageArea area = StorageArea.session});
6. Navigation Route Preservation (Navigator 2.0 & GoRouter) #
To preserve the navigation state on browser reload, attach WebRoutePreserverNavigatorObserver to your navigator observers.
MaterialApp(
navigatorObservers: [WebRoutePreserverNavigatorObserver()],
initialRoute: getRestoredRoute(defaultRoute: '/'),
routes: {
'/': (context) => const StorageDashboard(),
'/subpage': (context) => const SubPageDemo(),
},
onUnknownRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) => const StorageDashboard(),
);
},
);
7. Drop-in UI Controllers #
ReloadSafeTextEditingController
A TextEditingController subclass that auto-saves user typing into sessionStorage so it survives reloads.
late final ReloadSafeTextEditingController _inputController;
@override
void initState() {
super.initState();
_inputController = ReloadSafeTextEditingController(
key: 'profile_draft',
defaultValue: 'John Doe',
);
}
ReloadSafeNotifier<T>
A ValueNotifier<T> subclass that auto-persists state modifications. Supports custom object serialization.
late final ReloadSafeNotifier<int> _notifier;
@override
void initState() {
super.initState();
_notifier = ReloadSafeNotifier<int>(
key: 'live_counter',
defaultValue: 0,
area: StorageArea.local,
);
}
Author #
Developed and maintained by Muhammad Omar.
- Website: momarkhan.com
- LinkedIn: Muhammad Omar
Contributions #
Contributions are welcome! If you find a bug, have a feature request, or want to contribute code, please follow these steps:
- Issues: Open an issue on GitHub to discuss bugs or feature suggestions.
- Pull Requests:
- Fork the repository.
- Create a feature branch (
git checkout -b feature/amazing-feature). - Run
flutter analyzeto ensure no lint warnings or compilation errors exist. - Run
flutter testto verify that all unit and widget tests pass. - Commit your changes (
git commit -m 'Add amazing feature'). - Push to the branch (
git push origin feature/amazing-feature). - Open a Pull Request on the main repository.
License #
This project is licensed under the MIT License - see the LICENSE file for details.