native_datastore 1.8.0
native_datastore: ^1.8.0 copied to clipboard
Type-safe, async-first key-value storage for Flutter, backed by Android Jetpack DataStore and iOS UserDefaults. A modern alternative to shared_preferences.
1.8.0 #
Correctness and verification release, from an Android-architecture review of the
whole plugin. Three of these were reachable from ordinary use and none of them
could have been caught by the test suite, because no workflow compiled the
Kotlin or Swift at all — CI ran a 100% line-coverage gate over Dart whose every
platform call is stubbed at the BinaryMessenger boundary, and stopped there.
That is fixed too, and it is the change the rest depend on.
Fixed — data loss #
- The encrypted store no longer travels in Android Auto Backup or
device-to-device transfer. It lived in
filesDir/datastore, which both mechanisms copy by default. The AndroidKeyStore key that decrypts it does not travel and cannot, so a restored install held ciphertext it had no key for — and because the retry path minted a fresh key when the alias was missing, every secure read then failed for the life of that install, with an opaqueAEADBadTagExceptionand no way back.- The store now lives under
noBackupFilesDir, which is excluded from both by design. Existing files are moved there on first access; nothing is lost. - The README said secrets are "not in backups". That was true of the key and not of the ciphertext.
- The store now lives under
- A store whose key is gone now heals instead of failing forever. A key-fingerprint entry is written alongside the data; if the current key cannot read it, the unreadable contents are cleared and the store is re-stamped. On upgrade, a store with no fingerprint yet is checked by decrypting one existing entry, so installs already broken by a restore recover on first launch. Callers see absent keys and can re-authenticate.
KeyPermanentlyInvalidatedExceptionandAEADBadTagExceptionare no longer retried. Both areGeneralSecurityException, so the single blanket retry ran them a second time, failed identically, and buried the real cause. Each now surfaces asSecureKeyUnavailableExceptionwith a message that says what to do; only a stale cached key handle — the one case a retry can fix — is retried.
Fixed — crashes #
- Reading a key back at the wrong type returns
nullinstead of throwing. All four scalars share one flat key space, andPreferences.getcasts unchecked — sosetDouble('k', 1.5)followed bygetInt('k')handed aDoubleback typed asLongand failed at the Pigeon boundary. iOS already returnednilhere. Every getter,increment*,toggleBoolandcompareAndSet*now branch on the runtime type.getDoublewidens a stored int, matching iOS. - A second
FlutterEngineno longer crashes multi-process mode. The multi-process store was cached per plugin instance, but DataStore rejects a second instance over the same file — so add-to-app,FlutterEngineGroupand background isolates hitIllegalStateException: There are multiple DataStores active for the same file. Both multi-process stores are now process-wide, matching the single-process ones, which were always property delegates.
Fixed — behaviour #
configure()applies before it returns. It set the flag that selects the backing store inside a coroutine, so calls already in flight could land on the store the caller was switching away from.watch*survives aconfigure()that switches stores. The change stream resolved its store once, when collection began, so switching to multi-process mode afterwards left every watcher open and permanently silent.- Byte-valued keys no longer report a change on every write. The change diff
compared
ByteArraywith!=, which is referential in Kotlin, and the multi-process store re-parses its JSON on every emission. iOS comparedNSDataby content; Android now does too. containsKey,getBytesand the secureremove/containsKeystopped scanning. They walked every key in the store; they now use the same O(1) name proberemovealready used.
Security #
- Encrypted values are bound to the key they are stored under. AES-GCM
proved a blob was written by this key, not where — so anyone able to write
the store file could move the blob under
__str__:role_userto__str__:role_admin, or restore an old value under its own name, and the tag still verified. The entry name is now passed as GCM additional authenticated data, behind a format version byte so pre-1.8.0 blobs stay readable. - The key is requested StrongBox-backed on devices that have a secure element, falling back to the TEE where they do not.
API #
NativeDatastoreExceptionnow carries acode. Both hosts report the same vocabulary —plugin-detached,secure-key-unavailable,unsupported-platform-version,unsupported-type,keychain-error,encoding-error— so aswitchbehaves the same on Android and iOS. Previously Pigeon's fallback wrapper used the Java class name on Android and the Swift error's description string on iOS, so there was nothing portable to match on and the only signal was an English message.- Failures keep the stack trace of the call that failed.
_guardrethrew plainly, which replaced it with its own frame. setManyacceptsUint8List, so agetManyresult carrying bytes can be handed straight back. It also accepts aList<dynamic>whose elements are all strings — the shapejsonDecodereturns, which the oldis! List<String>check rejected — and now rejects a list with a non-string element instead of silently callingtoString()on it.DateTimeandMapare still not accepted, and now say why: their wire forms are anintand aString, indistinguishable from a plain scalar, so the native side cannot tell which namespace to write them to.
getAllandgetManyresolve a key held in several namespaces deterministically. Both platforms iterated their snapshot and let the last entry win, so the answer depended on dictionary order — it differed between a warm simulator and a clean CI runner, and between the two platforms. Both now prefer typed namespaces over the scalar slot, in the ordergetManyalready used.- The duplicated
_guardand key-validation logic — a verbatim copy in each of the two facades — now lives once inlib/src/errors.dart.
iOS #
The integration suite had never been run on iOS by anything — CI did not execute
it, and it is the only test that touches real UserDefaults and Keychain code.
Running it found two defects. Adding a Swift linter found a third set.
setManywith a byte payload crashed the app. It passed each value straight todefaults.set, which raises an Objective-C exception for anything that is not a property-list type — andUint8Listarrives asFlutterStandardTypedData. It now converts every entry before writing any of them, so bytes land in their namespace and an unrepresentable value fails the call withunsupported-typeinstead of terminating the process. That also gives iOS the all-or-nothing guarantee Android's singleedit {}already had.- The non-secure iOS API had no failure path at all.
onQueuetook a non-throwing body, so every operation reported.successno matter what. getStringreturned a number's string value.defaults.string(forKey:)converts anNSNumber, sosetDouble('k', 1.5)thengetString('k')handed back"1.5". That contradictedgetIntandgetBoolon the same platform, which already rejected a cross-type read, and Android, which returnsnull. It is now a miss, not a conversion.
Adding a Swift linter to CI surfaced 18 further error-level violations in code that had never been linted. These are the substantive ones.
- Three force casts removed from the Boolean read path.
getBool,toggleBoolandcompareAndSetBooleach checkedisStoredAsBool(value)and then force-cast toNSNumbera line or two later. Safe as written, and one edit away from crashing the host app. The type test and the extraction now live together instoredBool(forKey:), withstoredIntandstoredDoublealongside it — which also collapses the same five-line type dance that was repeated across six call sites. - The change-stream observer is removed by token.
removeObserver(self)drops every notification registration the object holds; the handler only ever meant to undo its own. It now keeps the registration token fromaddObserver(forName:object:queue:)and removes exactly that. SecureDatastore.removeno longer depends on a lucky evaluation order. It now maps over both buckets before testing the result, so a key that holds both a string and a bytes entry has both deleted — a short-circuiting form would have stopped at the first.- Corrected a comment that contradicted the public docs. The change-stream
handler claimed watchers are "deliberately not coalesced" and see every write.
Foundation coalesces
didChangeNotificationper runloop turn regardless, which is what the Dart doc has always said. The Dart doc was right.
CI now compiles and tests the native code #
Every gate moved into one reusable checks.yml, so pr.yml and release.yml
can no longer drift — they carried byte-for-byte copies of the same four steps.
- Kotlin and Swift are compiled (
flutter build apk/flutter build ios). Before this, a Kotlin syntax error or an AGP incompatibility could publish to pub.flutter-io.cn undetected. - 21 Kotlin unit tests, the first native tests in the repo, over the
multi-process JSON serializer, the change-diff helpers, the secure-store
relocation, the store registries and the blob framing.
android/src/test/did not exist. - ktlint and SwiftLint — there was no Kotlin or Swift lint of any kind.
Both are version-pinned, and SwiftLint runs
--strict, so a warning fails the build rather than scrolling past. example/integration_testruns on an emulator. It is the only thing that exercises real DataStore, AndroidKeyStore and change-stream code, and nothing ran it.- A Pigeon drift check. It found real drift on its first run: the checked-in
Messages.g.*predated a doc edit inpigeons/messages.dart. - A version-string gate.
pubspec.yaml,android/build.gradle.ktsandios/native_datastore.podspechad drifted to 1.7.1, 1.6.2 and 1.5.3 because nothing read the latter two.release.shnow rewrites them and the gate proves it. tool/generate_pigeon.shused the BSD spelling ofsed -i, which fails on the Linux runners the drift check has to run on.
Known gaps #
These were found in the same review and are not fixed here:
- No synchronous or prefetched read. A value needed before the first frame —
theme, locale, an onboarding flag — still costs a platform round trip.
shared_preferencescovers this withSharedPreferencesWithCache. SecureDatastorehas nowatch*, batch or atomic operations, andconfigure(multiProcess: true)still orphans existing secrets into a separate file with no migration.- Corruption still empties a store silently, with no signal to the app and no way to opt out of the replace-on-corruption policy.
- One store per app. The filename is fixed, so a modular app cannot scope storage per feature, and there is no injection seam for tests.
- No Direct Boot support. Nothing is readable before first unlock, which rules out boot-completed receivers and pre-unlock notifications.
DataMigrationis not exposed and there is noEncryptedSharedPreferencesimporter, soshared_preferencesis the only supported migration source.migrateFromSharedPreferencesalso skips that plugin's taggedList<String>andBigIntegervalues without reporting what it dropped.- Android and iOS only. A shared codebase running on web or desktop gets
MissingPluginException; there is no in-memory fallback, and no platform interface for a third party to add one. - iOS takes its cross-process lock only for the atomic operations. Under an
App Group,
setMany,removeMany,remove,clearand the plain getters/setters can still interleave with an extension.
1.7.1 #
Documentation and tooling only — no shipped code changed, and no behaviour differs from 1.7.0. Released so the corrected performance figures reach pub.flutter-io.cn rather than sitting only in the repository.
- Corrected platform-specific performance claims in the 1.7.0 entry. It
presented the batch-write speedup as a general result. It is Android-only.
- Writes on iOS go 35.60 ms → 34.00 ms (1.05x) — effectively nothing.
The Android gain (162x) comes from collapsing N whole-file rewrites into
one, and
UserDefaultshas no such rewrite to amortise. iOS still gets the read win (8.29 ms → 0.93 ms, 8.9x) from spending one channel hop instead of N. - "Writes are O(store size)" was likewise Android-only: iOS is flat from 0 to 500 keys (183 µs → 167 µs).
- The Base64 finding was Android-only too — iOS already stored
Datanatively, and its byte cost was already flat in payload size. - Every figure in the 1.7.0 entry now names the platform it was measured on.
- Writes on iOS go 35.60 ms → 34.00 ms (1.05x) — effectively nothing.
The Android gain (162x) comes from collapsing N whole-file rewrites into
one, and
- The watcher-retention test now supports its conclusion. The 1.6.2 entry
reported "no leak" from a single 200-cycle attach/detach sample. Re-running
that same cycle count repeatedly yields +5100 and −8765 bytes/cycle — the
sample sat inside the noise band and established nothing either way.
example/lib/profile_main.dartnow runs batches of 1000/2000/4000/8000 and reports bytes per cycle, which separates the two cases: a leak holds that figure roughly constant as cycles grow, while a heap reaching its working set lets it fall toward zero.- Measured on Android: −1282, 1493, 738, 189 bytes/cycle. It collapses, so there is no leak. The original conclusion was correct; the evidence offered for it was not.
- Known gap: the iOS figures above come from a simulator in debug mode —
simulators reject
--profile. They are sound as same-run ratios but are not real-device latencies.
1.7.0 #
Performance release, driven by profiling on an Android emulator (API 35, arm64,
profile mode) and an iOS simulator (debug mode — simulators reject --profile).
Ratios below are same-run comparisons; absolute microseconds are emulator and
simulator numbers and will differ on real hardware. Gains differ sharply by
platform, so each figure names the platform it was measured on.
- New: batch API —
getMany,setMany,removeMany. Reading keys one at a time costs one channel hop each, and on Android each hop materialises the whole store snapshot; writing one at a time rewrites the whole preferences file per key. Batching collapses both into a single native transaction.- 200 keys, same run, Android (emulator, profile mode): reads 47.0 ms → 1.04 ms (45x), writes 374.3 ms → 2.31 ms (162x).
- iOS (simulator, debug mode): reads 8.29 ms → 0.93 ms (8.9x), writes
35.60 ms → 34.00 ms (1.05x). The write win is Android-specific — it comes
from collapsing N whole-file rewrites into one, and
UserDefaultshas no such rewrite to amortise. iOS still gets the read win, from spending one channel hop instead of N. setManyis all-or-nothing: values are converted before the transaction opens, so an unsupported type fails before anything is written.getManyomits absent keys rather than mapping them tonull, so a missing key stays distinguishable from a stored one.- Values must be
String,bool,int,doubleorList<String>; use the typed setters forUint8List,DateTimeandMap, which carry type information the batch path does not.
- Byte payloads are stored natively on Android instead of Base64. (iOS already
stored
Datanatively; measurement confirms its byte cost was already flat in payload size, so this finding was Android-only.) Base64 inflated every blob ~33% on disk and, because the JVM holds strings as UTF-16, roughly 2.7x in memory on top of the byte array itself.setBytes/getBytesnow use DataStore'sbyteArrayPreferencesKey.- Cost is now essentially flat in payload size. Same run, 1 KB → 1 MB:
setBytes7.6 ms → 506 ms (66x growth) before, 5.3 ms → 9.4 ms (1.8x) after;getBytes1.2 ms → 117.8 ms (96x) before, 1.0 ms → 0.95 ms (flat) after. - Existing Base64 values are still read — the reader branches on the stored runtime type — so no migration is required.
- The multi-process JSON serializer gained a
"ba"type for byte payloads, which it previously dropped silently.
- Cost is now essentially flat in payload size. Same run, 1 KB → 1 MB:
- iOS: the change-stream diff no longer blocks the main thread.
UserDefaults.didChangeNotificationfires for any write in the app, and each one ran a fulldictionaryRepresentation()snapshot and diff synchronously on the posting thread — usually main. The diff now runs on a serial background queue, so an app writing its own unrelated defaults no longer stalls the main thread on this plugin's bookkeeping. A repeatedonListenwithout an interveningonCancelno longer registers a duplicate observer.- The per-notification snapshot itself is unchanged. Coalescing bursts into a single diff was tried and reverted: it drops intermediate values, so two quick writes surfaced only the last.
- Documented an iOS
watch*limitation that predates this release.UserDefaults.didChangeNotificationis coalesced by the system, so several writes within one runloop turn post a single notification and an intermediate value can be skipped. Android's DataStore emits per write and is unaffected. Thewatch*doc comments now say so: treat the stream as "the current value, kept fresh", not "every value this key ever held". Found by running the on-device integration suite on an iOS simulator for the first time — the existing watch test fails on iOS atmain, independently of this release's changes. - iOS fix: atomic operations are now atomic across processes.
incrementInt,incrementDouble,toggleBoolandcompareAndSet*were guarded only by an in-process serial queue. Onceconfigure(appGroupId:)points storage at a shared suite, an app extension in another process could interleave its own read-modify-write and silently lose an update — so the operations this plugin advertises as atomic were not. They now take an advisoryflockon a lock file in the App Group container. With no App Group configured there is no second process to race, and the lock is skipped entirely. - Android: the AndroidKeyStore key handle is cached instead of being
re-resolved on every encrypt and decrypt (two keystore-daemon round trips per
secure operation). A crypto failure drops the cached handle and retries once,
so a key invalidated out from under the process recovers instead of failing
every subsequent call.
- The per-thread
Cipherinstance is also cached, soCipher.getInstanceno longer walks the JCA provider list on every operation. - Honest note: none of the three secure-path changes (key caching,
Ciphercaching, dropping Base64 from ciphertext) produced a measurable improvement. The secure/regular read ratio held at 4.9x-5.3x across four clean runs. With those three candidate costs eliminated, the remaining overhead is the per-operation round trip to the keystore daemon thatcipher.init/doFinalrequire for a non-extractable key — inherent to the security model, and not removable without extracting the key. The changes are kept because they delete genuinely redundant work that should matter more against a hardware-backed keystore and for large secure payloads, but that remains unmeasured.
- The per-thread
remove/removeManyno longer scan the whole store.Preferences.Keyequality is by name alone, so a String-typed probe matches whatever type is stored under that name — turning removal into a few O(1)containslookups instead of a pass over every key.removeManynow counts keys removed rather than bucket entries, matching its documented contract.- Tests: 149 unit tests at 100% line coverage, plus on-device integration coverage for the batch API and for byte round-trips at 0 B, 1 B, 1 KB and 256 KB. The integration suite now runs green on both an Android emulator and an iOS simulator.
- Tooling: added
example/lib/profile_main.dart, a memory and scaling harness (flutter run --profile -t lib/profile_main.dart). It caught a real regression during this work: an early version of the byte change removed the legacy key immediately after writing the new one, and becausePreferences.Keyequality is by name alone, that deleted the value just written.
1.6.2 #
- Fix:
cancel()on awatch*subscription now completes immediately.NativeDatastore._watchwas anasync*generator parked inawait for (… in _changes). A generator suspended at anawaitcannot be resumed by a cancellation, soawait subscription.cancel()hung — and the underlying platform change observer stayed registered — until the next change event happened to arrive. The watcher is now built on an explicitStreamController, so cancelling tears the observer down at once.- A change arriving while the initial read is in flight is no longer dropped: the change subscription is opened before the first read.
- Overlapping notifications can no longer deliver a stale value after a fresher one — reads are chained.
- Errors from the change channel and from a failed re-read now surface as stream errors instead of being swallowed, and the watcher closes when the change stream closes.
- pub.flutter-io.cn score: 160/160. Shortened the
pubspec.yamldescription to the 60–180 character range pana expects, and formatted every Dart file with the Dart 3.7+ formatter.tool/generate_pigeon.shnow runsdart formaton the generated bindings — Pigeon still emits the pre-3.7 short style, which would otherwise reintroduce the formatting failure on every regeneration.- CI (
pr.yml,release.yml) gained adart format --set-exit-if-changedstep so formatting drift fails the build instead of the pub.flutter-io.cn report, plus atool/check_coverage.shgate that fails the build if line coverage drops below 100% and names the offending lines.
- Android: migrated to built-in Kotlin, without raising the Flutter floor.
From AGP 9 the Flutter Gradle Plugin supplies Kotlin itself and a plugin that
applies the Kotlin Gradle Plugin again fails the build.
android/build.gradle.ktsnow applies KGP only when the consuming app's AGP major version is below 9, and configuresjvmTargetthrough the KGP project extension instead of the removedandroid.kotlinOptions{}block. pana reports Built-in Kotlin-ready.- The
flutterconstraint stays at>=3.3.0— the conditional form documented for plugins that cannot require Flutter 3.44 is used deliberately, so no existing consumer is broken. - Verified by building the example app on both paths: AGP 8.11.1 (KGP applied)
and AGP 9.0.1 with
android.builtInKotlin=true(KGP skipped).
- The
- Example toolchain: upgraded to Gradle 9.1.0, AGP 9.0.1 and Kotlin 2.3.20, and migrated the example app itself to built-in Kotlin. Flutter 3.47 warns that support for the previous versions will be dropped soon. This affects the demo app only — it does not change what the published plugin requires, though the example now needs a Flutter 3.44+ toolchain to build.
- Android housekeeping: the plugin's Gradle module
versionwas stale at1.5.3; it now tracks the package version. Removed the leftoverandroid/settings.gradle, which declaredrootProject.name = 'android_datastore'and took precedence over the correctly namedandroid/settings.gradle.kts. - Tests: unit-test line coverage is now 100% (516/516). Added coverage for
every typed
watch*getter, the change-driven re-read and its key filter, subscription cancellation, change-stream and read errors, and the non-PlatformExceptionarm ofSecureDatastore's error guard.
1.6.1 #
- Documentation only — no code or API changes.
- README now links the project Wiki (task-focused guides: Getting Started, Secure Storage, Multi-Process Access, Troubleshooting) via a badge and a guides callout, while the README remains the canonical full API reference.
- Added
SECURITY.mddescribing the private vulnerability-reporting policy and the secure-storage threat model.
1.6.0 #
- New:
SecureDatastore.configure({multiProcess, appGroupId})for cross-process secrets. Brings the regular store's multi-process support to encrypted storage. Opt-in and non-destructive — the default single-process secure store is untouched.- On Android,
multiProcess: trueopens the encrypted store with aMultiProcessDataStorein its own file (native_datastore_secure_mp.json). The AndroidKeyStore key is already process-agnostic, so only the file backing changes; existing secrets in the default file are not migrated. - On iOS,
appGroupIdis used as the Keychain access group (kSecAttrAccessGroup) so an app and its extensions can share secrets. Requires the Keychain Sharing capability in Xcode. (This is a Keychain access group string, distinct from the App Group suite used by the regular store.)
- On Android,
- Example: the Secure tab now has a Multi-process access toggle that
calls
configure(multiProcess:)live. - Tests: end-to-end integration coverage for the secure store in both
single-process and multi-process modes (
plugin_integration_test.dart), verified on an iOS simulator and an Android emulator. - Benchmarks: a runnable harness (
integration_test/benchmark_test.dart) measuring regular vs secure set/get latency, with an illustrative results table in the README. - Docs: README multi-process section and FAQ updated for
SecureDatastore; new animated encryption diagram and a real secure-storage screen recording; a "what it protects" threat-model note.
1.5.3 #
- Documentation only — added a real screen recording of the bundled example app to the README ("See it in action"), showing the Regular and Secure stores running on a device.
1.5.2 #
- Documentation only — added a fourth animated diagram to the README
illustrating why the atomic operations prevent lost updates (manual
read-then-write vs
incrementInt()under two concurrent writers).
1.5.1 #
- Documentation only — no code or API changes. Expanded the README to help
developers get started faster: three animated diagrams (architecture, reactive
watch, and "your data survives app restarts"), a quick-reference cheat sheet, a "which method should I use?" decision table, and an FAQ.
1.5.0 #
Feature release bringing the plugin to parity with Jetpack DataStore's core capabilities. All additive — no breaking changes.
- New: reactive observation (
watch*). Observe a key as aStreamthat emits the current value on subscription and a fresh value on every change:watchString,watchBool,watchInt,watchDouble,watchStringList,watchBytes,watchDateTime,watchMap, pluswatchChanges()for the list of changed keys. Backed by DataStore'sFlowon Android andUserDefaultschange notifications on iOS, over a single shared event channel. - New: atomic read-modify-write.
incrementInt/decrementInt,incrementDouble,toggleBool, andcompareAndSet{String,Int,Double,Bool}. Each runs as one native transaction (DataStoreedit {}on Android, the serial queue on iOS), so concurrent callers never lose an update. - New:
migrateFromSharedPreferences({overwrite}). Imports existingshared_preferencesvalues (scalars and string lists) into this store and returns the number of keys imported. Safe to call on every launch. - New:
configure({multiProcess, appGroupId})for multi-process storage. Opt-in and non-destructive — the default single-process store is untouched. On Android,multiProcess: trueopens aMultiProcessDataStore(kept in its own file). On iOS,appGroupIdbacks storage with an App Group suite so app extensions and other processes in the group share data. - Docs: expanded README with sections for all of the above.
1.4.0 #
- New: Swift Package Manager support (iOS). The plugin now ships a
Package.swiftalongside the existing CocoaPodspodspec, so apps that have opted into Flutter's Swift Package Manager integration resolvenative_datastorethrough SPM. CocoaPods continues to work unchanged — both build systems point at the same sources underios/native_datastore/Sources/native_datastore/. No action is required from existing CocoaPods users. - New: iOS privacy manifest. Added
PrivacyInfo.xcprivacydeclaring theUserDefaultsrequired-reason API (NSPrivacyAccessedAPICategoryUserDefaults, reasonCA92.1), satisfying Apple's App Store privacy-manifest requirement. - Raised iOS minimum deployment target to 13.0 (from 12.0) to match the minimum supported by current Flutter stable. iOS 12 is no longer supported by the Flutter framework.
- Android dependency updates:
androidx.datastore:datastore-preferences1.1.7 → 1.2.1andkotlinx-coroutines-android1.7.3 → 1.11.0. - Fixed: Android build failure on current Kotlin toolchains. The Pigeon-
generated
Messages.g.ktdeclaredpackage in.sudhi.native_datastorewithout escapingin, a reserved Kotlin keyword, which fails to compile on Kotlin 2.x (Package name must be a '.'-separated identifier list). The generated file is now escaped (package `in`.sudhi.native_datastore). A newtool/generate_pigeon.shwrapper regenerates the bindings and applies this escape automatically — use it instead ofdart run pigeon. - Tooling:
pigeon26 → 27(bindings regenerated),meta^1.17.0 → ^1.18.0. - License changed from BSD-3-Clause to Apache License 2.0. Both are permissive;
Apache-2.0 adds an explicit patent grant and trademark protection, making the package
safer to adopt for enterprise/corporate projects. Added a
NOTICEfile per Apache convention. This is not a restriction — existing usage remains free and unaffected.
1.3.2 #
- Fixed OIDC authentication in the GitHub Actions release workflow. The publish
job now explicitly requests a GitHub OIDC token for the
https://pub.flutter-io.cnaudience and registers it viadart pub token addbefore publishing, sodart pub publishno longer falls back to interactive browser auth when used withsubosito/flutter-action(which doesn't auto-configure pub.flutter-io.cn credentials the waydart-lang/setup-dart@v1.3+does). - No code changes — package contents are identical to 1.3.1.
1.3.1 #
- Nothing special just a build automation with Github actions
1.3.0 #
- New:
SecureDatastorefor encrypted-at-rest storage. A separate class (SecureDatastore()) backed by Keychain Services on iOS (kSecClassGenericPassword,kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) and AndroidKeyStore-backed AES-256-GCM over a dedicated DataStore file on Android (hardware-backed key where available, fresh 96-bit IV per write). Surface:setString/getString,setBytes/getBytes, plusremove/clear/getKeys/containsKey. Values are capped at 1 MiB. Android requires API 23 (Marshmallow) or higher; older devices receive a clearUnsupportedOperationExceptionfrom the secure API only — the regularNativeDatastorestill works. - Breaking —
clear()now returnsFuture<void>instead ofFuture<bool>. The previousboolwas alwaystrueon success; callers awaiting the result need no change beyond removing any comparison against the return value. - iOS plugin lifetime hardening: added
detachFromEngine(for:)+registrar.publish(...)so theFlutterBinaryMessengerreleases the Pigeon dispatcher (and the plugin instance it captures) when the engine is torn down. Prevents stale instances under hot-restart andFlutterEngineGroup. - iOS retain-extension fix: every queue dispatch now uses
[weak self]via a new internalonQueue<T>helper. A teardown mid-flight short-circuits with aplugin-detachederror instead of pinning the plugin alive for the duration of the serial-queue backlog. - Android cancellation-race fix:
launchOnAttached(formerlylaunchSafe) now guarantees the Pigeon callback fires exactly once, even when the coroutine scope is cancelled before the body runs. Previously such a race could leave the Dart-sideFuturehanging inBinaryMessenger's pending-replies map until the engine itself was destroyed. - Bounded payloads:
setBytesandsetMapnow reject values larger than 1 MiB with a clearNativeDatastoreException. UserDefaults and DataStore are designed for small preferences; use a database or the filesystem for bulk binary storage. - Internal refactor (no behavior change):
- Centralized bucket prefixes (
__list__:,__bytes__:,__datetime__:,__map__:) as named constants per language with a clear sync comment. Eliminates 30+ magic-string sites that previously had to be edited in lockstep. - Pigeon FFI method names match the Dart facade:
getDateTimeMillis/setDateTimeMillis→getDateTime/setDateTime,getJsonMap/setJsonMap→getMap/setMap. The wire encoding (millis / JSON) is now an implementation detail of the host. - Swift error class renamed to
NativeDatastoreErrorto match Kotlin. getAll()documentation now explicitly enumerates the runtime-type union of returned values (includingUint8Listfor bytes, raw millis-intfor DateTime, raw JSON-Stringfor Map).- Renamed for clarity: Swift
prefix→keyNamespace,queue→serialQueue; KotlinlaunchSafe→launchOnAttached. - Repeated dartdoc on typed getters/setters consolidated via
{@template}/{@macro}.
- Centralized bucket prefixes (
1.2.0 #
- Android resilience on aggressive-kill OEMs (MIUI, ColorOS, OriginOS, HyperOS, etc.):
added
ReplaceFileCorruptionHandlerso a half-written prefs file (caused by the OS killing the process mid-write) auto-recovers as empty instead of throwingCorruptionExceptionon every subsequent call. - iOS strict numeric typing:
getBool/getInt/getDouble/getDateTimeMillisnow useCFGetTypeIDandNSNumber.objCTypeto returnnullinstead of silently coercing across stored types (e.g.,getIntaftersetBoolno longer returns1). - Reserved-prefix key validation: user keys starting with the internal sentinels
__list__:,__bytes__:,__datetime__:,__map__:are now rejected with a clear error, preventing silent collisions with typed-storage slots. - Stronger error wrapping:
_guardnow also wraps non-PlatformExceptionerrors (e.g.,FormatExceptionfrom corrupt stored JSON,JsonUnsupportedObjectErrorfrom a non-encodablesetMapvalue) so every public method honors its documented "throwsNativeDatastoreException" contract. - Android detach race:
onDetachedFromEnginenow cancels the coroutine scope before tearing down the Pigeon channel, andlaunchSaferethrowsCancellationExceptionso an in-flight callback never tries to reply through a dead channel. - Note: the plugin is single-process. If your app runs a secondary process (e.g., a push service) that also writes preferences, see the README's "Multi-process limitation" section.
1.1.2 #
- Released on 2026-04-06.
1.1.1 #
- Released on 2026-04-06.
1.1.0 #
- Released on 2026-04-06.
1.1.0 #
- Added 3 new data types:
Uint8List-- binary data viagetBytes()/setBytes()(Base64 on Android, native Data on iOS).DateTime-- date/time viagetDateTime()/setDateTime()(stored as UTC milliseconds since epoch).Map<String, dynamic>-- JSON maps viagetMap()/setMap()(stored as JSON string).
- Updated
remove(),containsKey(),getAll(), andgetKeys()to support new types. - Added "Set All Types" button in example app to demo all 8 data types.
- Updated README with supported types table, error handling guide, null handling examples, and storage details.
- Expanded unit tests from 57 to 78 covering all new types.
- Expanded integration tests to cover all 8 types including null returns.
- Breaking (iOS): Changed UserDefaults key prefix from
in.sudhi.native_datastore.tonative_datastore.-- removes personal domain from a public library. Existing iOS data stored with the old prefix will not be accessible after this update.
1.0.2 #
- Released on 2026-04-03.
1.0.0 #
- Released on 2026-04-03.
0.0.1 #
- Initial release with support for Android (Jetpack DataStore) and iOS (UserDefaults).
- Type-safe key-value storage: String, int, double, bool, and List
- Full CRUD operations: get, set, remove, clear, getAll, getKeys, containsKey.
- Built with Pigeon for type-safe platform communication.