native_datastore

Flutter plugin · Android & iOS

Store a value.
Close the app.
It's still there.

native_datastore is persistent key-value storage for Flutter — one async, type-safe Dart API over Jetpack DataStore on Android and UserDefaults on iOS. No method channels to hand-write, no setup files to edit.

  • Flutter 3.3 and up
  • Android 5.0 · Jetpack DataStore
  • iOS 13.0 · UserDefaults
flutter pub add native_datastore
NativeDatastore() 0 keys
Demo key-value store contents
KeyTypeValueRemove

This panel is backed by your browser's own storage, so — the values are read back. On a device, DataStore and UserDefaults do exactly this job.

Why switch

SharedPreferences is the legacy path

Google's own guidance is to prefer DataStore. Here is what changes when you do.

Concern SharedPreferences native_datastore
Thread safetyUnsafe on the UI thread; can trigger ANRsFully async, coroutine-backed
Error handlingFails silentlyTyped NativeDatastoreException
Parsing errorsThrow at runtimeNo runtime parse exceptions
Disk I/OBlocking commit() or fire-and-forget apply()One consistent async API
Type safetyReturns a default on type mismatchTyped keys, checked at compile time
Concurrent writesNo transactional guaranteeAtomic read-modify-write

Already shipping shared_preferences? One call imports everything: await ds.migrateFromSharedPreferences().

What you get

Eight types, two platforms, one API

One Dart API bridged by Pigeon to Jetpack DataStore on Android and UserDefaults on iOS.
Your Dart calls cross a generated Pigeon channel and land on whichever store the platform already recommends.

Both sides of the bridge

What your call becomes natively

You write one Dart line. Here is the API it lands on, per type, per platform — so you always know what is actually on disk.

How each Dart type is stored on Android and iOS
Dart type Read / write Android · DataStore iOS · UserDefaults
StringgetString / setStringstringPreferencesKeystring(forKey:)
boolgetBool / setBoolbooleanPreferencesKeybool(forKey:)
intgetInt / setIntlongPreferencesKeyinteger(forKey:)
doublegetDouble / setDoubledoublePreferencesKeydouble(forKey:)
List<String>getStringList / setStringListJSON-encoded stringNative string array
Uint8ListgetBytes / setBytesBase64-encoded stringNative Data
DateTimegetDateTime / setDateTimeLong — millis, UTCInt64 — millis, UTC
Map<String, dynamic>getMap / setMapJSON-encoded stringJSON-encoded string

Every getter returns null when the key is absent. DateTime is always stored and returned in UTC — call .toLocal() if you need wall-clock time.

Android

Minimum API 21 (Android 5.0) · API 23 for the secure store

  • Backed by androidx.datastore:datastore-preferences with Kotlin coroutines.
  • Every operation runs on Dispatchers.IO, so nothing touches the UI thread and nothing can ANR.
  • Stored at files/datastore/native_datastore_prefs.preferences_pb.
  • A ReplaceFileCorruptionHandler is installed: if the OS kills your process mid-write, the store recovers as empty instead of throwing on every call forever after.
  • Secrets use AES-256-GCM with an AndroidKeyStore key, hardware-backed where the device supports it.
  • Multi-process access is opt-in for background services.

iOS

Minimum iOS 13.0

  • Backed by UserDefaults.standard, with every key namespaced native_datastore. so it cannot collide with yours.
  • String lists and Uint8List are stored natively as arrays and Data — no JSON encoding tax.
  • Getters are strict: a stored value of the wrong underlying type returns null rather than silently coercing.
  • Ships both a Swift Package Manager Package.swift and a CocoaPods podspec, so it builds either way with no configuration.
  • Includes a PrivacyInfo.xcprivacy manifest declaring the UserDefaults required-reason API for App Store review.
  • Secrets go to the Keychain, shareable with app extensions via an App Group.

The whole surface

Cheat sheet

Copy, paste, adapt. Every call below is the real signature.

final ds = NativeDatastore();

// Write and read — getters return null when the key is absent
await ds.setString('username', 'sudhi');
final name = await ds.getString('username');      // 'sudhi'
await ds.setBool('darkMode', true);
await ds.setInt('loginCount', 42);
await ds.setDouble('rating', 4.8);
await ds.setStringList('tags', ['flutter', 'dart']);
await ds.setBytes('avatar', bytes);               // Uint8List, up to 1 MiB
await ds.setDateTime('lastSeen', DateTime.now()); // stored as UTC
await ds.setMap('profile', {'level': 5});         // any JSON-able map

// Observe a key as a Stream
ds.watchInt('loginCount').listen((v) => print('count = $v'));

// Atomic updates — safe when several writers race
await ds.incrementInt('loginCount');              // returns the new value
await ds.decrementInt('lives');
await ds.toggleBool('darkMode');
await ds.compareAndSetString('status', expected: 'pending', value: 'done');

// Query and delete
await ds.containsKey('username');                 // bool
await ds.getKeys();                               // List<String>
await ds.getAll();                                // Map<String, Object>
await ds.remove('username');
await ds.clear();

// One-time import from the shared_preferences package
await ds.migrateFromSharedPreferences();

// Secrets — encrypted at rest
final secure = SecureDatastore();
await secure.setString('refresh_token', jwt);
final token = await secure.getString('refresh_token');

SecureDatastore

Tokens don't belong in plain storage

The regular store does not encrypt. For refresh tokens, API keys and anything else worth stealing, use SecureDatastore — same call shapes, ciphertext on disk.

Android
AES-256-GCM with a key minted in the AndroidKeyStore, hardware-backed where the device allows it. A fresh 96-bit IV per write. Requires API 23+.
iOS
Keychain Services with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly — never backed up, never migrated to a new device.

Surface is deliberately small: String and Uint8List, plus remove, clear, getKeys, containsKey. Values cap at 1 MiB.

Read the secure storage guide
A plaintext token leaves Dart code, is encrypted with an AES-256-GCM key held in the AndroidKeyStore or iOS Keychain, and only ciphertext is written to disk.
Plaintext in, ciphertext on disk — the key never leaves the platform keystore.

Cost of a call

Reads are cheap. Encryption isn't free.

Throughput per operation, higher is better. The split is the point: every regular-store call outruns every secure-store call, and all eight clear thousands of operations per second.

Benchmark throughput by operation, in operations per second
Operation Throughput Ops/sec Mean µs
regular getString25,04439.9
regular getInt24,61640.6
regular setString5,828171.6
regular setInt5,272189.7
secure getBytes4,972201.1
secure getString4,750210.5
secure setBytes1,833545.6
secure setString1,570636.9

Illustrative, not a spec. Measured on an iOS simulator (iPhone 17 Pro, debug build), 300 iterations per operation after warm-up. Real devices, release builds and Android hardware will differ — often substantially. Run it yourself: cd example && flutter run -t lib/benchmark_main.dart.

On a real device

The example app

Both stores, every type, running on hardware. Clone the repo and flutter run from example/.

The example app writing every supported type on the Regular tab, then reading them all back.
Regular store — writing all eight types, then reading them back.
The example app saving encrypted secrets on the Secure tab, listing the keys, then clearing the store.
Secure store — saving secrets, listing keys, clearing. Values stay encrypted at rest.
A write to a key flows through the store into a watchInt stream, which rebuilds the UI automatically.
watchInt() pushing a change straight into the UI — no manual refresh.

Three steps

Start writing keys

Add it

flutter pub add native_datastore

No Gradle edits, no Info.plist keys, no manifest changes.

Import it

import 'package:native_datastore/native_datastore.dart';

One import covers both NativeDatastore and SecureDatastore.

Use it

final ds = NativeDatastore();
await ds.setString('username', 'sudhi');
final name = await ds.getString('username');

Everything is a Futureawait it, and the UI thread stays free.

Minimum versions
Flutter3.3.0
Dart SDK3.11.4
AndroidAPI 21 (5.0) — API 23 for SecureDatastore
iOS13.0