314 lines
7.4 KiB
Dart
314 lines
7.4 KiB
Dart
/// Riverpod provider optimization utilities
|
|
///
|
|
/// Features:
|
|
/// - Granular rebuild prevention with .select()
|
|
/// - Provider caching strategies
|
|
/// - Debounced provider updates
|
|
/// - Performance-optimized provider patterns
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'debouncer.dart';
|
|
|
|
/// Extension for optimized provider watching
|
|
extension ProviderOptimizationExtensions on WidgetRef {
|
|
/// Watch only a specific field to minimize rebuilds
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final name = ref.watchField(userProvider, (user) => user.name);
|
|
/// ```
|
|
T watchField<S, T>(ProviderListenable<S> provider, T Function(S) selector) {
|
|
return watch(provider.select(selector));
|
|
}
|
|
|
|
/// Watch multiple fields efficiently
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final (name, age) = ref.watchFields(
|
|
/// userProvider,
|
|
/// (user) => (user.name, user.age),
|
|
/// );
|
|
/// ```
|
|
T watchFields<S, T>(ProviderListenable<S> provider, T Function(S) selector) {
|
|
return watch(provider.select(selector));
|
|
}
|
|
|
|
/// Listen to provider only when condition is met
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// ref.listenWhen(
|
|
/// userProvider,
|
|
/// (prev, next) => prev?.id != next?.id,
|
|
/// (prev, next) {
|
|
/// // Handle change
|
|
/// },
|
|
/// );
|
|
/// ```
|
|
void listenWhen<T>(
|
|
ProviderListenable<T> provider,
|
|
bool Function(T? previous, T next) condition,
|
|
void Function(T? previous, T next) listener, {
|
|
void Function(Object error, StackTrace stackTrace)? onError,
|
|
}) {
|
|
T? previous;
|
|
listen<T>(
|
|
provider,
|
|
(prev, next) {
|
|
if (condition(prev, next)) {
|
|
listener(prev, next);
|
|
previous = next;
|
|
}
|
|
},
|
|
onError: onError,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Debounced state notifier for performance optimization
|
|
abstract class DebouncedStateNotifier<T> extends StateNotifier<T> {
|
|
final Debouncer _debouncer;
|
|
|
|
DebouncedStateNotifier(
|
|
super.state, {
|
|
int debounceDuration = 300,
|
|
}) : _debouncer = Debouncer(milliseconds: debounceDuration);
|
|
|
|
/// Update state with debouncing
|
|
void updateDebounced(T newState) {
|
|
_debouncer.run(() {
|
|
if (mounted) {
|
|
state = newState;
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Update state immediately (bypass debouncing)
|
|
void updateImmediate(T newState) {
|
|
_debouncer.cancel();
|
|
if (mounted) {
|
|
state = newState;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_debouncer.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
/// Cached async value to prevent unnecessary rebuilds
|
|
class CachedAsyncValue<T> {
|
|
final AsyncValue<T> _value;
|
|
final DateTime _timestamp;
|
|
final Duration _cacheDuration;
|
|
|
|
CachedAsyncValue(
|
|
this._value, {
|
|
Duration cacheDuration = const Duration(minutes: 5),
|
|
}) : _timestamp = DateTime.now(),
|
|
_cacheDuration = cacheDuration;
|
|
|
|
AsyncValue<T> get value => _value;
|
|
|
|
bool get isExpired {
|
|
return DateTime.now().difference(_timestamp) > _cacheDuration;
|
|
}
|
|
|
|
bool get isValid {
|
|
return !isExpired && _value is AsyncData<T>;
|
|
}
|
|
}
|
|
|
|
/// Provider cache manager
|
|
class ProviderCacheManager {
|
|
static final Map<String, CachedAsyncValue> _cache = {};
|
|
|
|
/// Get cached value or compute new one
|
|
static AsyncValue<T> getOrCompute<T>({
|
|
required String key,
|
|
required AsyncValue<T> Function() compute,
|
|
Duration cacheDuration = const Duration(minutes: 5),
|
|
}) {
|
|
final cached = _cache[key] as CachedAsyncValue<T>?;
|
|
|
|
if (cached != null && cached.isValid) {
|
|
return cached.value;
|
|
}
|
|
|
|
final value = compute();
|
|
_cache[key] = CachedAsyncValue(value, cacheDuration: cacheDuration);
|
|
|
|
return value;
|
|
}
|
|
|
|
/// Invalidate specific cache entry
|
|
static void invalidate(String key) {
|
|
_cache.remove(key);
|
|
}
|
|
|
|
/// Clear all cache
|
|
static void clear() {
|
|
_cache.clear();
|
|
}
|
|
|
|
/// Clean expired cache entries
|
|
static void cleanExpired() {
|
|
_cache.removeWhere((key, value) => value.isExpired);
|
|
}
|
|
}
|
|
|
|
/// Optimized family provider cache
|
|
class FamilyProviderCache<Arg, T> {
|
|
final Map<Arg, T> _cache = {};
|
|
final int _maxSize;
|
|
|
|
FamilyProviderCache({int maxSize = 50}) : _maxSize = maxSize;
|
|
|
|
T? get(Arg arg) => _cache[arg];
|
|
|
|
void set(Arg arg, T value) {
|
|
// Simple LRU: remove oldest if cache is full
|
|
if (_cache.length >= _maxSize) {
|
|
final firstKey = _cache.keys.first;
|
|
_cache.remove(firstKey);
|
|
}
|
|
_cache[arg] = value;
|
|
}
|
|
|
|
void invalidate(Arg arg) {
|
|
_cache.remove(arg);
|
|
}
|
|
|
|
void clear() {
|
|
_cache.clear();
|
|
}
|
|
|
|
int get size => _cache.length;
|
|
}
|
|
|
|
/// Performance-optimized state notifier mixin
|
|
mixin PerformanceOptimizedNotifier<T> on StateNotifier<T> {
|
|
/// Track rebuild count in debug mode
|
|
int _rebuildCount = 0;
|
|
|
|
@override
|
|
set state(T value) {
|
|
if (kDebugMode) {
|
|
_rebuildCount++;
|
|
if (_rebuildCount % 10 == 0) {
|
|
debugPrint(
|
|
'⚠️ ${runtimeType.toString()} has been rebuilt $_rebuildCount times',
|
|
);
|
|
}
|
|
}
|
|
super.state = value;
|
|
}
|
|
|
|
/// Update state only if changed
|
|
void updateIfChanged(T newState) {
|
|
if (state != newState) {
|
|
state = newState;
|
|
}
|
|
}
|
|
|
|
/// Update part of state efficiently
|
|
void updatePart<S>(S Function(T) selector, S newValue) {
|
|
if (selector(state) != newValue) {
|
|
// This requires implementing proper state copying
|
|
// Subclasses should override this
|
|
throw UnimplementedError('updatePart must be implemented in subclass');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Selector helper for complex state
|
|
class StateSelector<T> {
|
|
final T state;
|
|
|
|
const StateSelector(this.state);
|
|
|
|
/// Select a field from state
|
|
S select<S>(S Function(T) selector) => selector(state);
|
|
|
|
/// Select multiple fields
|
|
R selectAll<R>(R Function(T) selector) => selector(state);
|
|
}
|
|
|
|
/// Optimized consumer for minimal rebuilds
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// OptimizedConsumer<UserState>(
|
|
/// selector: (state) => state.name,
|
|
/// builder: (context, name, child) {
|
|
/// return Text(name);
|
|
/// },
|
|
/// )
|
|
/// ```
|
|
class OptimizedConsumer<T, S> extends ConsumerWidget {
|
|
final ProviderListenable<T> provider;
|
|
final S Function(T) selector;
|
|
final Widget Function(BuildContext context, S value, Widget? child) builder;
|
|
final Widget? child;
|
|
|
|
const OptimizedConsumer({
|
|
super.key,
|
|
required this.provider,
|
|
required this.selector,
|
|
required this.builder,
|
|
this.child,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final value = ref.watch(provider.select(selector));
|
|
return builder(context, value, child);
|
|
}
|
|
}
|
|
|
|
/// Batch state updates for performance
|
|
class BatchedStateUpdates<T> {
|
|
final void Function(T) updateState;
|
|
final List<void Function(T)> _pendingUpdates = [];
|
|
final Debouncer _debouncer;
|
|
|
|
BatchedStateUpdates(
|
|
this.updateState, {
|
|
int debounceDuration = 100,
|
|
}) : _debouncer = Debouncer(milliseconds: debounceDuration);
|
|
|
|
/// Queue an update
|
|
void queueUpdate(void Function(T) update) {
|
|
_pendingUpdates.add(update);
|
|
_debouncer.run(_flushUpdates);
|
|
}
|
|
|
|
/// Flush all pending updates
|
|
void _flushUpdates() {
|
|
if (_pendingUpdates.isEmpty) return;
|
|
|
|
// Apply all updates in batch
|
|
for (final update in _pendingUpdates) {
|
|
// Note: This would need proper implementation based on state type
|
|
// updateState(update);
|
|
}
|
|
|
|
_pendingUpdates.clear();
|
|
}
|
|
|
|
/// Force flush immediately
|
|
void flush() {
|
|
_debouncer.cancel();
|
|
_flushUpdates();
|
|
}
|
|
|
|
void dispose() {
|
|
_debouncer.dispose();
|
|
_pendingUpdates.clear();
|
|
}
|
|
}
|