Implement ChangeNotifier vs StateNotifier

Viewed 3929

I'm quite familiar with Provider package and combine it with the ChangeNotifier.

Let's say I have 3 getters and method with a different function :

  1. Toggle Loading
  2. Toggle Image Loading
  3. Toggle ObsecurePassword

Using ChangeNotifer

import 'package:flutter/foundation.dart';

class GlobalChangeNotifier extends ChangeNotifier {
  bool _isLoading = false;
  bool _isImageLoading = false;
  bool _isObsecurePassword = false;

  bool get isLoading => _isLoading;
  bool get isImageLoading => _isImageLoading;
  bool get isObsecurePassword => _isObsecurePassword;

  void setLoading(bool value) {
    _isLoading = value;
    notifyListeners();
  }

  void setImageLoading(bool value) {
    _isImageLoading = value;
    notifyListeners();
  }

  void setObsecurePassword(bool value) {
    _isObsecurePassword = !value;
    notifyListeners();
  }
}

final globalChangeNotifier = GlobalChangeNotifier();

If I'm using ChangeNotifier, I only need to create 1 file and just call a method like globalChangeNotifier.METHOD() or value like globalChangeNotifier.value.

But now, I've learned about Riverpod package, and in the documentation, it's using StateNotifier.

I want to migrate my previous code from ChangeNotifier to StateNotifier. But in my understanding, StateNotifier only can hold 1 type data, so if I want to migrate above code I should create 3 files, let's say:

  1. provider_isloading.dart,
  2. provider_isimageloading.dart and
  3. provider_obsecurepassword.dart.

Using StateNotifier

// provider_isloading.dart
class IsImageLoading extends StateNotifier<bool> {
  IsImageLoading() : super(false);

  void toggleImageLoading(bool value) {
    state = value;
  }
}

final isImageLoadingProvider = StateNotifierProvider((ref) => IsImageLoading());

// provider_isimageloading.dart

class IsLoading extends StateNotifier<bool> {
  IsLoading() : super(false);
  void toggleLoading(bool value) => state = value;
}

final isLoadingProvider = StateNotifierProvider((ref) => IsLoading());

// provider_obsecurepassword.dart
class IsObsecurePassword extends StateNotifier<bool> {
  IsObsecurePassword() : super(false);

  void toggleObsecurePassword(bool value) {
    state = !value;
  }
}

final isObsecurePasswordProvider = StateNotifierProvider((ref) => IsObsecurePassword());

And I also need to create 1 file to export all of those files:

GlobalStateNotifer.dart

export './provider_loading.dart';
export './provider_imageloading.dart';
export './provider_obsecurepassword.dart';

My question is, is it the best practice to make it as I've explained earlier?

My Folder's Structure

My Folder's Structure

3 Answers

When using Riverpod, it makes a great deal of sense to create static providers on the class they are providing. From your example, you could refactor to:

class IsImageLoading extends StateNotifier<bool> {
  IsImageLoading() : super(false);

  static final provider = StateNotifierProvider((ref) => IsImageLoading());

  void toggleImageLoading(bool value) {
    state = value;
  }
}

You should also consider whether or not you need your providers to be available outside of the class you are actually using them. Something tells me you probably won't be using your password provider anywhere but your login page. Consider creating a private provider in that class.

However, if your desire is to keep your current approach, you could create a class, A, that contains the 3 bool values and a class that extends StateNotifier<A>.

For example:

enum LoadingType { A, B, C }

class LoadingToggles {
  bool A, B, C;

  LoadingToggles({this.A = false, this.B = false, this.C = false});

  static final provider = StateNotifierProvider.autoDispose((ref) => LoadingState(LoadingToggles()));
}

class LoadingState extends StateNotifier<LoadingToggles> {
  LoadingState(LoadingToggles state) : super(state ?? LoadingToggles());

  void toggle(LoadingType type) {
    switch (type) {
      case LoadingType.A:
        state.A = !state.A;
        break;
      case LoadingType.B:
        state.B = !state.B;
        break;
      case LoadingType.C:
        state.C = !state.C;
        break;
      default:
        // Handle error state
    }
  }
}

Finally, just want to add that there is likely a better way to handle loading overall. Consider if you can use a FutureProvider/StreamProvider with Riverpod's AsyncValue instead of manually toggling a loading state.

I think you should use immutable classes like following code

@immutable
abstract class GlobalState {
  const GlobalState();
}

class IsImageLoading extends GlobalState {
  const IsImageLoading();
}

class IsLoading extends GlobalState {
  const IsLoading();
}

class IsObsecurePassword extends GlobalState {
  const IsObsecurePassword();
}

and your StateNotifier will be like following

class GlobalStateNotifier extends StateNotifier<GlobalState> {
  GlobalStateNotifier(GlobalState state) : super(state);

  void changeState(GlobalState newState) {
    state = newState;
  }
}

very late response, but I'm search the same things now and I don't find a correct way to use riverpod or StateNotifier, but probably you should use like you use ChangeNotifier, or for flutter_bloc users they should to use like flutter_bloc logic... or both.

For this case and from what I've read, I would use only one dart file to put all that code, like your changenotifier class, and make simple stateproviders for IsImageLoading, isLoading, isObscurePassword

In another MyViewProvider class you can change the state of those statesProvider, or don't create this and change the state directly in the view.


final isImageLoadingProvider = StateProvider((ref) => false);

final isLoadingProvider = StateProvider((ref) => false);

final isObscurePasswordProvider = StateProvider((ref) => false);

final myViewProvider = Provider<MyViewProvider>((ref) {
  return MyViewProvider(ref.read);
});

class MyViewProvider {
  MyViewProvider(this._read);

  final Reader _read;

  void setLoading(bool value) {
    _read(isLoadingProvider).state = value;
  }

  void setImageLoading(bool value) {
    _read(isImageLoadingProvider).state = value;
  }

  void setObscurePassword() {
    final isObscure = _read(isLoadingProvider).state;
    _read(isObscurePasswordProvider).state = !isObscure;
  }
}

I would appreciate any comments or improvements

Related