native_datastore 1.3.2 copy "native_datastore: ^1.3.2" to clipboard
native_datastore: ^1.3.2 copied to clipboard

A modern Flutter plugin for persistent key-value storage. Uses Android Jetpack DataStore on Android and UserDefaults on iOS. A type-safe, async-first alternative to shared_preferences.

native_datastore #

Pub Version Pub Points Pub Likes Pub Popularity License: BSD-3 Flutter GitHub issues GitHub stars

A modern Flutter plugin for persistent key-value storage, powered by platform-native APIs.

Platform Backend
Android Jetpack DataStore (Preferences)
iOS UserDefaults

Features #

  • Jetpack DataStore on Android -- Google's officially recommended replacement for SharedPreferences. Async, non-blocking, coroutine-based I/O.
  • 8 supported data types -- String, bool, int, double, List<String>, Uint8List, DateTime, and Map<String, dynamic>.
  • Drop-in replacement -- Same getter/setter pattern as shared_preferences. Zero learning curve.
  • Type-safe platform communication -- Built with Pigeon. No hand-written method channels, no string-based lookups.
  • Cross-platform -- One Dart API for Android and iOS.
  • Lightweight -- No platform interface layer. A thin, clean bridge to native APIs.

Supported Types #

Dart Type Getter Setter Nullable
String getString(key) setString(key, value) Yes
bool getBool(key) setBool(key, value) Yes
int getInt(key) setInt(key, value) Yes
double getDouble(key) setDouble(key, value) Yes
List<String> getStringList(key) setStringList(key, value) Yes
Uint8List getBytes(key) setBytes(key, value) Yes
DateTime getDateTime(key) setDateTime(key, value) Yes
Map<String, dynamic> getMap(key) setMap(key, value) Yes

All getters return null when the key does not exist.


Why not SharedPreferences? #

If you're using SharedPreferences (or Flutter's shared_preferences which wraps it), you're relying on a legacy Android API that Google has been actively deprecating.

Concern SharedPreferences Jetpack DataStore
Thread safety Not safe on UI thread; can cause ANRs Fully async with Kotlin Coroutines
Error handling Fails silently Proper error signaling via Flow
Runtime exceptions Parsing errors cause crashes No runtime exceptions from parsing
Disk I/O Blocking commit() / fire-and-forget apply() Consistent async API
Type safety Returns defaults on type mismatch Typed keys with compile-time safety
Consistency No transactional guarantees Atomic read-modify-write

Google's recommendation: "Prefer DataStore over SharedPreferences." -- Android Developers Docs


Getting Started #

1. Install #

Add to your pubspec.yaml:

dependencies:
  native_datastore: ^1.2.0

Then run:

flutter pub get

2. Import #

import 'package:native_datastore/native_datastore.dart';

// Also needed for Uint8List:
import 'dart:typed_data';

3. Use #

final datastore = NativeDatastore();

// -- Write --
await datastore.setString('username', 'sudhi');
await datastore.setBool('darkMode', true);
await datastore.setInt('loginCount', 42);
await datastore.setDouble('rating', 4.8);
await datastore.setStringList('tags', ['flutter', 'dart', 'mobile']);
await datastore.setBytes('avatar', Uint8List.fromList([0x89, 0x50, 0x4E]));
await datastore.setDateTime('lastLogin', DateTime.now());
await datastore.setMap('profile', {'name': 'sudhi', 'level': 5});

// -- Read --
final username  = await datastore.getString('username');       // "sudhi"
final darkMode  = await datastore.getBool('darkMode');         // true
final count     = await datastore.getInt('loginCount');        // 42
final rating    = await datastore.getDouble('rating');         // 4.8
final tags      = await datastore.getStringList('tags');       // ["flutter", "dart", "mobile"]
final avatar    = await datastore.getBytes('avatar');          // Uint8List [0x89, 0x50, 0x4E]
final lastLogin = await datastore.getDateTime('lastLogin');    // DateTime (UTC)
final profile   = await datastore.getMap('profile');           // {"name": "sudhi", "level": 5}

// -- Query --
final allKeys  = await datastore.getKeys();                   // ["username", "darkMode", ...]
final allData  = await datastore.getAll();                    // {username: sudhi, darkMode: true, ...}
final exists   = await datastore.containsKey('username');     // true

// -- Delete --
await datastore.remove('username');   // Remove a single key
await datastore.clear();              // Remove all data

Error Handling #

All operations throw NativeDatastoreException on failure. The exception wraps the underlying platform error with context:

try {
  await datastore.getString('key');
} on NativeDatastoreException catch (e) {
  print(e.message);  // Human-readable description
  print(e.cause);    // Original PlatformException (if any)
}

Empty keys are rejected immediately:

await datastore.getString('');  // Throws NativeDatastoreException: Key must not be empty

Handling Null Values #

All getters return null when the key does not exist. Use null-aware operators or provide defaults:

// With default values
final username = await datastore.getString('username') ?? 'Guest';
final count = await datastore.getInt('loginCount') ?? 0;

// With null checks
final lastLogin = await datastore.getDateTime('lastLogin');
if (lastLogin != null) {
  print('Last login: ${lastLogin.toIso8601String()}');
}

// Check before reading
if (await datastore.containsKey('profile')) {
  final profile = await datastore.getMap('profile');
  // Use profile...
}

API Reference #

Read operations #

Method Return Type Description
getString(key) Future<String?> Read a string value
getBool(key) Future<bool?> Read a boolean value
getInt(key) Future<int?> Read an integer value
getDouble(key) Future<double?> Read a double value
getStringList(key) Future<List<String>?> Read a string list
getBytes(key) Future<Uint8List?> Read binary data
getDateTime(key) Future<DateTime?> Read a date/time (stored as UTC millis)
getMap(key) Future<Map<String, dynamic>?> Read a JSON-compatible map

Write operations #

Method Return Type Description
setString(key, value) Future<void> Write a string value
setBool(key, value) Future<void> Write a boolean value
setInt(key, value) Future<void> Write an integer value
setDouble(key, value) Future<void> Write a double value
setStringList(key, value) Future<void> Write a string list
setBytes(key, value) Future<void> Write binary data (Uint8List)
setDateTime(key, value) Future<void> Write a date/time (stored as UTC millis)
setMap(key, value) Future<void> Write a JSON-compatible map

Query operations #

Method Return Type Description
getAll() Future<Map<String, Object>> Get all key-value pairs
getKeys() Future<List<String>> Get all stored keys
containsKey(key) Future<bool> Check if a key exists

Delete operations #

Method Return Type Description
remove(key) Future<bool> Remove a key (returns true if it existed)
clear() Future<bool> Remove all stored data

Secure Storage #

For secrets (auth tokens, refresh tokens, encryption keys) use the SecureDatastore class, which encrypts values at rest using platform key management:

  • iOS — Keychain Services (kSecClassGenericPassword) with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly. Values are readable after the first device unlock following a boot (including from background work), never migrated to a new device or restored from backup.
  • Android — AES-256-GCM with a key minted in the AndroidKeyStore (hardware-backed where the device supports it), encrypting values into a dedicated DataStore file. Each write uses a fresh 96-bit IV. Requires Android API 23 (Marshmallow) or higher; older devices throw UnsupportedOperationException from the secure API.
import 'package:native_datastore/native_datastore.dart';

final secure = SecureDatastore();

await secure.setString('refresh_token', tokenJwt);
final token = await secure.getString('refresh_token');

await secure.setBytes('symmetric_key', encryptionKey);
final key = await secure.getBytes('symmetric_key');

await secure.remove('refresh_token');
await secure.clear(); // wipes every value in the secure store

Surface is intentionally minimal: String and Uint8List only, plus remove / clear / getKeys / containsKey. Values are capped at 1 MiB — callers needing larger payloads should encrypt themselves and store via the filesystem. Errors flow through the same NativeDatastoreException used by NativeDatastore.


Storage Details #

Understanding how each type is stored on each platform:

Dart Type Android (DataStore) iOS (UserDefaults)
String stringPreferencesKey string(forKey:)
bool booleanPreferencesKey bool(forKey:)
int longPreferencesKey integer(forKey:)
double doublePreferencesKey double(forKey:)
List<String> JSON-encoded string Native string array
Uint8List Base64-encoded string Native Data
DateTime Long (millis since epoch UTC) Int64 (millis since epoch UTC)
Map<String, dynamic> JSON-encoded string JSON-encoded string

Note: DateTime values are always stored and retrieved in UTC. If you pass a local DateTime, it is converted to UTC before storage. The returned DateTime is always UTC -- use .toLocal() if you need local time.


Migrating from shared_preferences #

The API is intentionally similar -- switching is a one-line change:

// Before (shared_preferences)
final prefs = await SharedPreferences.getInstance();
await prefs.setString('key', 'value');
final value = prefs.getString('key');

// After (native_datastore)
final datastore = NativeDatastore();
await datastore.setString('key', 'value');
final value = await datastore.getString('key');

Key differences:

shared_preferences native_datastore
Initialization SharedPreferences.getInstance() NativeDatastore()
Reads Synchronous (cached) Future-based (async)
Android backend SharedPreferences Jetpack DataStore
Extra types -- Uint8List, DateTime, Map

Platform Details #

Android #

  • Uses androidx.datastore:datastore-preferences with Kotlin Coroutines
  • All operations run on Dispatchers.IO -- never blocks the UI thread
  • Data location: data/data/<package>/files/datastore/native_datastore_prefs.preferences_pb
  • A ReplaceFileCorruptionHandler is installed so a half-written prefs file (caused by the OS killing the process mid-write) recovers as empty instead of throwing CorruptionException on every subsequent call.
  • Minimum SDK: 21 (Android 5.0)

iOS #

  • Uses UserDefaults.standard
  • Keys are namespaced with native_datastore. prefix to avoid collisions
  • String lists stored natively as arrays (no JSON encoding overhead)
  • Binary data (Uint8List) stored natively as Data
  • Numeric getters (getBool, getInt, getDouble, getDateTime) are strict: they return null when the stored value's underlying type does not match (no silent coercion across types).
  • Minimum iOS: 12.0

Resilience on aggressive-kill OEMs #

Memory- and battery-optimised Android skins (MIUI, ColorOS, OriginOS, HyperOS, ColorOS-derived ROMs, Realme UI, FuntouchOS, etc.) regularly terminate background processes without warning. native_datastore is built to survive that:

  • Atomic writes. Each write goes through Jetpack DataStore, which uses the filesystem's atomic rename to replace the prefs file. A process kill mid-write either commits the full new file or leaves the previous file intact -- never a half-merged record.
  • Durable acknowledgements. await datastore.setX(...) only resolves after the underlying edit { } block has fsync'd, so a successfully awaited write is on disk before the next line of Dart runs.
  • Auto-recovery from corruption. If a kill lands inside the fsync window and the file ends up unreadable, the corruption handler replaces it with empty preferences on the next read. The app keeps working -- at worst losing the single write that was interrupted, never bricked.
  • Clean detach. When the Flutter engine is destroyed, in-flight coroutines are cancelled before the Pigeon channel is torn down, so the plugin never replies on a dead channel.

⚠️ Multi-process limitation #

The Android backend uses single-process Preferences DataStore. It is not safe to use this plugin from more than one process at a time. This matters if your app declares any of the following in AndroidManifest.xml:

  • A <service>, <receiver>, or <activity> with android:process=":foo"
  • A vendor push SDK (Xiaomi, OPPO, Vivo, Huawei, FCM in a separate process, Tencent TPNS, etc.) that runs in its own process
  • A ContentProvider configured with a separate process

Symptoms of multi-process misuse: silently lost writes, "old" values reappearing after an app upgrade, or in the worst case repeated CorruptionExceptions (which the corruption handler will swallow by resetting the file to empty -- i.e., data loss).

Recommended pattern: call native_datastore only from the main process (the one Flutter runs in). If a secondary process needs to read config, push a snapshot to it through Intent extras, a ContentProvider, or a small SharedPreferences file dedicated to that process.


Library Size #

The library is lightweight with a minimal footprint:

Layer Hand-written Generated (Pigeon) Total
Dart 358 lines 463 lines 821 lines
Kotlin (Android) 301 lines 509 lines 810 lines
Swift (iOS) 277 lines 480 lines 757 lines
Total 936 lines 1,452 lines 2,388 lines
  • ~61% is auto-generated Pigeon code (message channel boilerplate)
  • ~39% is hand-written plugin logic (~300 lines per platform)
  • Total source size: ~87 KB across 7 files
  • No external dependencies beyond Flutter SDK and platform-native APIs

Requirements #

Dependency Minimum Version
Flutter 3.3.0
Dart SDK 3.11.4
Android API 21 (5.0)
iOS 12.0

Contributing #

Contributions are welcome! Please open an issue or submit a pull request.


Releasing #

Releases are automated via .github/workflows/release.yml. Pushing a tag that matches v* runs tests and, on pass, publishes to pub.dev (via OIDC — no API key in the repo) and creates a matching GitHub Release.

# 1. Bump pubspec.yaml version (e.g. 1.3.0 → 1.3.1)
# 2. Commit and push the version bump
git commit -am "Release 1.3.1"
git push

# 3. Tag and push the tag — this triggers the workflow
git tag v1.3.1
git push origin v1.3.1

The workflow verifies the pushed tag matches v{pubspec.version} and aborts if they disagree. If a publish fails partway, either bump the patch version and push a new tag, or delete the bad tag locally and on the remote and retry:

git tag -d v1.3.1
git push --delete origin v1.3.1

License #

BSD 3-Clause License -- see the LICENSE file for details.

2
likes
140
points
85
downloads

Documentation

Documentation
API reference

Publisher

verified publishersudhi.in

Weekly Downloads

A modern Flutter plugin for persistent key-value storage. Uses Android Jetpack DataStore on Android and UserDefaults on iOS. A type-safe, async-first alternative to shared_preferences.

Homepage
Repository (GitHub)
View/report issues

Topics

#storage #datastore #preferences #persistence #keyvalue

License

BSD-3-Clause (license)

Dependencies

flutter, meta

More

Packages that depend on native_datastore

Packages that implement native_datastore