Why does notifyListeners() not update consumer?

Issue

In my app built with Flutter, I am using the provider package to add state management to my app. Additionally, I am using the shared preferences package to keep track of the login state of my user (token based). The app consumes a Laravel API that makes use of Sanctum.

Everything is working as expected. However, after logging out the user and logging back in with a different user, causes the data of the previous user to be shown. I noticed the token of the old user keeps persisting in the providers, which causes old data to load.

main.dart

Future main() async {
  await dotenv.load(fileName: ".env");
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
        create: (context) => AuthProvider(),
        child: Consumer<AuthProvider>(builder: (context, authProvider, child) {
          return MultiProvider(
              providers: [
                ChangeNotifierProvider<CategoryProvider>(
                    create: (context) => CategoryProvider(authProvider)),
                ChangeNotifierProvider<TransactionProvider>(
                    create: (context) => TransactionProvider(authProvider)),
                ChangeNotifierProvider<ProfileProvider>(
                    create: (context) => ProfileProvider(authProvider))
              ],
              child: MaterialApp(
                title: 'Flutter App',
                routes: {
                  '/': (context) {
                    final authProvider = Provider.of<AuthProvider>(context);
                    return authProvider.isAuthenticated ? Home() : Login();
                  },
                  '/login': (context) => Login(),
                  '/register': (context) => Register(),
                  '/profile': (context) => Profile(),
                  '/categories': (context) => Categories(),
                },
              ));
        }));
  }
}

Given the above example, I was expecting for any change to my AuthProvider, to rebuild the Providers that are listed in the Consumer widget.

auth_provider.dart

class AuthProvider extends ChangeNotifier {
  bool isAuthenticated = false;
  late String token;

  AuthProvider() {
    init();
  }

  Future<void> init() async {
    this.token = await getToken();

    if (this.token.isNotEmpty) {
      this.isAuthenticated = true;
    }

    ApiService apiService = ApiService(this.token);
    notifyListeners();
  }

  ApiService apiService = ApiService('');

  Future<void> register(String name, String email, String password,
      String passwordConfirm, String deviceName) async {
    
    this.token = await apiService.register(name, email, password, passwordConfirm, deviceName);
    setToken(this.token);
    this.isAuthenticated = true;

    notifyListeners();

  }

  Future<void> login(String email, String password, String deviceName) async {
    this.token = await apiService.login(email, password, deviceName);
    setToken(this.token);
    this.isAuthenticated = true;
    notifyListeners();
  }

  Future<void> logout() async {
    this.token = '';
    this.isAuthenticated = false;

    setToken(this.token);

    final prefs = await SharedPreferences.getInstance();
    prefs.clear();

    notifyListeners();
  }

  Future<void> setToken(token) async {
    final prefs = await SharedPreferences.getInstance();
    prefs.setString('token', token);
  }

  Future<String> getToken() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString('token') ?? '';
  }
}

In the logout() function, I am clearing the token.

category_provider.dart

class CategoryProvider extends ChangeNotifier {
  List<Category> categories = [];
  late ApiService apiService;
  late AuthProvider authProvider;

  CategoryProvider(AuthProvider authProvider) {
    this.authProvider = authProvider;
    this.apiService = ApiService(authProvider.token);

    init();
  }

  Future init() async {
    categories = await apiService.fetchCategories();
    notifyListeners();
  }

  Future<void> addCategory(String name) async {
    try {
      Category addedCategory = await apiService.addCategory(name);
      categories.add(addedCategory);

      notifyListeners();
    } catch (Exception) {
      print(Exception);
    }
  }

  // omitted functions
}

The ApiService is a class that receives the passed token and does API calls for the providers.

api.dart

class ApiService {
  late String token;

  ApiService(String token) {
    this.token = token;
  }

  final String baseUrl = dotenv.env['APP_URL'].toString() + '/api/';

  Future<List<Category>> fetchCategories() async {
    http.Response response =
        await http.get(Uri.parse(baseUrl + 'categories'), headers: {
      HttpHeaders.contentTypeHeader: 'application/json',
      HttpHeaders.acceptHeader: 'application/json',
      HttpHeaders.authorizationHeader: 'Bearer $token',
    });
    List categories = jsonDecode(response.body)['data'];

    return categories.map((category) => Category.fromJson(category)).toList();
  }

  // omitted functions
}

Why does the notifiyListeners() in the logout function of auth_provider.dart not trigger the consumers to rebuild? Am I missing something else that might cause this issue?

Update after answer

In the providers array of main.dart, I changed the ChangeNotifierProvider to ChangeNotifierProxyProvider. The difference is that the ChangeNotifierProxyProvider allows for a update() callback, so the provider can get updated if AuthProvider updates.

Code example:

ChangeNotifierProxyProvider<AuthProvider, CategoryProvider>(
    create: (context) => CategoryProvider(authProvider),
    update: (context, authProvider, categoryProvider) => CategoryProvider(authProvider)
),

Solution

The Consumer is being updated. Your Providers aren’t recreating their values.

Provider.create is only called once, the first time that the value is needed. After a user logs out and another user logs in, the same CategoryProvider instance still exists, so as far as Provider knows, there’s no reason to create another one. The ApiService instance stored in CategoryProvider still uses the old token, which causes old data to load.

To update the token, you will need to either update or recreate CategoryProvider with the new token. One option for this is ChangeNotifierProxyProvider, which provides an update callback parameter.

Answered By – Nitrodon

Answer Checked By – Gilberto Lyons (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *