I am using provider and flutter_bluc in my app. Right now I am struggling with this case: A new state is passed to BlocBuilder, which rebuilds part of the Widget Tree UI. What I want is to also update my bottomNavigationVisibility, which extends ChangeNotifier. I use this bottomNavigationVisibility for updating another part of the widget tree.
That way I could easily achieve good separation logic for my UIViews IMHO. However, right now I am always getting this error:
setState() or markNeedsBuild() called during build.
Here are the code samples:
#override
Widget build(BuildContext context) {
final bottomNavigationVisibility =
Provider.of<ProviderBottomNavigation>(context);
...
Container(
height: 60.0,
child: BlocBuilder<SoundsblocBloc, SoundsblocState>(
condition: (previousState, state) {
if (previousState is InitialSoundsblocLoading) return true;
return false;
}, builder: (context, state) {
if (state is InitialSoundsblocLoaded) {
if (state.sounds
.where((sound) => sound.focused)
.toList()
.isEmpty) {
bottomNavigationVisibility.isVisibleT(false);
} else {
bottomNavigationVisibility.isVisibleT(true);
}
}
return ListView.builder(
itemCount: listOfSounds.length,
...
})),
class ProviderBottomNavigation extends ChangeNotifier {
bool _isVisible = false;
bool get isVisible => _isVisible;
void isVisibleT(bool val) {
_isVisible = val;
notifyListeners();
}
}
How should I change my code to update my ChangeNotifier from widget A, which is observed in widget B, after a particular bloc state was returned?
Also, could I be wrong and using the Provider for separate UI parts with flutter_bloc is a bad combo?
Related
I have a reuable stateful widget that returns a button layout. The button text changes to a loading spinner when the network call is in progress and back to text when network request is completed.
I can pass a parameter showSpinner from outside the widget, but that requires to call setState outside of the widget, what leads to rebuilding of other widgets.
So I need to call setState from inside the button widget.
I am also passing a callback as a parameter into the button widget. Is there any way to isolate the spinner change state setting to inside of such a widget, so that it still is reusable?
The simplest and most concise solution does not require an additional library. Just use a ValueNotifier and a ValueListenableBuilder. This will also allow you to make the reusable button widget stateless and only rebuild the button's child (loading indicator/text).
In the buttons' parent instantiate the isLoading ValueNotifier and pass to your button widget's constructor.
final isLoading = ValueNotifier(false);
Then in your button widget, use a ValueListenableBuilder.
// disable the button while waiting for the network request
onPressed: isLoading.value
? null
: () async {
// updating the state is super easy!!
isLoading.value = true;
// TODO: make network request here
isLoading.value = false;
},
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: isLoading,
builder: (context, value, child) {
if (value) {
return CircularProgressIndicator();
} else {
return Text('Load Data');
}
},
);
}
You can use StreamBuilder to solve this problem.
First, we need to create a stream. Create a new file to store it, we'll name it banana_stream.dart, for example ;).
class BananaStream{
final _streamController = StreamController<bool>();
Stream<bool> get stream => _streamController.stream;
void dispose(){
_streamController.close();
}
void add(bool isLoading){
_streamController.sink.add(isLoading);
}
}
To access this, you should use Provider, so add a Provider as parent of the Widget that contain your reusable button.
Provider<BananaStream>(
create: (context) => BananaStream(),
dispose: (context, bloc) => bloc.dispose(),
child: YourWidget(),
),
Then add the StreamBuilder to your button widget:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: Provider.of<BananaStream>(context, listen:false),
initialData: false,
builder: (context, snapshot){
final isLoading = snapshot.data;
if(isLoading == false){
return YourButtonWithNoSpinner();
} else{
return YourButtonWithSpinner();
}
}
);
}
}
And to change isLoading outside, you can use this code:
final provider = Provider.of<BananaStream>(context, listen:false);
provider.add(true); //here is where you change the isLoading value
That's it!
Alternatively, you can use ValueNotifier or ChangeNotifier but i find it hard to implement.
I found the perfect solution for this and it is using the bloc pattern. With this package https://pub.dev/packages/flutter_bloc
The idea is that you create a BLOC or a CUBIT class. Cubit is just a simplified version of BLOC. (BLOC = business logic component).
Then you use the bloc class with BlocBuilder that streams out a Widget depending on what input you pass into it. And that leads to rebuilding only the needed button widget and not the all tree.
simplified examples in the flutter counter app:
// input is done like this
onPressed: () {
context.read<CounterCubit>().decrement();
}
// the widget that builds a widget depending on input
_counterTextBuilder() {
return BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) {
if (state.counterValue < 0){
return Text("negative value!",);
} else if (state.counterValue < 5){
return Text("OK: ${state.counterValue}",
);
} else {
return ElevatedButton(onPressed: (){}, child: const Text("RESET NOW!!!"));
}
},
);
}
The problem i am facing is,
I am connecting my ui to backend with websocket using subscribe method(graphql client). That means there is a connection between my ui and backend. I am storing the data i got from backend in the local storage
From the local storage, i am getting that data,
Whenever i receive the data from backend it should be reflected in the ui automatically. For reflecting change in ui, i am using state management provider package.What should i do to make my widget rebuild on listening to the changes i had made using provider package;
class MyNotifier extends ChangeNotifier {
bool _listenableValue = false;
bool get listenableValue => _listenableValue
MyNotifier.instance();
void setValue(){
_listenableValue = !_listenableValue;
notifyListeners();
}
}
...
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MyNotifier>(
create: (context) => MyNotifier.instance(),
builder: (context, child){
return Column(
children: [
StaticWidget(),
//This text widget will rebuild when value is changed
Selector<MyNotifier, bool>(
selector: (_, notifier) => notifier.listenableValue,
builder: (_, value, __) => Text('$value');
),
//Change value with button
IconButton(
icon: //icon
onPressed: (){
context.read<MyNotifier>().setValue();
},
);
]
);
}
);
}
Don' t use Consumer. Consumer will rebuild all widgets when a data changed. This is the bad situation for performance.
Selector is the best in my opinion.
If you have your state in a ChangeNotifier like:
class MyState extends ChangeNotifier{
addStorage(Map<String, String> data) {...}
getAllDataFromStorage() {...}
}
Then you can make your UI rebuild by just wrapping the desired widgets in a Consumer.
Consumer<MyState>(builder: (context, state) {
return Container(
padding: EdgeInsets.only(top: 10),
child: LayoutBuilder(builder: (context, constraints) {
if (screenLayout >= 1024) {
return desktopWidget(context, visitsList);
} else if (screenLayout >= 768 && screenLayout <= 1023) {
return tabletWidget(context, visitsList);
} else {
return mobileWidget(context, visitingsList, screenLayout);
}
})},
);
Note that somewhere above this snippet you should have a ChangeNotifierProvider injecting your MyState in the widget tree.
For a really thorough and complete guide take a look at Simple state management
**best way to listen changes in provider its to make getter and setter i will show you example below**
class ContactProvider with ChangeNotifier {
bool isBusy = true;
bool get IsBusy => isBusy;
set IsBusy(bool data) {
this.isBusy = data;
notifyListeners();
}
void maketru(){
IsBusy=false;
}
}
and now you can use this context.read<ContactProvider>().Isbusy;
In short, the question is: How do I reuse the state of an entire widget subtree?
This is what my code currently looks like:
...
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is Authenticating) {
return AppLoading();
} else if (state is NotAuthenticated) {
return AppOnboarding();
} else if (state is Authenticated) {
return AppMain();
} else {
return null;
}
}
),
...
Nothing fancy here, just a BlocBuilder rebuilding its child whenever the state of the underlying Bloc changes.
Now, take for instance the following state transitions: NotAuthenticated => Authenticating => NotAuthenticated because there was something wrong with the inputed information. This would result in the AppOnboarding() widget being rebuild completely from scratch with all the information lost. How can I reuse the state from the old AppOnboarding() widget to make it look like the widget was never rebuild?
What I've already tried
I've already tried using a GlobalKey for this which I would pass to the key property of my AppOnboarding widget. This is my code:
_AuthenticatingState extends State<Authenticating> {
GlobalKey<AppOnboardingState> _appOnboardingKey;
#override
initState() {
super.initState();
_appOnboardingKey = GlobalKey();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<...>(
builder: (context, state) {
...
if (state is NotAuthenticated) {
return AppOnboarding(key: _appOnboardingKey);
}
...
}
),
}
}
I was a little surprised this didn't work. Do global keys not maintain the state of the entrie widget-subtree?
Flutter runs at 60 frames per second. The key tells Flutter that some widget is the same one that existed in the last frame. If you remove some widget, even for a single frame, it will dispose the widget (call the dispose method and get rid of the widget).
In other words, the key doesn't "save" any state for later.
You have two options:
Don't dispose the widget at all.
builder: (context, state) {
return Stack(children: [
Offstage(child:AppLoading(), offstage: state is! Authenticating),
Offstage(child:AppOnboarding()), offstage: state is! NotAuthenticated),
Offstage(child:AppMain()), offstage: state is! Authenticated),
]) }
}
Save yourself that widget state, so that you can rebuild the widget later with that same information.
So I have a table in moor which is returning a Future<List<..>> with that data I am trying to make pie chart
class Stats extends StatefulWidget {
#override
_StatsState createState() => _StatsState();
}
class _StatsState extends State<Stats> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildTaskList(context)
],
);
}
}
FutureBuilder<List<Account>> _buildTaskList(BuildContext context) {
Map<String, double> dataMap = Map();
dataMap.putIfAbsent("SNACKS", () => Provider.of<Amount>(context,listen: false).snack_amount);
dataMap.putIfAbsent("ENTERTAINMENT", () => Provider.of<Amount>(context,listen: false).entertainment_amount);
dataMap.putIfAbsent("STATIONARY", () => Provider.of<Amount>(context,listen: false).stationary_amount);
dataMap.putIfAbsent("OTHERS", () => Provider.of<Amount>(context,listen: false).others_amount);
final dao = Provider.of<AccountDao>(context);
return FutureBuilder(
future: dao.getAllAccounts(),
builder: (context, AsyncSnapshot<List<Account>> snapshot) {
final accounts = snapshot.data ?? List();
Provider.of<Amount>(context,listen: false).add(Calculate(accounts, Type.SNACKS), Calculate(accounts, Type.ENTERTAINMENT),
Calculate(accounts, Type.STATIONARY), Calculate(accounts, Type.OTHERS));
if (accounts == null) {
return Text('No Accounts Yet');
} else {
return PieChart(
dataMap: dataMap,
);
}
},
);
}
double Calculate(List<Account> list, Type type){
double sum=0;
for(int i=0;i<list.length;i++){
if(list[i].type==type){
sum=sum+list[i].amount;
}
}
return sum;
}
Now everytime I am running this I am getting an error
**The following assertion was thrown while dispatching notifications for Amount:
setState() or markNeedsBuild() called during build.
This _InheritedProviderScope widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: _InheritedProviderScope
value: Instance of 'Amount'
listening to value
The widget which was currently being built when the offending call was made was: FutureBuilder>
dirty
state: _FutureBuilderState>#37282
When the exception was thrown, this was the stack:
0 Element.markNeedsBuild. (package:flutter/src/widgets/framework.dart:4167:11)
1 Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:4182:6)
2 _InheritedProviderScopeElement.markNeedsNotifyDependents (package:provider/src/inherited_provider.dart:426:5)
3 ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:207:21)
4 Amount.add (package:moneymanager/main.dart:55:5)
...
The Amount sending notification was: Instance of 'Amount'
**
This is the Amount Class
class Amount extends ChangeNotifier {
double snack_amount=0.0;
double entertainment_amount=0.0;
double stationary_amount=0.0;
double others_amount=0.0;
void add(double snack,double entertainment,double stationary, double others){
snack_amount=snack;
entertainment_amount=entertainment;
stationary_amount=stationary;
others_amount=others;
notifyListeners();
}
}
The function to get the Future<List<..>>
```Future> getAllAccounts()=>select(accounts).get();``
EDIT
I edited my answer as suggested
return FutureBuilder(
future: dao.getAllAccounts(),
builder: (context, AsyncSnapshot<List<Account>> snapshot) {
if(snapshot.connectionState==ConnectionState.done){
final accounts = snapshot.data ?? List();
if (accounts == null) {
return Text('No Accounts Yet');
} else {
Provider.of<Amount>(context,listen: false).add(Calculate(accounts, Type.SNACKS), Calculate(accounts, Type.ENTERTAINMENT),
Calculate(accounts, Type.STATIONARY), Calculate(accounts, Type.OTHERS));
dataMap.putIfAbsent("SNACKS", () => Provider.of<Amount>(context,listen: false).snack_amount);
dataMap.putIfAbsent("ENTERTAINMENT", () => Provider.of<Amount>(context,listen: false).entertainment_amount);
dataMap.putIfAbsent("STATIONARY", () => Provider.of<Amount>(context,listen: false).stationary_amount);
dataMap.putIfAbsent("OTHERS", () => Provider.of<Amount>(context,listen: false).others_amount);
return PieChart(
dataMap: dataMap,
);
}
}else if(snapshot.connectionState==ConnectionState.waiting){
return Container();
}else{
return Container();
}
},
but still the same error
The following assertion was thrown while dispatching notifications for Amount: setState() or markNeedsBuild() called during build.
This error means that you are calling setState during the build phase
And from you logs it explicitly states
A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase
you can implement ConnectionState.waiting ConnectionState.done
So I am implementing something like below:
class TempProvider extends ChangeNotifier(){
List<Widget> _list = <Widget>[];
List<Widget get list => _list;
int _count = 0;
int get count => _count;
Future<List<Widget>> getList() async{
addToList(Text('$count'));
List _result = await db....
_result.forEach((_item){
addToList(Button(
onTap: () => increment();
child: Text('Press'),
));
});
}
addToList(Widget widget){
_list.add(widget);
notifyListeners();
}
increment(){
_count += 1;
notifyListeners();
}
}
class Parent extends StatelessWidget{
#override
Widget build(BuildContext context) {
return FutureProvider(
create: (context) => TempProvider().getList(),
child: Child(),
);
}
}
class Child extends StatelessWidget{
#override
Widget build(BuildContext context) {
var futureProvider = Provider.of<List<Widget>>(context);
return Container(
child: futureProvider == null
? Text('Loading...'
: ListView.builder(
itemCount: futureProvider.length,
itemBuilder: (BuildContext context, int index){
return futureProvider[index];
}
),
));
}
}
Basically, what this does is that a List of Widgets from a Future is the content of ListView Builder that I have as its objects are generated from a database query. Those widgets are buttons that when pressed should update the "Count" value and should update the Text Widget displaying the latest "Count" value.
I was able to test the buttons and they seem to work and are incrementing the _count value via backend, however, the displayed "Count" on the Text Widget is not updating even if the Provider values are updated.
I'd like to ask for your help for what's wrong here, with my understanding, things should just update whenever the value changes, is this a Provider anti-pattern, do I have to rebuild the entire ListView, or I missed something else?
I'm still getting myself acquainted with this package and dart/flutter in general, hoping you can share me your expertise on this. Thank you very much in advance.
so I have been on a lot of research and a lot of trial and errors last night and this morning, and I just accidentally bumped into an idea that worked!
You just have to have put the listening value on a consumer widget making sure it listens to the nearest Provider that we have already implemented higher in the widget tree. (Considering that I have already finished drawing my ListView builder below the FutureProvider Widget)
..getList() async{
Consumer<ChallengeViewProvider>(
builder: (_, foo, __) => Text(
'${foo.count}',
),
);
List _result = await db....
_result.forEach((_item){
addToList(Button(
onTap: () => increment();
child: Text('Press'),
));
});
}
I have also refactored my widgets and pulled out the Button as a stateless widget for reuse. Though make sure that referenced Buttons are subscribed to the same parent provider having the Counter value and have the onTap property call out the increment() function through Provider<>.of
Hoping this will help anyone in the future!