Skip to content

Instantly share code, notes, and snippets.

@DattatreyaReddy
Last active November 18, 2024 23:47
Show Gist options
  • Save DattatreyaReddy/e7974c82e877c047bac45e55ec098a74 to your computer and use it in GitHub Desktop.
Save DattatreyaReddy/e7974c82e877c047bac45e55ec098a74 to your computer and use it in GitHub Desktop.
Riverpod state persistence with Shared Preference
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../constants/db_keys.dart';
import '../../../controllers/common_controllers.dart';
import '../../extensions/custom_extensions.dart';
/// [SharedPreferenceAsyncClient] is a mixin to add [_get] and [update] functions to
/// the provider.
/// this will first try to retrieve the value from the [_key] in the [_client].
///
/// if the value is not present in the [_client] then it will call the [_networkCall]
/// and update the [_client] with the value returned from the [_networkCall].
/// and then return the value.
///
/// if the value is present in the [_client] then it will return the value
/// and also make the api call to update the [_client] with the
/// latest value returned
///
/// * Remember to use [ initialize ] function to assign [key], [networkCall]
/// and decoder [fromJson] and encoder [toJson]
/// in [build] function of provider.
///
/// * [State] should not be a Nullable Type.
mixin SharedPreferenceAsyncClient<State extends Object> {
// functions from AsyncNotifierProviderBase
set state(AsyncValue<State?> newState);
AsyncValue<State?> get state;
late final String _key;
late final SharedPreferences _client;
late final dynamic Function(State)? _toJson;
late final State? Function(dynamic)? _fromJson;
late final AsyncValueGetter<State?> _networkCall;
AutoDisposeAsyncNotifierProviderRef<State?> get ref;
FutureOr<State?> initialize<U>(
DBKeys key,
AsyncValueGetter<State?> networkCall, {
dynamic Function(State)? toJson,
State? Function(dynamic)? fromJson,
}) async {
_client = ref.watch(sharedPreferencesProvider);
_key = key.name;
_toJson = toJson;
_fromJson = fromJson;
_networkCall = networkCall;
_persistenceRefreshLogic(ref);
final storedValue = _get;
if (storedValue != null) {
updateStateWithNetworkCall();
return storedValue;
}
return networkCall();
}
void updateStateWithNetworkCall() async {
state = await AsyncValue.guard(() => _networkCall());
}
State? get _get {
final value = _client.get(_key);
if (_fromJson != null) {
return _fromJson!(jsonDecode(value.toString()));
}
if (value != null && value is List) {
// if value is List<Object> then the only possible type is List<String>
// as SharedPreferences only saves List<String> type.
// casting it to List<String> and then to T? to
// avoid comparing List<Object> with T?.
return value.map((e) => e.toString()).toList() as State?;
}
return value is State? ? value : null;
}
void _persistenceRefreshLogic(
AutoDisposeAsyncNotifierProviderRef<State?> ref) =>
ref.listenSelf((prev, next) {
if (prev?.valueOrNull != next.valueOrNull) _set(next.valueOrNull);
});
Future<bool> _set(State? value) async {
if (value == null) return _client.remove(_key);
if (_toJson != null) {
_client.setString(_key, jsonEncode(_toJson!(value)));
}
if (value is bool) {
return await _client.setBool(_key, value);
} else if (value is double) {
return await _client.setDouble(_key, value);
} else if (value is int) {
return await _client.setInt(_key, value);
} else if (value is String) {
return await _client.setString(_key, value);
} else if (value is List<String>) {
return await _client.setStringList(_key, value);
}
return false;
}
}
/// [SharedPreferenceClientMixin] is a mixin to add [_get] and [update] functions to
/// the provider.
///
/// * Remember to use [ initialize ] function to assign [key], [initial]
/// and decoder [fromJson] and encoder [toJson]
/// in [build] function of provider.
///
/// * optionally provide [initial] for giving initial value to the [key].
///
/// * [State] should not be a Nullable Type.
mixin SharedPreferenceClient<State extends Object> {
late final String _key;
late final SharedPreferences _client;
late final State? _initial;
set state(State? newState);
State? get state;
late final dynamic Function(State)? _toJson;
late final State? Function(dynamic)? _fromJson;
AutoDisposeNotifierProviderRef<State?> get ref;
State? initialize<U>(
DBKeys key, {
State? initial,
dynamic Function(State)? toJson,
State? Function(dynamic)? fromJson,
}) {
_client = ref.watch(sharedPreferencesProvider);
_key = key.name;
_initial = initial ?? key.initial;
_toJson = toJson;
_fromJson = fromJson;
_persistenceRefreshLogic(ref);
return _get ?? _initial;
}
void update(State? value) => state = value;
void updateWithPreviousState(State? Function(State?) operation) =>
state = operation(state);
State? get _get {
final value = _client.get(_key);
if (_fromJson != null) {
return _fromJson!(jsonDecode(value.toString()));
}
if (value != null && value is List) {
// if value is List<Object> then the only possible type is List<String>
// as SharedPreferences only saves List<String> type.
// casting it to List<String> and then to T? to
// avoid comparing List<Object> with T?.
return value.map((e) => e.toString()).toList() as State?;
}
return value is State? ? value : _initial;
}
void _persistenceRefreshLogic(AutoDisposeNotifierProviderRef<State?> ref) =>
ref.listenSelf((prev, next) {
if (prev != next) _set(next);
});
Future<bool> _set(State? value) async {
if (value == null) return _client.remove(_key);
if (_toJson != null) {
_client.setString(_key, jsonEncode(_toJson!(value)));
}
if (value is bool) {
return await _client.setBool(_key, value);
} else if (value is double) {
return await _client.setDouble(_key, value);
} else if (value is int) {
return await _client.setInt(_key, value);
} else if (value is String) {
return await _client.setString(_key, value);
} else if (value is List<String>) {
return await _client.setStringList(_key, value);
}
return false;
}
}
/// [SharedPreferenceEnumClientMixin] is a mixin to add [get] and [update] functions to
/// the provider.
///
/// * Remember to initialize [key], [enumList] in [build] function of provider
/// * optionally provide [initial] for giving initial value to the [key].
///
/// * [State] should be an [Enum] type and not a Nullable Type.
mixin SharedPreferenceEnumClient<State extends Enum> {
late String _key;
late SharedPreferences _client;
State? _initial;
late List<State> _enumList;
set state(State? newState);
AutoDisposeNotifierProviderRef<State?> get ref;
State? initialize(
DBKeys key, {
required List<State> enumList,
State? initial,
}) {
_client = ref.watch(sharedPreferencesProvider);
_key = key.name;
_initial = initial ?? key.initial;
_enumList = enumList;
_persistenceRefreshLogic(ref);
return _get;
}
void update(State? value) => state = value;
State? _getEnumFromIndex(int? value) =>
value.liesBetween(upper: _enumList.length - 1)
? _enumList[value!]
: _initial;
State? get _get => _getEnumFromIndex(_client.getInt(_key));
Future<bool> _set(int? value) {
if (value == null) return _client.remove(_key);
return _client.setInt(_key, value);
}
void _persistenceRefreshLogic(AutoDisposeNotifierProviderRef<State?> ref) =>
ref.listenSelf(
(prev, next) {
if (prev != next) {
_set(next == null ? null : _enumList.indexOf(next));
}
},
);
}
@riverpod
class ManagerNumberLocal extends _$ManagerNumberLocal
with SharedPreferenceClient<String> {
@override
String? build() => initialize(DBKeys.managerNumber);
}
@riverpod
class StgTypeLocal extends _$StgTypeLocal
with SharedPreferenceEnumClient<StgType> {
@override
StgType? build() => initialize(DBKeys.stg, enumList: StgType.values);
}
enum DBKeys {
managerNumber(null),
stg(StgType.stg0);
const DBKeys(this.initial);
final dynamic initial;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment