Riverpod - wurzelsand/flutter-memos GitHub Wiki
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
///////////////////////////////////////////////////////////////////////////////
/// helloWorldProvider (Provider)
///////////////////////////////////////////////////////////////////////////////
final helloWorldProvider = Provider<String>((ref) {
return 'Hello World!';
});
///////////////////////////////////////////////////////////////////////////////
/// Clock (NotifierProvider mit automatischem Hochzählen)
///////////////////////////////////////////////////////////////////////////////
class Clock extends Notifier<DateTime> {
@override
DateTime build() {
final timer = Timer.periodic(const Duration(seconds: 1), (_) {
state = DateTime.now();
});
ref.onDispose(timer.cancel);
return DateTime.now();
}
}
final clockProvider = NotifierProvider<Clock, DateTime>(Clock.new);
///////////////////////////////////////////////////////////////////////////////
/// Counter (NotifierProvider mit manuellem Hochzählen)
///////////////////////////////////////////////////////////////////////////////
class Counter extends Notifier<int> {
@override
int build() => 0;
void increment() {
state++;
}
}
final counterStateProvider = NotifierProvider<Counter, int>(() => Counter());
///////////////////////////////////////////////////////////////////////////////
/// Fake WeatherRepository (FutureProvider)
///////////////////////////////////////////////////////////////////////////////
class WeatherRepository {
static const _conditions = ['cloudy', 'sunny', 'rainy', 'foggy'];
var _runs = 0;
Future<String> getWeather() async {
_runs++;
if (_runs % 3 == 0) {
throw StateError('server not found.');
} else {
await Future.delayed(const Duration(seconds: 1));
return _conditions[_runs % 4];
}
}
}
final weatherRepositoryProvider = Provider<WeatherRepository>(
(ref) => WeatherRepository(),
);
final weatherFutureProvider = FutureProvider.autoDispose<String>((ref) {
final weatherRepository = ref.watch(weatherRepositoryProvider);
return weatherRepository.getWeather();
}, retry: (retryCount, error) => null); // null to stop retrying
void main() {
runApp(const ProviderScope(child: MaterialApp(home: MainApp())));
}
///////////////////////////////////////////////////////////////////////////////
/// Fake FirebaseAuth (StreamProvider)
///////////////////////////////////////////////////////////////////////////////
class FirebaseAuth {
FirebaseAuth._();
static final FirebaseAuth instance = FirebaseAuth._();
final _authStateController = StreamController<String?>.broadcast();
var _runs = 0;
String? _currentUser;
Stream<String?> authStateChanges() async* {
yield _currentUser;
yield* _authStateController.stream;
}
Future<void> signIn() async {
_runs++;
await Future.delayed(const Duration(seconds: 1));
if (_runs % 3 == 0) {
_authStateController.addError(StateError('SignIn error'));
} else {
_currentUser = 'John Doe';
_authStateController.add(_currentUser);
}
}
Future<void> signOut() async {
await Future.delayed(const Duration(seconds: 1));
_currentUser = null;
_authStateController.add(_currentUser);
}
}
final authStateChangesProvider = StreamProvider.autoDispose<String?>((ref) {
final firebaseAuth = ref.watch(firebaseAuthProvider);
return firebaseAuth.authStateChanges();
});
final firebaseAuthProvider = Provider<FirebaseAuth>((ref) {
return FirebaseAuth.instance;
});
class AuthController extends AsyncNotifier<void> {
@override
FutureOr<void> build() {}
Future<void> signIn() async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref.read(firebaseAuthProvider).signIn(),
);
}
Future<void> signOut() async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref.read(firebaseAuthProvider).signOut(),
);
}
}
final authControllerProvider =
AsyncNotifierProvider.autoDispose<AuthController, void>(AuthController.new);
///////////////////////////////////////////////////////////////////////////////
/// FlexibleCounter (Provider mit Parameter über family-modifier)
///////////////////////////////////////////////////////////////////////////////
class FlexibleCounter extends Notifier<int> {
FlexibleCounter(this._step);
final int _step;
@override
int build() => 0;
void increment() {
state += _step;
}
}
final flexibleCounterStateProvider =
// Letzter in <Type, Type, Type> deklariert Typ vom Parameter (equatable!)
NotifierProvider.family<FlexibleCounter, int, int>(
(step) => FlexibleCounter(step),
);
///////////////////////////////////////////////////////////////////////////////
/// MainApp
///////////////////////////////////////////////////////////////////////////////
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(counterStateProvider, (previous, next) {
final messenger = ScaffoldMessenger.of(context);
messenger.clearSnackBars();
messenger.showSnackBar(
SnackBar(
content: Text('Value is $next'),
duration: const Duration(seconds: 1),
),
);
});
final text = ref.watch(helloWorldProvider);
final currentTime = ref.watch(clockProvider);
final timeFormatted = DateFormat.Hms().format(currentTime);
final counter = ref.watch(counterStateProvider);
final asyncWeather = ref.watch(weatherFutureProvider);
final authStateAsync = ref.watch(authStateChangesProvider);
final authControllerState = ref.watch(authControllerProvider);
final isAuthLoading =
authStateAsync.isLoading || authControllerState.isLoading;
final flexibleCounter1 = ref.watch(flexibleCounterStateProvider(1));
final flexibleCounter2 = ref.watch(flexibleCounterStateProvider(2));
return Scaffold(
body: Center(
child: Column(
children: [
Text(text),
Text(timeFormatted),
ElevatedButton(
child: Text('Value: $counter'),
onPressed: () =>
ref.read(counterStateProvider.notifier).increment(),
),
ElevatedButton(
child: const Text('Reset Counter'),
onPressed: () => ref.invalidate(counterStateProvider),
),
Row(
mainAxisAlignment: .center,
children: [
ElevatedButton(
onPressed: () {
ref.invalidate(weatherFutureProvider);
},
child: const Text('Weather'),
),
switch (asyncWeather) {
AsyncValue(isLoading: true) =>
const CircularProgressIndicator(),
AsyncData(:final value) => Text('Weather: $value'),
AsyncError(:final error) => Text('$error'),
_ =>
const CircularProgressIndicator(), // Der Wildcard-Fallback für den Compiler
},
],
),
ElevatedButton(
onPressed: isAuthLoading
? null
: () {
switch (authStateAsync) {
case AsyncData(value: _?):
ref.read(authControllerProvider.notifier).signOut();
case AsyncValue():
ref.read(authControllerProvider.notifier).signIn();
}
},
child: switch (authStateAsync) {
AsyncData(:final value?) => Text(value),
AsyncError(:final error) => Text('$error'),
AsyncValue() => const Text('login'),
},
),
Row(
mainAxisAlignment: .center,
children: [
ElevatedButton(
onPressed: () => ref
.read(flexibleCounterStateProvider(1).notifier)
.increment(),
child: Text('$flexibleCounter1 (next -> +1)'),
),
ElevatedButton(
onPressed: () => ref
.read(flexibleCounterStateProvider(2).notifier)
.increment(),
child: Text('$flexibleCounter2 (next -> +2)'),
),
],
),
],
),
),
);
}
}- Dieses Beispiel mit Signals umgesetzt: signals_instead_riverpod_providers.dart
Diese Übersicht verdeutlicht das Verhalten von Riverpod-Providern in Abhängigkeit von ihrer Konfiguration (autoDispose, keepAlive und permanent) bei verschiedenen Events und Aktionen.
| Situation / Aktion | 1. Nur autoDispose | 2. autoDispose + keepAlive() | 3. Ohne autoDispose (Permanent) |
|---|---|---|---|
| Letzter Listener weg(z. B. Page-Wechsel) | ⚰️ Disposed (Komplett gelöscht) |
✅ Bleibt am Leben (Zustand wird gecacht) |
✅ Bleibt am Leben (Für immer im RAM) |
| Manuelles ref.invalidate() | ⚰️ Disposed (💤 Wartet auf neuen Listener) |
⚰️ Disposed (💤 Wartet auf neuen Listener) |
🔄 Sofortiger Neustart (Wird sofort neu berechnet) |
| Manuelles ref.refresh() | 🔄 Sofortiger Neustart (Wird sofort neu erzwungen) |
🔄 Sofortiger Neustart (Wird sofort neu erzwungen) |
🔄 Sofortiger Neustart (Wird sofort neu erzwungen) |
| Abhängigkeit ändert sich(ref.watch im Body) | 🔄 Sofortiger Neustart (Wird sofort neu berechnet) |
🔄 Sofortiger Neustart (Wird sofort neu berechnet) |
🔄 Sofortiger Neustart (Wird sofort neu berechnet) |
| Aufruf von link.close() | 🚫 Nicht möglich(Gibt keinen Link) | 🔄 Wechselt zu Verhalten 1: • Mit Listener? -> Bleibt aktiv. • Ohne Listener? -> ⚰️ Sofort gelöscht |
🚫 Nicht möglich(Gibt keinen Link) |
Wenn du ein ref.invalidate() aufrufst, verhalten sich Spalte 2 und Spalte 3 unterschiedlich, falls gerade kein Widget aktiv zuhört (kein aktiver Listener):
- Permanenter Provider (Spalte 3): Startet nach dem Invalidate sofort im Hintergrund eine neue Berechnung (z. B. einen Netzwerk-Request), selbst wenn die Daten aktuell niemand sieht.
-
keepAlive-Provider (Spalte 2): Wirft die alten Daten weg und geht in den Ruhezustand über. Er berechnet sich erst dann neu, wenn wieder ein Widget den Provider aktiv anfordert. Das spart Ressourcen!
Der Aufruf von link.close() löscht den Provider nicht zwangsläufig sofort, sondern hebt lediglich das "Kündigungsschutz-Privileg" auf. Es wird geprüft:
- Sind noch Widgets da, die den Provider via
ref.watchbeobachten? Wenn ja, bleibt er aktiv (rutscht aber auf das Standard-Verhalten von Spalte 1 ab). - Ist der Nutzer bereits auf einer anderen Seite und es gibt keine Listener mehr? Wenn ja, wird der Provider exakt in der Millisekunde des
close()-Aufrufs zerstört.