I know the more common ways to handle this kind of a thing involve using a ChangeNotifierProvider
(CNP), etc., but I want to understand how Provider
actually works.
If I have a StatelessWidget
with a Provider
somewhere in its widget tree descendents, and that StatelessWidget
gets recreated (i.e., its build()
method runs again), I would expect a new Provider
to be created. So, if the Provider
is being created from a value that has been passed to that StatelessWidget
, I would expect a new Provider
to be created with that new value. Then, any downstream widgets that are using Provider.of<T>(context, listen:true)
would update...I would think.
...But that doesn't happen. When that parent StatelessWidget
is rebuilt with a new input value, the Provider
doesn't rebuild itself. Why not?
Again, I'm not saying this would be a great approach, I just want to understand why this is happening like this.
Here is some basic code to show what I mean: Here it is in DartPad.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: CounterUpper(),
),
),
);
}
}
class _CounterUpperState extends State<CounterUpper>
with TickerProviderStateMixin {
Ticker? ticker;
double animationValue = 0;
@override
void initState() {
print('Running _CenterUpperState.initState()');
super.initState();
ticker = Ticker((Duration elapsed) {
if (elapsed.inSeconds - animationValue > 1) {
setState(() {
print(
'Running _CenterUpperState.setState() with animationValue: $animationValue and elapsed.inSeconds: ${elapsed.inSeconds}');
animationValue = elapsed.inSeconds.toDouble();
});
}
if (elapsed.inSeconds > 5) {
ticker?.stop();
}
});
ticker!.start();
}
@override
Widget build(BuildContext context) {
print(
'Running _CenterUpperState.build() with animationValue: $animationValue');
return ProviderHolder(animationValue: animationValue);
}
}
class ProviderHolder extends StatelessWidget {
const ProviderHolder({super.key, required this.animationValue});
final double animationValue;
@override
Widget build(BuildContext context) {
print(
'Running ProviderHolder.build() with animationValue: $animationValue');
return Provider<ValueWrapper>(
create: (_) {
print('Running Provider.create() with animationValue: $animationValue');
return ValueWrapper(animationValue);
},
child: ContentHolder(),
);
}
}
class ContentHolder extends StatelessWidget {
const ContentHolder({super.key});
@override
Widget build(BuildContext context) {
final double providerValue =
Provider.of<ValueWrapper>(context, listen: true).value;
print('Running ContentHolder.build() with providerValue: $providerValue');
return Text(providerValue.toString());
}
}
class ValueWrapper {
const ValueWrapper(this.value);
final double value;
}
class CounterUpper extends StatefulWidget {
const CounterUpper({super.key});
@override
_CounterUpperState createState() => _CounterUpperState();
}
I've tried re-arranging this many different ways (different versions of the parent widget including StatefulWidget
). I understand that the most common way to do this would be to use a Provider
of some object that can itself be manipulated. Something like a basic class that contains an integer as well as some methods to alter the value of that integer. Then, instead of re-creating the parent widget, you would just use those methods to change the value.
But still...why doesn't this work? It is acting like the Provider
is defined as const even though it has been passed a value that is not const.
Provider doesn't call create again on rebuild, it has it's own handling to look at the build context that you give it to find a state object and re-use it. as you had already deduced from your own logging, the create log only gets called once, this is how the create function is handled inside of provider (Even though rebuild runs, provider chooses to ignore your create script as to not recreate objects that already exist, this prevents expensive recreation on a value that already exists).
As with programming that leaves a few options,
Hope this makes sense, as with any package, you have to understand although that some choices may not make sense to you as you'd rather it destroy it's previous state and rebuild from scratch, optimizations may be made to instead assist the general public who already have a provider and want it's own state to remain unchanged. Hence why worded as oncreate to avoid confusion that it is getting rebuilt on every local state change.
Options 3 here is what I would do, something like this (done quickly so could be optimized, however it works as a rudimentary example):
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: CounterUpper(),
),
),
);
}
}
class CounterUpper extends StatefulWidget {
const CounterUpper({super.key});
@override
State<CounterUpper> createState() => _CounterUpperState();
}
class _CounterUpperState extends State<CounterUpper> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<ValueWrapper>(create: (_) => ValueWrapper(), child: const ContentHolder());
}
}
class ContentHolder extends StatelessWidget {
const ContentHolder({super.key});
@override
Widget build(BuildContext context) {
return Text(Provider.of<ValueWrapper>(context, listen: true).animationValue.toString());
}
}
class ValueWrapper with ChangeNotifier {
ValueWrapper() : animationValue = 0 {
ticker = Ticker((Duration elapsed) {
if (elapsed.inSeconds - animationValue > 1) {
updateAnimationValue(elapsed.inSeconds.toDouble());
animationValue = elapsed.inSeconds.toDouble();
}
if (elapsed.inSeconds > 5) {
ticker.stop();
}
});
ticker.start();
}
double animationValue;
late Ticker ticker;
void updateAnimationValue(double value) {
animationValue = value;
notifyListeners();
}
}