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);
});

Libraries

app_preference
A library to store and retrieve app preferences.
app_preference_plugin_utils
A plugin interface for app_preference.