app_preference 0.2.1 copy "app_preference: ^0.2.1" to clipboard
app_preference: ^0.2.1 copied to clipboard

A library that makes accessing shared preferences or secured storage a lot easier. It handles the common use case that reads or write preferences. It also supports read and write serializable complex [...]

app_preference #

Star this Repo Pub Package Build Status

Ever found yourself juggling between shared_preferences and flutter_secure_storage to manage user preferences? Or maybe you've been wrestling with inconsistent APIs, async issues, custom serialization, or unit testing? Or simply want to be reactive on preference value changes. Say hello to app_preference, your one-stop solution for all these headaches.

Get Started in a Flash 🚀 #

> flutter pub add shared_preferences app_preference_shared_preferences app_preference_secure_storage

Effortless Preferences with shared_preferences #

import 'package:app_preference/app_preference.dart';
import 'package:app_preference_shared_preferences/app_preference_shared_preferences.dart';

final SharedPreferences sharedPreferences = getSharedPreference();
final AppPreferenceAdapter sharedPreferencesAdapter = SharedPreferencesAdapter(sharedPreferences);

final userNamePref = AppPreference<String>.direct(
  adapter: sharedPreferencesAdapter
  key: 'user_name',
  defaultValue: '<unknown>',
);

print(userPref.value); // prints '<unknown>' if first time use or 'my_name' for returned user.

// Update the value and persisted it back to shared preferences in background.
userPref.value = 'my_name';

print(userPref.value); // prints 'my_name'

Fort Knox Security with flutter_secure_storage #

import 'package:app_preference_secure_storage/app_preference_secure_storage.dart';

final FlutterSecureStorage secureStorage = getSecureStorage();
final AppPreferenceAdapter secureStorageAdapter = SecureStorageAdapter(secureStorage)

final userTokenPref = AppPreference<String?>.direct(
  adapter: secureStorageAdapter
  key: 'user_token',
  defaultValue: null,
);

userTokenPref.value = await authenticateUser(userId, password);

await invokeApi(userToken: userTokenPref.value);

Everyone loves JSON #

class UserTokens {
  final String idToken;
  final String accessToken;
  final String refreshToken;

  const UserTokens(this.idToken, this.accessToken, this.refreshToken);

  factory UserTokens.fromJson(Map<String, dynamic> json) => ....
  Map<String, dynamic> toJson() => ....

  @override
  bool operator ==(dynamic other) => ...

  @override
  int get hashCode => ...

  static const empty = UserTokens("", "", "");
}

final userTokenPref = AppPreference<UserTokens>.serialized(
  adapter: adapter
  key: 'user_tokens',
  defaultValue: UserTokens.empty,
  serializer: (tokens) => tokens.toJson(),
  deserializer: UserTokens.fromJson,
);

NOTICE:

By given defaultValue:

  • when the library is reading the value from adapter, if the value is not found, it will return the defaultValue instead.
  • when the library is writing the value to adapter, if the value is the same as defaultValue, it will write null to adapter, instead of serializing the defaultValue.

Serialize Like a Pro 🎩 #

Given you want to serialize UserTokens.empty as null, which would remove the stored value.

const dataSigner = createDataSigner() // Create a data signer that can user to create a signature for the data or verified it

final userTokenPref = AppPreference<UserTokens>.customSerialized(
  adapter: adapter
  key: 'user_tokens',
  serializer: (UserTokens tokens) {
    if(tokens == UserTokens.empty) {
      return null;
    } else {
      final data = tokens.toJson();
      final signature = dataSigner.sign(data);
      return jsonEncode({ "data": data, "signature": signature });
    }
  },
  deserializer: (String? data) {
    if(data == null) {
      return UserTokens.empty;
    }

    try {
      final decoded = jsonDecode(data!) as Map<String, dynamic>;

      if (decoded case { "data": final Map<String, dynamic> data, "signature": final String signature }) {
        final userTokens = UserTokens.fromJson(data);
        if(!dataSigner.verify(userTokens, signature)) {
          throw Exception("Signature doesn't match");
        }

        return userTokens;
      }
      else {
        throw Exception('Could not parse user tokens');
      }
    } catch (ex) {
      print('Failed to deserialize user tokens: $ex');

      return UserTokens.empty;
    }
  }
);

Unleash the Power ⚡ #

Async? No Sweat #

final userTokenPref = AppPreference<String?>.direct(
  adapter: secureStorageAdapter
  key: 'user_token',
  defaultValue: null,
);

await userTokenPref.ready; // wait until `flutter_secure_storage` returned the value.

print(userTokenPref.value); // Value is loaded

Ensured async read #

print(await userTokenPref.ensuredRead());

Ensured async creation #

final userTokenPref = await AppPreference<String?>.direct(
  adapter: secureStorageAdapter
  key: 'user_token',
  defaultValue: null,
).ensuredCreation();

Reactivity: Your UI's Best Friend #

// With Mobx
class UserNameWidget extends StatelessWidget {
  final AppPreference<String> userNamePref;

  const UserNameWidget({super.key, required this.userNamePref});

  @override
  Widget build(BuildContext context) => Observer(
    builder: (_) => Text(
      '${userNamePref.value}',
    ),
  );
}
// Mobx isn't an option? No problem!
class UserNameWidget extends StatelessWidget {
  final AppPreference<String> userNamePref;

  const UserNameWidget({super.key, required this.userNamePref});

  @override
  Widget build(BuildContext context) => StreamBuilder(
    stream: userNamePref.valueStream()
    builder: (_, snapshot) => Text(
      '${snapshot.data}',
    ),
  );
}

The magic of reaction #

late final AppPreference<UserSessions> _userSessionPref;

@override
void initState() {
  super.initState();

  _userSessionPref = getUserSessionPref();

  _userSessionPref.subscribeChanges((session) {
    // session changed!
    if(session == UserSessions.empty) {
      // Session changed to empty!;
      Navigator.restorablePushReplacementNamed(context, '/unauthorized',);
    }
  });
}

Logging & Error Handling: Keep Calm and Log On #

app_preference uses Logging library to do log and error reporting.

If the app uses logging too, nothing you need to do, the integration has been done automatically.

Or you can access the AppPreference.logger to more granular setup.

We don't use logging #

AppPreference.onLog((log) {
  print('${log.time} [${log.loggerName}](${log.level}): ${log.message})');
  if(log.error != null) print('Error: ${log.error}');
  if(log.stackTrace !=null) print('StackTrace: ${log.stackTrace}');
});

Just care about error #

AppPreference.onError((message, error, stackTrace) {
  Crashlytics.instance.recordError(error, stackTrace, reason: message);
});
2
likes
140
points
99
downloads

Publisher

verified publisherpub.timnew.me

Weekly Downloads

A library that makes accessing shared preferences or secured storage a lot easier. It handles the common use case that reads or write preferences. It also supports read and write serializable complex types in a type-safe way. It also supports listening to the value changes. It exposes its values as mobx observable values, but it can actually works with any state management library.

Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

flutter, logging, mobx

More

Packages that depend on app_preference