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.0.2

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

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
  • 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
  • Minimum iOS: 12.0

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.


License

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

Libraries

native_datastore
A modern Flutter plugin for persistent key-value storage.