I am calling setState in flutter BlocListener. is there any problem doing this?
....
return BlocListener<XBloc, XState>(
listener: (context, state) {
if (state is XLoadedState) {
setState(() {
name = state.name;
});
}....
....
It's not a problem but it's kinda useless and anti pattern. And using setState you are forcing everything to rebuild even if it's not necessary.
You could just wrap the widget that uses name into a BlocBuilder<XBloc,XState>, for example like this:
BlocBuilder<XBloc,XState>(
builder: (context, state){
if (state is XLoadedState){
return Text(state.name);
}else{
//return something for when state.name is null, I guess
}
}
)
You can check more about this here
Related
I am practicing with flick_bloc and I wonder when to use BlocBuilder, when to use BlocListener and when to use BlocConsumer. I asked a few people, they said that BlocBuilder is used the most and I also started and just practiced with it, but it seems that Blocbuilder only changed for the first time, I don't know if it's true. Can you guys give me some comments on these spellings
Bloc Builder
Used for building widgets, For Example: If you want to show a list of employee names on a page, you can return a ListView widget based on the bloc state. Also, if the employee list comes from an API, then you will need different states such as Loading, Success and Failure states. Based on these different states you can return different widgets from BlocBuilder. A CircularProgressIndicator for showing loading state, ListView for showing employee list in the success state and Error text widget for showing error message if the API fails.
BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
)
Bloc Listener
BlocBuilder can only return widgets. If you want to show a snackbar or want to Navigate from one page to another, then you need to use BlocListener for that.
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
}
)
Bloc Consumer
If you have the use of both BlocListener and BlocBuilder, then it is better to use BlocConsumer. It reduces the boilerplate of using BlocListener and BlocBuilder together.
Code Without Bloc Consumer:
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
},
child: BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
),
)
Code using Bloc Consumer:
BlocConsumer<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
},
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
)
BlocBuilder: You can use it to just build out your widgets, but the draw back is that you can't build in Snackbars or Dialogs into the flow, because you must return a widget in blocbuilder and you don't want to return a snackbar or dialog.
BlocListener: This would permit you to use your dialogs and snackbars, but the issue is that it can't let you do anything a blocbuilder would let you do. Which is as you might have guessed, is to return a widget, it's more suited for dismissible UI components like the dialogs and snackbars.
BlocConsumer: This widget helps you combine both a BlocListener and a BlocBuilder, so you can return static components and dismissible UI components.
So if you won't need Snackbars or Dialogs, use a BlocBuilder, If you need Snackbars or Dialogs, use a BlocListener. If you want both of them to work in synergy use a BlocConsumer.
BlocBuilder
This is used when we want to draw a Widget based on what is the current State. In the following example a new “text” gets drawn every time the state changes.
Sample Example
BlocBuilder<OrdersBloc, OrdersState>(
buildWhen: (context, state) {
return state is OrdersState.OrderCompleted
},
builder: (context, state) {
if (state is OrdersState.OrderCompleted) {
return Container(child: Text('Order Completed!'));
} else if (OrdersState.OrderInProgress) {
return Container(child: Text('In Progress'));
} else if (OrdersState.OrderRequested) {
return Container(child: Text('A customer placed an order!'));
} else {
return Container(child: Text('Waiting for an order'));
}
},
);
BlocListener
This is just a listener not a builder (like the above), that means that its job is keep listening for new changes in the state and not to return a widget. You can use listener when you want to show any dialog or any toast, or navigation from one page to another(these are few examples).
Sample Example
BlocListener<OrdersBloc, OrdersState>(
listenWhen: (context, state) {
return state is OrdersState.OrderCompleted;
},
listener: (context, state) {
// Navigate to next screen
Navigator.of(context).pushNamed('OrderCompletedScreen');
},
child: Container(child: Text('Always draw this text!')),
);
BlocConsumer
This is used when we want to draw something based on the current state and execute some actions depending on the new arriving states. This is a mix between “BlocListener” and “BlocBuilder”.
Sample Example
BlocConsumer<OrdersBloc, OrdersState>(
listenWhen: (context, state) {
return state is OrdersState.OrderCompleted ||
state is OrdersState.OrderRefunded;
},
listener: (context, state) {
if (state is OrdersState.OrdersCompleted) {
// Navigate to next screen
Navigator.of(context).pushNamed('OrderCompletedScreen');
} else if (state is OrdersState.OrderRefunded) {
// Report to analytics
Analytics.reportRefunded(state.orderId);
}
},
buildWhen: (context, state) {
return state is OrdersState.OrderCompleted ||
state is OrdersState.OrderInProgress ||
state is OrdersState.OrderRequested;
},
builder: (context, state) {
if (state is OrdersState.OrderCompleted) {
return Container(child: Text('Order Served!'));
} else if (OrdersState.OrderInProgress) {
return Container(child: Text('In Progress'));
} else {
return Container(child: Text('No State'));
}
},
);
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!!!"));
}
},
);
}
How can I update the UI if I need to wait for the FutureBuilder? Do I need to call my future function twice, one for for the builder and one again to change the UI?
FutureBuilder<String>(
future: getUserOrder(4045),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data,style: Theme.of(context).textTheme.headline);
} else if (snapshot.hasError) {
// I need to change the state at this point
return Text("${snapshot.error}",style: Theme.of(context).textTheme.headline);
} else {
return CircularProgressIndicator();
}
}),
Calling setState inside the FutureBuilder throws this error:
setState() or markNeedsBuild() called during build.
I don't need to display a button or any other other to be clicked. I want to perform the action automatically when the date is loaded in the futureBuilder
Since I couldn't call setState inside FutureBuilder the solution was remove it and do something like this:
getBillingInfo() {
Provider.of<MyRents>(context, listen: false)
.getBillingInfo(context)
.then((billingInfo) {
setState(() {
if (billingInfo["companyInfo"] != null &&
billingInfo["taxes"].isNotEmpty) {
_canGenerateInvoices = true;
} else {
_canGenerateInvoices = false;
}
});
});
}
...
void initState() {
super.initState();
getBillingInfo();
}
...
Visibility(
visible: _canGenerateInvoices,
child: MyWidget()
)
Having this, when I perform other actions I can always change the value of _canGenerateInvoices
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.
I have a group profile page, where a user can change the description of a group. He clicks on the description, gets on a new screen and saves it to Firestore. He then get's back via Navigator.pop(context) to the group profile page which lists all elements via FutureBuilder.
First, I had the database request for my FutureBuilder inside the main build method (directly inside future builder 'future: request') which was working but I learnt it is wrong. But now I have to wait for a rebuild to see changes. How do I tell FutureBuilder that there is a data update?
I am loading Firestore data as follows within the group profile page:
Future<DocumentSnapshot> _future;
#override
void initState() {
super.initState();
_getFiretoreData();
}
Future<void> _getFiretoreData() async{
setState(() {
this._future = Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();});
}
The FutureBuilder is inside the main build method and gets the 'already loaded' future like this:
FutureBuilder(future: _future, ...)
Now I would like to tell him: a change happened to _future, please rebuild ;-).
Ok, I managed it like this (which took me only a few lines of code). Leave the code as it is and get a true callback from the navigator to know that there was a change on the second page:
// check if second page callback is true
bool _changed = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProfileUpdate(userId: globals.userId.toString())),
);
// if it's true, reload future data
_changed ? _getFiretoreData() : Container();
On the second page give the save button a Navigator.pop(context, true).
i would advice you not to use future builder in this situation and use future.then() in an async function and after you get your data update the build without using future builder..!
Future getData() async {
//here you can call the function and handle the output(return value) as result
getFiretoreData().then((result) {
// print(result);
setState(() {
//handle your result here.
//update build here.
});
});
}
How about this?
#override
Widget build(BuildContext context) {
if (_future == null) {
// show loading indicator while waiting for data
return Center(child: CircularProgressIndicator());
} else {
return YourWidget();
}
}
You do not need to set any state. You just need to return your collection of users in your GetFirestoreData method.
Future<TypeYouReturning> _getFirestoreData() async{
return Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();
}
Inside your FutureBuilder widget you can set it up something like Theo recommended, I would do something like this
return FutureBuilder(
future: _getFirestoreData(),
builder: (context, AsyncSnapshot<TypeYouReturning> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
if (snapshot.data.length == 0)
return Text("No available data just yet");
return Container();//This should be the desire widget you want the user to see
}
},
);
Why don't you use Stream builder instead of Future builder?
StreamBuilder(stream: _future, ...)
You can change the variable name to _stream for clarity.