I read one of Andrea’s tutorials and tried to write a sample app.
Issue: The build doesn’t appear to re-run when the value changes.
The intention is to tap the button and see the Text widget update. The button tap does appear to write shared preferences based on console print. I don’t see the equivelent of a ‘notifyListeners’ in this Riverpod Provider – that’s what I think I am missing.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SettingsRepository {
const SettingsRepository(this.sharedPreferences);
final SharedPreferences sharedPreferences;
bool onboardingComplete() {
return sharedPreferences.getBool('onboardingComplete') ?? false;
}
Future<void> setOnboardingComplete(bool complete) {
return sharedPreferences.setBool('onboardingComplete', complete);
}
}
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError();
});
final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
// watch another provider to obtain a dependency
final sharedPreferences = ref.watch(sharedPreferencesProvider);
// pass it as an argument to the object we need to return
return SettingsRepository(sharedPreferences);
});
/// asynchronous initialization can be performed in the main method
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();
runApp(ProviderScope(
overrides: [
// override the previous value with the new object
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
],
child: const MyApp(),
));
}
class MyApp extends ConsumerStatefulWidget {
const MyApp({super.key});
@override
ConsumerState<MyApp> createState() => _MyAppState();
}
class _MyAppState extends ConsumerState<MyApp> {
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends ConsumerStatefulWidget {
const MyHomePage({super.key});
@override
ConsumerState<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends ConsumerState<MyHomePage> {
@override
Widget build(BuildContext context) {
print('build');
return Scaffold(
body: SafeArea(
child: Column(
children: [
//The objective is to get this Text to reflect the current value
Text(
'Value: ${ref.watch(settingsRepositoryProvider).onboardingComplete()}'),
ElevatedButton(
onPressed: () {
var v =
ref.read(settingsRepositoryProvider).onboardingComplete();
// the console shows this changing on each button press
print('Setting ${!v}');
ref.read(settingsRepositoryProvider).setOnboardingComplete(!v);
},
child: const Text('Toggle'),
)
],
),
),
);
}
}
3
Answers
I sorted this out using StateNotifier. The last tricky part was assigning state a new Object - otherwise ref.watch did not trigger a rebuild. The dsp instance was the one I retrieved with ref.watch, modified, and passed into save.
It seems like the issue might be due to the missing update notification to the UI after the value is changed in the SettingsRepository. In Riverpod, to update the UI when a value changes, you can use StateNotifier or StateNotifierProvider.
This introduces a SettingsNotifier that extends StateNotifier. It manages the state of whether onboarding is complete or not. The settingsProvider uses this notifier to provide the state to the UI, and UI updates automatically when the state changes due to Riverpod’s reactive nature.
The widget doesn’t rebuild which makes sense since it is only watching
settingsRepositoryProvider
which will only react when thesharedPreferencesProvider
changes.Calling
ref.read(settingsRepositoryProvider).setOnboardingComplete
will not trigger a rebuild onsharedPreferencesProvider
and thus will not trigger a rebuild onsettingsRepositoryProvider
.What you need is a
Notifier
to hold the state, theNotifier
is initialised synchronously, thetoggle
method invoke the state setter which will notify listeners to react.