Al C
Al C

Reputation: 5377

Triggering Widget Rebuilds with Provider's context.read<T>() Method

According to Flutter's documentation and this example, as I'm understanding it, a key difference between the Provider package's context.read<T> and context.watch<T> methods relate to triggering widget rebuilds. You can call context.watch<T>() in a build method of any widget to access current state, and to ask Flutter to rebuild your widget anytime the state changes. You can't use context.watch<T>() outside build methods, because that often leads to subtle bugs. Instead, they say, use context.read<T>(), which gets the current state but doesn't ask Flutter for future rebuilds.

I tried making this simple app:

class MyDataNotifier extends ChangeNotifier {
  String _testString = 'test';

  // getter
  String get testString => _testString;

  // update
  void updateString(String aString) {
    _testString = aString;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => MyDataNotifier(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text(context.read<MyDataNotifier>().testString),
        ),
        body: Container(
          child: Level1(),
        ),
      ),
    );
  }
}

class Level1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          onChanged: (val) {
            context.read<MyDataNotifier>().updateString(val);
          },
        ),
        Text(context.read<MyDataNotifier>().testString),
      ],
    );
  }
}

All the calls are to counter.read<T>(). The app's state changes, but the UI is not rebuilt with the new value. I have to change one of the calls to counter.watch<T>() to get the state to rebuild.

On the other hand, in DZone's simple example, the UI rebuilds, and all the calls are to context.read().

What's different between their code and mine? Why can't I rebuild with counter.read() calls?

Upvotes: 1

Views: 2583

Answers (1)

cameron1024
cameron1024

Reputation: 10136

TLDR: after a quick glance, the DZone article looks like it has a bug.

Longer answer

context.watch<Foo>() does 2 things:

  1. return the instance of the state from the tree
  2. mark context as dependent on Foo

context.read<Foo>() only does 1).

Whenever your UI depends on Foo, you should use context.watch, since this appropriately informs Flutter about that dependency, and it will be rebuilt properly.

In general, it boils down to this rule of thumb:

  • Use context.watch in build() methods, or any other method that returns a Widget
  • Use context.read in onPressed handlers (and other related functions)

The main reason people seem to use context.read inappropriately is for performance reasons. In general, preferring context.read over context.watch for performance is an anti-pattern. Instead, you should use context.select if you want to limit how often a widget rebuilds. This is most useful whenever you have a value that changes often.

Imagine you have the following state:

class FooState extends ChangeNotifier {
  // imagine this us updated very often
  int millisecondsSinceLastTap;

  // updated less often
  bool someOtherProperty = false;
}

If you had a widget that displays someOtherProperty, context.watch could cause many unnecessary rebuilds. Instead, you can use context.select only depend on a processed part of the state:

// read the property, rebuild only when someOtherProperty changes
final property = context.select((FooState foo) => foo.someOtherProperty);  
return Text('someOtherProperty: $property');

Even with a frequently updating value, if the output of the function provided to select doesn't change, the widget won't rebuild:

// even though millisecondsSinceLastTap may be updating often,
// this will only rebuild when millisecondsSinceLastTap > 1000 changes
final value = context.select((FooState state) => state.millisecondsSinceLastTap > 1000);
return Text('${value ? "more" : "less"} than 1 second...');

Upvotes: 5

Related Questions