app_preference 0.2.1
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 #
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
- app_preference: The core of the library
- app_preference_shared_preferences: Adapter for shared_preferences
- app_preference_secure_storage: Adapter for flutter_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 writenull
to adapter, instead of serializing thedefaultValue
.
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);
});