1

I have several accounts persisted in SharedPreferences and want to make the current account (which the user choses in a dialog) accessible across the entire app. When the user changes the current account, the UI should automatically update to show that account. However, the app state systems I have tried do not update my StatefulWidgets when the current user changes.

I have tried SharedPreferences, InheritedWidget, Provider, and ChangeNotifier. I haven't been able to listen to SharedPreferences changes, and the other solutions don't update the UI when the state changes.

// Main.dart

void main() => runApp(
  ChangeNotifierProvider<AppStateManager>.value(
    value: AppStateManager(),
    child: MyApp()
  )
);
class AppStateManager extends ChangeNotifier {
  int _currentStudentIndex;
  int get currentStudentIndex => _currentStudentIndex;

  set currentStudentIndex(int index) {
    _currentStudentIndex = index;
    notifyListeners();
  }
}
// Code that runs when the user selects a new account

onPressed: () {
  Provider.of<AppStateManager>(context).currentStudentIndex = index;
  Navigator.pop(context);
},
// The state for my StatefulWidget 

_TodayState() {
    getCurrentStudent().then((student) => setState(() {
      _currentStudent = student;
    }));
}

Future<Student> getCurrentStudent() async {
  List<String> students = await PreferencesManager.getStudents();

  final AppStateManager stateManager = Provider.of<AppStateManager>(context);

  Map<String, dynamic> currentStudent = jsonDecode(students[stateManager.currentStudentIndex ?? 0]);

  return Student.fromJson(currentStudent);
}
eospi
  • 33
  • 1
  • 4
  • There are a variety of ways to force a full redraw of your entire app if needed:https://stackoverflow.com/questions/43778488/force-flutter-to-redraw-all-widgets. – Eric Seidel Jul 26 '19 at 20:15
  • Would a top-level provider of the student not cause a full rebuild of the entire subtree when the student changes? https://pub.dev/packages/provider – Eric Seidel Jul 26 '19 at 20:16
  • Thanks for the reply! The user selects a new student in a dialog, updating the current student index in the provider. I would think that the widget tree should rebuild, but my breakpoint in the _TodayState initializer doesn't activate. – eospi Jul 27 '19 at 23:03
  • Could it be that the provider value is being updated in a dialog instead of a regular widget? – eospi Jul 29 '19 at 20:35

1 Answers1

2

I tried recreating your code from the provided snippets and could reproduce your problem (here). You are correctly setting the state and calling notifyListeners() but you're not using the state in your build method. You are assuming that _TodayState is constructed every time you see it but that's not true. For example, Flutter keeps it in memory if it's in a stack of activities (e.g. when you push a navigator route on top of it).

In other words, your constructor code (getCurrentStudent().then(...)) isn't executing as often as you think it is.

To ensure that your UI gets updated, put things in the build() method. For example:

Consumer<AppStateManager>(
  builder: (context, manager, child) => FutureBuilder<Student>(
    future: getCurrentStudent(manager.currentStudentIndex),
    builder: (context, snapshot) {
      if (!snapshot.hasData) {
        return const CircularProgressIndicator();
      }
      return Text('Student: ${snapshot.data.name}');
    },
  ),
),

This works. Here's the gist. It does fetch students every time the state changes, which might or might not be what you want.

If you absolutely need to mix setState, futures and InheritedWidgets, you might need to listen to the provided value manually (with something like stateManager.addListener(listener)). But it's better to avoid this, as it can be a source of bugs.

As a general advice with Provider: listen to it (either with Consumer<...> or with Provider.of<...>) in the build method, not anywhere else.

filiph
  • 2,673
  • 2
  • 26
  • 33