flutterriverpodflutter-riverpodfreezedflutter-freezed

Riverpod trigger rebuild when update state with freezed copywith even if nothing changed


I thought Riverpod will only trigger rebuild if the state value is different but turn out it rebuild every time when state is set although the value is the same. Is that true?

The case is as below

@Freezed(genericArgumentFactories: true)
class Model with _$Model {
  const factory Model({required int id}) = _Model;
}

class Manager {
  static StateProvider<Model> modelProvider =
      StateProvider<Model>((ref) => Model(id: 1));
  Manager() {
    Stream.periodic(Duration(seconds: 1)).take(1000).listen((event) {
      ref.read(modelProvider.notifier).update((state) {
        var cloneState = state.copyWith();
        print("${state == cloneState}"); //This print true
        return cloneState;
      });
    });
  }
}

class TestWidget extends ConsumerWidget {
  const TestWidget();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    var model = ref.watch(Manager.modelProvider);
    print("model change......................"); //print every second
    return Text(model.id.toString());
  }
}

It showed that the TestWidget was rebuilt every seconds but I thought it shouldn't as the state is the same although I set it again. Am I missing something? Thanks.


Solution

  • It's all about using identical(old, current) under the hood to compare states. identical presents for itself the following:

    /// Check whether two references are to the same object.
    ///
    /// Example:
    /// ```dart
    /// var o = new Object();
    /// var isIdentical = identical(o, new Object()); // false, different objects.
    /// isIdentical = identical(o, o); // true, same object
    /// isIdentical = identical(const Object(), const Object()); // true, const canonicalizes
    /// isIdentical = identical([1], [1]); // false
    /// isIdentical = identical(const [1], const [1]); // true
    /// isIdentical = identical(const [1], const [2]); // false
    /// isIdentical = identical(2, 1 + 1); // true, integers canonicalizes
    /// ```
    external bool identical(Object? a, Object? b);
    

    Here is a complete copy-run example:

    void main() => runApp(const ProviderScope(child: MyApp()));
    
    @Freezed(genericArgumentFactories: true)
    class Model with _$Model {
      const factory Model({required int id}) = _Model;
    }
    
    class Manager {
      static StateProvider<Model> modelProvider = StateProvider<Model>((ref) {
        Stream.periodic(const Duration(seconds: 1)).take(1000).listen((event) {
          ref.read(modelProvider.notifier).update((state) {
            final cloneState = state.copyWith();
            // const cloneState = Model(id: 1); //The print true in both cases
            print("${state == cloneState}"); //This print true
            print("identical: ${identical(state, cloneState)}"); //This print false
            return cloneState;
          });
        });
    
        return const Model(id: 1);
      });
    }
    
    class MyApp extends ConsumerWidget {
      const MyApp();
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        var model = ref.watch(Manager.modelProvider);
        print("model change......................"); //print every second
        return MaterialApp(home: Text(model.id.toString()));
      }
    }
    

    I modified the example a little, but kept the essence the same. Only by applying `const' can we achieve the absence of rebuilds.