skip to Main Content

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


  1. Chosen as BEST ANSWER

    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.

    save(DspPojo dsp) {
        // state = dsp;  // doesn't seem to trigger rebuild
        state =
            DspPojo.fromJson(dsp.toJson()); // create new object to trigger rebuild
        debug('sharedPref: ${json.encode(state)}');
        prefs.setString('dsp', json.encode(state));
      }
    
    

  2. 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.

    Login or Signup to reply.
  3. The widget doesn’t rebuild which makes sense since it is only watching settingsRepositoryProvider which will only react when the sharedPreferencesProvider changes.

    final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
      // Watching sharedPreferencesProvider here will cause settingsRepositoryProvider to react when sharedPreferencesProvider changes
      final sharedPreferences = ref.watch(sharedPreferencesProvider);
      return SettingsRepository(sharedPreferences);
    });
    

    Calling ref.read(settingsRepositoryProvider).setOnboardingComplete will not trigger a rebuild on sharedPreferencesProvider and thus will not trigger a rebuild on settingsRepositoryProvider.

    What you need is a Notifier to hold the state, the Notifier is initialised synchronously, the toggle method invoke the state setter which will notify listeners to react.

    final isOnboardingCompleteProvider =
        NotifierProvider<IsOnboardingCompleteNotifier, bool>(IsOnboardingCompleteNotifier.new);
    
    class IsOnboardingCompleteNotifier extends Notifier<bool> {
      @override
      bool build() {
        final settingsRepository = ref.watch(settingsRepositoryProvider);
        return settingsRepository.onboardingComplete();
      }
    
      void toggle() {
        state = !state; // Invoking the state setter will notify listeners to react
        ref.read(settingsRepositoryProvider).setOnboardingComplete(state);
      }
    }
    
    // Replace your Text widget and ElevatedButton
    Text('Value: ${ref.watch(isOnboardingCompleteProvider)}'),
    ElevatedButton(
      onPressed: ref.read(isOnboardingCompleteProvider.notifier).toggle,
      child: const Text('Toggle'),
    ),
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search