Preferences Generator

pub version style: lint License: MIT

A build_runner code generator that turns a single annotated class into a fully type-safe, storage-agnostic preferences API — with automatic serialization, reactive streams, and ChangeNotifier integration.

┌─ Your schema (1 file) ─────────────────────────────────────────┐
│  @PrefsModule.reactive()                                       │
│  abstract class AppSettings with _$AppSettings, ChangeNotifier │
│    AppSettings._({ String username = 'guest', ... });          │
└───────────────────────────────────────────────────────────────-┘
                          ↓  build_runner
┌─ Generated (1 part file) ──────────────────────────────────────┐
│  • _AppSettingsKeys   — compile-time storage key constants     │
│  • mixin _$AppSettings — all methods, streams, load/refresh    │
│  • class _AppSettings  — concrete implementation + state       │
└────────────────────────────────────────────────────────────────┘

Table of Contents

  1. Installation
  2. Quick Start
  3. Presets — choosing the right API shape
  4. Method Name Templates
  5. All @PrefsModule Parameters
  6. All @PrefEntry Parameters
  7. All Supported Types
  8. Custom Serialization
  9. Key Casing
  10. Reactive UI — Streams and ChangeNotifier
  11. Error Handling for Sync Writes
  12. Lifecycle — _load, refresh, removeAll
  13. Implementing PrefsAdapter
  14. Global build.yaml Options
  15. Known Limitations
  16. AI Agents & LLM Compatibility

Installation

# pubspec.yaml
dependencies:
  preferences_annotation: ^3.0.0

dev_dependencies:
  preferences_generator: ^3.0.0
  build_runner: ^2.6.0

Quick Start

1. Define your schema

// lib/settings.dart
import 'package:preferences_annotation/preferences_annotation.dart';

part 'settings.prefs.dart';   // ← the generated file

enum AppTheme { light, dark, system }

@PrefsModule.dictionary()
abstract class AppSettings with _$AppSettings {
  factory AppSettings(PrefsAdapter adapter) = _AppSettings;

  // Every named parameter = one preference.
  // The default value is both the in-memory initial state and the
  // value restored when remove() is called.
  AppSettings._({
    String username = 'guest',
    bool isDarkMode = false,
    AppTheme theme = AppTheme.system,
    @PrefEntry(key: 'launch_counter')
    int launchCount = 0,
  });
}

2. Implement PrefsAdapter (once per storage backend)

class SharedPreferencesAdapter implements PrefsAdapter {
  final SharedPreferences _prefs;
  SharedPreferencesAdapter(this._prefs);

  @override Future<T?> get<T>(String key) async => _prefs.get(key) as T?;
  @override Future<void> remove(String key) => _prefs.remove(key);
  @override Future<void> removeAll() => _prefs.clear();

  @override
  Future<void> set<T>(String key, T value) async {
    if (value is String)       await _prefs.setString(key, value);
    else if (value is bool)    await _prefs.setBool(key, value);
    else if (value is int)     await _prefs.setInt(key, value);
    else if (value is double)  await _prefs.setDouble(key, value);
    else if (value is List<String>) await _prefs.setStringList(key, value);
  }
}

3. Generate

dart run build_runner build --delete-conflicting-outputs

4. Use

final prefs = await SharedPreferences.getInstance();
final settings = AppSettings(SharedPreferencesAdapter(prefs));

// .dictionary() generates: sync getter, async setter, async remover
print(settings.getUsername());          // 'guest'
await settings.setUsername('Alice');
print(settings.getUsername());          // 'Alice'
await settings.removeUsername();
print(settings.getUsername());          // 'guest'  (default restored)

Presets

A preset is a named constructor on @PrefsModule that pre-fills all method name templates for a common storage pattern. Every preset parameter can be overridden individually.

Preset comparison

Preset Primary use case Generated methods for username
.dictionary() shared_preferences (recommended default) getUsername() sync • setUsername(v) async • removeUsername() async
.reactive() Flutter reactive UIs with ChangeNotifier username getter • setUsername(v) sync • usernameStream stream
.syncOnly() Hive, GetStorage, in-memory maps getUsername()putUsername(v)deleteUsername() — all sync
.syncFirst() Mixed-latency — prefer sync, async available usernamesetUsername(v)usernameAsync()setUsernameAsync(v)
.minimal() CLI tools, scripts usernamesetUsername(v)removeUsername() — nothing else
.exhaustive() Every variant — useful for demos and testing getUsernameSyncsetUsernameSync(v)getUsernameAsync()setUsernameAsync(v)watchUsernameStream
.readOnly() Config loaded externally, no writes username getter • usernameAsync()
.disabled() Custom starting point — everything off (nothing — add only what you need)

Preset examples

// Flutter app with shared_preferences
@PrefsModule.dictionary()
abstract class AppSettings with _$AppSettings { ... }

// Flutter reactive UI — streams + ChangeNotifier
@PrefsModule.reactive()
abstract class AppSettings with _$AppSettings, ChangeNotifier { ... }

// CLI tool — minimal surface
@PrefsModule.minimal()
abstract class CliConfig with _$CliConfig { ... }

// Secure storage with snake_case keys
@PrefsModule(keyCase: KeyCase.snake)
abstract class SecureSettings with _$SecureSettings, ChangeNotifier { ... }

// Custom — disabled base, opt-in only what you want
@PrefsModule.disabled(
  getter:    '{{name}}',
  setter:    'update{{Name}}',
  removeAll: 'wipeAll',
)
abstract class MySettings with _$MySettings { ... }

Method Name Templates

All per-entry method configuration parameters accept a String? template with two substitution tokens.

Token reference

Token Substitution isFirstLaunch example
{{name}} Raw camelCase field name isFirstLaunch
{{Name}} Field name, first letter capitalised IsFirstLaunch

Token examples

Template Field: username Field: isDarkMode
'{{name}}' username isDarkMode
'get{{Name}}' getUsername getIsDarkMode
'set{{Name}}' setUsername setIsDarkMode
'remove{{Name}}' removeUsername removeIsDarkMode
'{{name}}Async' usernameAsync isDarkModeAsync
'set{{Name}}Async' setUsernameAsync setIsDarkModeAsync
'{{name}}Stream' usernameStream isDarkModeStream
'watch{{Name}}Stream' watchUsernameStream watchIsDarkModeStream
'on{{Name}}Changed' onUsernameChanged onIsDarkModeChanged
'toggle{{Name}}' toggleUsername toggleIsDarkMode

Null vs disabled

  • null on @PrefsModule — disables that method type for the entire module.
  • null on @PrefEntry — inherits from the module template (default behaviour).
  • PrefEntry.disabled on @PrefEntry — force-disables for this entry only.

All @PrefsModule Parameters

@PrefsModule(
  // ── Behaviour ───────────────────────────────────────────────────────────
  notifiable: true,         // call notifyListeners() on any change
                            // only meaningful when mixing in ChangeNotifier
  keyCase: KeyCase.snake,   // storage key casing (see Key Casing section)

  onWriteError: MyClass._handleError,
  // Optional: void Function(Object error, StackTrace st)
  // Called when a sync (fire-and-forget) write fails.
  // Without this, write failures are silently discarded.

  // ── Per-entry method templates (null = disabled for whole module) ────────
  getter:      '{{name}}',           // sync getter
  setter:      'set{{Name}}',        // sync setter
  remover:     'remove{{Name}}',     // sync remover
  asyncGetter: '{{name}}Async',      // async getter
  asyncSetter: 'set{{Name}}Async',   // async setter
  asyncRemover:'remove{{Name}}Async',// async remover
  streamer:    '{{name}}Stream',     // stream getter (null by default in base)

  // ── Module-level method names (literal strings, no token substitution) ──
  removeAll: 'removeAll',  // clears all storage, reloads defaults
  refresh:   'refresh',    // re-reads all values from storage
)

All @PrefEntry Parameters

@PrefEntry(
  // ── Storage ─────────────────────────────────────────────────────────────
  key: 'my_custom_key',
  // Overrides the storage key for this field only.
  // Precedence: @PrefEntry(key:) > @PrefsModule(keyCase:) > build.yaml

  // ── Method name templates (null = inherit module, PrefEntry.disabled = off)
  getter:      '{{name}}',
  setter:      'set{{Name}}',
  remover:     'remove{{Name}}',
  asyncGetter: '{{name}}Async',
  asyncSetter: 'set{{Name}}Async',
  asyncRemover:'remove{{Name}}Async',
  streamer:    '{{name}}Stream',

  // ── Shortcuts ────────────────────────────────────────────────────────────
  readOnly: true,
  // Disables setter, asyncSetter, remover, asyncRemover for this field.
  // Streams are NOT affected — the field still gets a stream if enabled.

  notifiable: false,
  // Overrides whether changes to this field call notifyListeners().
  // null → inherits the module-level setting.

  // ── Non-constant default ─────────────────────────────────────────────────
  initial: MyClass._getInitialValue,
  // Use when the default cannot be a compile-time constant.
  // Signature: TypeEntry Function()
  // Example: DateTime? creationDate needs initial: () => DateTime.now()

  // ── Custom serialization (choose one approach) ───────────────────────────
  converter: const MyConverter(),
  // A const PrefConverter<TypeEntry, TypeStorage> instance.

  toStorage: MyClass._toStorage,
  fromStorage: MyClass._fromStorage,
  // Inline static functions. Must be used together.
  // toStorage:   TypeStorage Function(TypeEntry value)
  // fromStorage: TypeEntry  Function(TypeStorage value)
)

@PrefEntry usage patterns

AppSettings._({
  // 1. Custom storage key
  @PrefEntry(key: 'launch_counter')
  int launchCount = 0,

  // 2. Custom stream name
  @PrefEntry(streamer: 'watch{{Name}}Changes')
  String? authToken,

  // 3. Custom setter name (common for booleans)
  @PrefEntry(setter: 'toggle{{Name}}', asyncSetter: 'toggle{{Name}}Async')
  bool isDarkMode = false,

  // 4. Disable one specific method for this entry only
  @PrefEntry(asyncSetter: PrefEntry.disabled)
  int readFrequentlyWriteRarely = 0,

  // 5. Make a field read-only (no setter or remover generated)
  @PrefEntry(readOnly: true)
  String installId = 'uuid-1234-abcd',

  // 6. Suppress ChangeNotifier for a frequently-changing field
  @PrefEntry(notifiable: false)
  String temporarySessionId = '',

  // 7. Non-constant default value
  @PrefEntry(initial: AppSettings._getCreationDate)
  DateTime? creationDate,

  // 8. Custom type via PrefConverter
  @PrefEntry(converter: ColorConverter())
  Color? accentColor,

  // 9. Custom type via inline functions
  @PrefEntry(toStorage: AppSettings._sessionToJson,
             fromStorage: AppSettings._sessionFromJson)
  ApiSession? session,
});

All Supported Types

The generator automatically serializes these types to and from the adapter's primitive contract — no manual conversion needed.

Natively supported (no annotation required)

Dart type Storage type Notes
int int
double double
bool bool
String String
List<String> List<String> Direct storage
List<int> String JSON-encoded
Set<int> String JSON-encoded
Set<String> String JSON-encoded
Map<String, String> Map<String, dynamic>
Map<String, dynamic> Map<String, dynamic>
Enum String Stored as .name, restored with .values.byName()
DateTime String Stored as ISO 8601 via .toIso8601String()
Duration int Stored as .inMicroseconds
Named record ({int w, int h}) Map<String, dynamic> Field names preserved
Positional record (int, String) Map<String, dynamic> Keys: f0, f1, …
Nullable variants T? same as T null returned when key absent

Custom types (annotation required)

// Option A: PrefConverter — reusable, recommended
class UriConverter extends PrefConverter<Uri, String> {
  const UriConverter();
  @override Uri    fromStorage(String v) => Uri.parse(v);
  @override String toStorage(Uri v)     => v.toString();
}

@PrefEntry(converter: UriConverter())
Uri? apiEndpoint,

// Option B: Inline static functions — one-off types
@PrefEntry(
  toStorage:   AppSettings._userToMap,
  fromStorage: AppSettings._userFromMap,
)
User? currentUser,

static Map<String, dynamic> _userToMap(User u) => u.toJson();
static User _userFromMap(Map<String, dynamic> m) => User.fromJson(m);

Custom Serialization

Complete PrefConverter example

import 'package:preferences_annotation/preferences_annotation.dart';

/// Serializes a [Color] to/from its ARGB integer representation.
class ColorConverter extends PrefConverter<Color, int> {
  const ColorConverter();            // must be const

  @override
  Color fromStorage(int value) => Color(value);

  @override
  int toStorage(Color value) => value.toARGB32();
}

// Usage in schema:
@PrefEntry(converter: ColorConverter())
Color? accentColor,

PrefConverter type parameter guide

PrefConverter<TypeEntry, TypeStorage>
              ─────────  ────────────
              Your type  What the adapter stores
              e.g. Color e.g. int

TypeStorage must be one of the natively supported types above.


Key Casing

Storage key casing is resolved in this priority order (highest first):

@PrefEntry(key: 'my_key')           ← always wins
  ↓
@PrefsModule(keyCase: KeyCase.snake) ← overrides build.yaml
  ↓
build.yaml: options: key_case: snake ← project-wide default
  ↓
KeyCase.asis                         ← field name unchanged (fallback)

Available KeyCase values

Value launchCount isDarkMode
asis 'launchCount' 'isDarkMode'
snake 'launch_count' 'is_dark_mode'
camel 'launchCount' 'isDarkMode'
pascal 'LaunchCount' 'IsDarkMode'
kebab 'launch-count' 'is-dark-mode'

build.yaml global key casing

# <project>/build.yaml
targets:
  $default:
    builders:
      preferences_generator|preferences:
        options:
          key_case: snake   # applies to every module in this package

Reactive UI

Streams

Enable streams with the streamer template parameter. Any entry can have its own stream regardless of whether the module uses ChangeNotifier.

// Module-wide streams (reactive preset)
@PrefsModule.reactive()            // streamer: '{{name}}Stream' by default
abstract class AppSettings with _$AppSettings, ChangeNotifier { ... }
// Generates: usernameStream, themeModeStream, etc.

// Per-entry stream opt-in (base module, no streams by default)
@PrefsModule(keyCase: KeyCase.snake)
abstract class SecureSettings with _$SecureSettings, ChangeNotifier {
  SecureSettings._({
    @PrefEntry(streamer: 'watch{{Name}}Stream')
    String? authToken,                    // generates: watchAuthTokenStream

    @PrefEntry(streamer: '{{name}}Stream')
    bool areBiometricsEnabled = false,    // generates: areBiometricsEnabledStream

    ApiSession? apiSession,               // no stream — ChangeNotifier only
  });
}

StreamBuilder in Flutter

// Stream fires when:
//   1. A setter is called (immediate, optimistic update)
//   2. _load() reads a changed value from storage (on startup + refresh())
//   3. removeAll() restores all fields to defaults

StreamBuilder<String>(
  stream: settings.usernameStream,
  builder: (context, snap) {
    return Text(snap.data ?? 'loading...');
  },
)

ChangeNotifier + ListenableBuilder

@PrefsModule.reactive()
abstract class AppSettings with _$AppSettings, ChangeNotifier {
  factory AppSettings(PrefsAdapter adapter) = _AppSettings;
  AppSettings._({ ThemeMode themeMode = ThemeMode.system });
}

// In your widget tree — rebuilds whenever any setting changes:
ListenableBuilder(
  listenable: appSettings,
  builder: (context, _) => MaterialApp(
    themeMode: appSettings.themeMode,
  ),
)

Suppressing notifyListeners for specific fields

// This field changes frequently (e.g., scroll position).
// Suppress notifications to avoid unnecessary rebuilds.
@PrefEntry(notifiable: false)
double scrollOffset = 0.0,

Error Handling

Synchronous setters use a fire-and-forget Future(() async { ... }) write. Without onWriteError, storage failures are silently discarded — the in-memory value is always updated immediately (optimistic update).

@PrefsModule.reactive(onWriteError: AppSettings._onWriteError)
abstract class AppSettings with _$AppSettings, ChangeNotifier {
  factory AppSettings(PrefsAdapter adapter) = _AppSettings;

  AppSettings._({ String username = 'guest' });

  static void _onWriteError(Object error, StackTrace st) {
    // Log, report to Sentry, show a snackbar, etc.
    debugPrint('[Prefs] Write failed: $error\n$st');
  }
}

Async setters (await settings.setUsernameAsync('Alice')) propagate exceptions normally — wrap them in try/catch at the call site.


Lifecycle

How _load() works

_load() is the private method the generator produces. It is:

  • Called once automatically in the constructor (fire-and-forget).
  • Guarded by _isLoaded = false — once loaded, subsequent calls are no-ops.
  • Re-armed by refresh() and removeAll(), which reset _isLoaded = false.
  • Pushes changed values to stream controllers when a value read from storage differs from the in-memory cache.
// Calling multiple async getters in sequence is safe — only one storage
// read cycle occurs regardless of how many getters are called.
final name = await settings.getUsernameAsync();
final mode = await settings.getThemeModeAsync();  // no second storage read

refresh()

Re-reads all values from storage. Any changed values are pushed to their stream controllers and notifyListeners() is called if values changed.

// Call after the app returns to foreground, or after an external write.
await settings.refresh();

removeAll()

Clears all storage keys, then re-reads (restoring defaults). All stream controllers receive the default values. notifyListeners() is called.

await settings.removeAll();  // equivalent to a factory reset

Implementing PrefsAdapter

The adapter contract is intentionally minimal — only primitive types. The generator handles all serialization for complex types.

abstract interface class PrefsAdapter {
  Future<T?> get<T>(String key);          // T is always a primitive
  Future<void> set<T>(String key, T value);
  Future<void> remove(String key);
  Future<void> removeAll();
}

Which primitives does the adapter receive?

Dart field type Adapter T
int, bool, double, String Same type
List<String> List<String>
Set<*>, Map<*>, Record, DateTime (when stored as JSON), custom String or Map<String, dynamic>
Enum String
DateTime String
Duration int

Complete shared_preferences adapter

class SharedPreferencesAdapter implements PrefsAdapter {
  final SharedPreferences _prefs;
  SharedPreferencesAdapter(this._prefs);

  @override
  Future<T?> get<T>(String key) async => _prefs.get(key) as T?;

  @override
  Future<void> remove(String key) => _prefs.remove(key);

  @override
  Future<void> removeAll() => _prefs.clear();

  @override
  Future<void> set<T>(String key, T value) async {
    switch (value) {
      case final String v:       await _prefs.setString(key, v);
      case final bool v:         await _prefs.setBool(key, v);
      case final int v:          await _prefs.setInt(key, v);
      case final double v:       await _prefs.setDouble(key, v);
      case final List<String> v: await _prefs.setStringList(key, v);
      default:
        // Map<String, dynamic> — store as JSON string
        if (value is Map) await _prefs.setString(key, jsonEncode(value));
    }
  }
}

Minimal in-memory adapter (for testing / console apps)

class InMemoryAdapter implements PrefsAdapter {
  final _store = <String, dynamic>{};

  @override Future<T?> get<T>(String key) async => _store[key] as T?;
  @override Future<void> set<T>(String key, T value) async => _store[key] = value;
  @override Future<void> remove(String key) async => _store.remove(key);
  @override Future<void> removeAll() async => _store.clear();
}

Global build.yaml Options

targets:
  $default:
    builders:
      preferences_generator|preferences:
        options:
          # Project-wide default key casing.
          # Overridden by @PrefsModule(keyCase:) and @PrefEntry(key:).
          # Values: asis | snake | camel | pascal | kebab
          key_case: snake

          # Override the generated file extension (default: .prefs.dart)
          # build_extensions:
          #   '.dart': '.g.dart'

Known Limitations

Null distinction. With containsKey removed from the adapter contract, there is no way to distinguish a nullable field that was never written from one explicitly set to null. Both return null from get(). If the distinction matters, store a non-null sentinel value instead.


AI Agents & LLM Compatibility

This section is written for AI coding assistants (Claude, Copilot, Cursor, Gemini, etc.) to use as a reference when generating code with this package.

Decision tree: which preset?

Does the app use Flutter?
├── Yes
│   ├── Needs reactive UI (streams or ChangeNotifier)?
│   │   ├── Yes → @PrefsModule.reactive()     [add ChangeNotifier to class]
│   │   └── No  → @PrefsModule.dictionary()   [async writes, sync reads]
│   └── Backend is fully synchronous (Hive)?
│       └── Yes → @PrefsModule.syncOnly()
└── No (Dart CLI / server)
    ├── Needs full API surface?
    │   └── Yes → @PrefsModule.exhaustive()
    └── No → @PrefsModule.minimal()

Required class structure (invariant)

Every @PrefsModule class must have exactly:

@PrefsModule.<preset>()
abstract class MySettings with _$MySettings {    // ← 'with _$MySettings' required
  factory MySettings(PrefsAdapter adapter) = _MySettings; // ← factory required
  MySettings._({ /* schema */ });                          // ← private ctor required
}

// When using ChangeNotifier:
abstract class MySettings with _$MySettings, ChangeNotifier { ... }

Schema constructor rules

MySettings._({
  // ✅ Compile-time constant default
  String username = 'guest',
  bool flag = false,
  int count = 0,
  MyEnum value = MyEnum.first,

  // ✅ Nullable — no default needed (null is the default)
  String? optionalValue,

  // ✅ Non-constant default — use @PrefEntry(initial:)
  @PrefEntry(initial: MySettings._now)
  DateTime? createdAt,

  // ✅ Custom type — must provide converter or toStorage/fromStorage
  @PrefEntry(converter: MyConverter())
  MyType? customValue,

  // ❌ WRONG — non-const default without @PrefEntry(initial:)
  // DateTime createdAt = DateTime.now(),  // will not compile
});

Correct part directive

part 'my_settings.prefs.dart';   // ← always .prefs.dart, not .g.dart

Complete reactive example (copy-paste ready)

// lib/app_settings.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:preferences_annotation/preferences_annotation.dart';

part 'app_settings.prefs.dart';

enum AppTheme { light, dark, system }

@PrefsModule.reactive(onWriteError: AppSettings._onWriteError)
abstract class AppSettings with _$AppSettings, ChangeNotifier {
  factory AppSettings(PrefsAdapter adapter) = _AppSettings;

  AppSettings._({
    String username = 'guest',
    bool isDarkMode = false,
    AppTheme theme = AppTheme.system,
    int? lastNotificationId,
    List<String> recentSearches = const <String>[],
    Duration sessionTimeout = const Duration(minutes: 30),

    @PrefEntry(converter: ColorConverter())
    Color? accentColor,

    @PrefEntry(key: 'launch_counter', streamer: 'on{{Name}}Updated')
    int launchCount = 0,

    @PrefEntry(readOnly: true)
    String installId = 'uuid-1234-abcd',

    @PrefEntry(notifiable: false, initial: AppSettings._now)
    DateTime? lastActiveAt,
  });

  static void _onWriteError(Object e, StackTrace st) =>
      debugPrint('[AppSettings] write error: $e');

  static DateTime _now() => DateTime.now();
}
// lib/adapters/shared_preferences_adapter.dart
// (see Implementing PrefsAdapter section above)
// lib/main.dart
late final AppSettings appSettings;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final prefs = await SharedPreferences.getInstance();
  appSettings = AppSettings(SharedPreferencesAdapter(prefs));
  await appSettings.refresh(); // loads storage → fires streams
  runApp(const App());
}
// lib/app.dart
ListenableBuilder(
  listenable: appSettings,
  builder: (context, _) => MaterialApp(
    themeMode: appSettings.isDarkMode ? ThemeMode.dark : ThemeMode.light,
  ),
)

// Stream-driven widget:
StreamBuilder<int>(
  stream: appSettings.onLaunchCountUpdated,  // custom stream name
  builder: (context, snap) => Text('Launches: ${snap.data ?? 0}'),
)

Common mistakes AI agents make

Mistake Correct
part 'settings.g.dart' part 'settings.prefs.dart'
class MySettings (non-abstract) abstract class MySettings
Missing with _$MySettings abstract class MySettings with _$MySettings
Missing factory constructor factory MySettings(PrefsAdapter adapter) = _MySettings;
DateTime now = DateTime.now() @PrefEntry(initial: MySettings._now) DateTime? now,
settings.usernameStream when using .dictionary() streams are not generated by .dictionary() — use .reactive()
settings.setUsername('x') when using .dictionary() .dictionary() setters are async: await settings.setUsername('x')
Referencing settings.username when using .dictionary() getter name is getUsername() in .dictionary() preset
AffixConfig, CustomConfig, NamedConfig (v2 API) Use template strings: setter: 'set{{Name}}'

Generated method name reference (by preset, field username)

Preset          getter          setter              remover             stream
──────────────────────────────────────────────────────────────────────────────────
dictionary()    getUsername()   setUsername() async removeUsername() async  —
reactive()      username        setUsername()       removeUsername()    usernameStream
syncOnly()      getUsername()   putUsername()       deleteUsername()    —
syncFirst()     username        setUsername()       removeUsername()    —
                usernameAsync() setUsernameAsync()  removeUsernameAsync()
minimal()       username        setUsername()       removeUsername()    —
exhaustive()    getUsernameSync setUsernameSync     removeUsernameSync  watchUsernameStream
                getUsernameAsync setUsernameAsync   removeUsernameAsync

Packages

Package Role
preferences_generator build_runner generator — dev dependency
preferences_annotation Annotations & interfaces — runtime dependency