Flutter - StreamBuilder builder function runs when navigator pops - flutter

I have a widget called RootContainer which receives a Widget child and wrap it inside a StreamBuilder:
class RootContainer extends StatelessWidget {
final Widget child;
RootContainer({this.child});
#override
Widget build(BuildContext context) {
return StreamBuilder<OverlayAlert>(
stream: ApiService.ThrottledException.stream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
Future.delayed(Duration.zero, () => showAlert(context, snapshot));
}
return this.child;
},
);
}
void showAlert(BuildContext context, snapshot) {
print("showing dialog");
showDialog(
context: context,
builder: (context) => OverlayAlertDialog(snapshot),
);
}
When an error occurs, I add a new value to the stream ApiService.exception.stream which triggers the StreamBuilder builder and then it opens a dialog.
This is the current widget tree:
The problem starts when I want to pop the navigator, the StreamBuilder.builder builds again!
I thought it may happen because the RootContainer is being rebuilt, but placing a print before the StreamBuilder has resulted in just one print.
I tried to .listen to the stream, and the stream didn't fire when I popped the navigator so I can confirm that there's nothing wrong with ApiService.ThrottledException.stream.
The snapshot when the navigator is popped is equal (the data) to the last emission.
You can see in the following demo that whenever I press the back button the dialog pops up again:
What could cause the StreamBuilder to rebuild itself when I press on the back button?

I had to change RootContainer to extend StatefulWidget instead of StatelessWidget.
I have no idea what's happening behind the scene, but it works! Any explanation would be nice.

Related

Flutter Shows SnackBar over bottomsheet and below it

Hello im struggling to get the new way of showing a Snackbar to display over a bottomsheet.
With the below code the Snackbar shows over the top of the bottomsheet but also under the bottom sheet.
Maybe this is expected behaviour?
When I dismiss the bottom sheet, the snackbar disappears with it, but reveals another snackbar under it
My code:
onTap: () {
showTaskInfo(context, taskDetails.taskID);
},
//show bottom sheet - (parent is Scaffold)
Future<dynamic> showTaskInfo(BuildContext context, int taskID) {
final GlobalKey<ScaffoldState> modelScaffoldKey = GlobalKey<ScaffoldState>();
return showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return Scaffold(key: modelScaffoldKey, body: TaskInfo(taskID, modelScaffoldKey));
});
}
//TaskInfo is a RiverPod state Widget
class TaskInfoState extends ConsumerState<TaskInfo> {
#override
Widget build(BuildContext context) {
showSnackBar(context, widget.modelScaffoldKey)
}
}
void showSnackBar(BuildContext context, GlobalKey<ScaffoldState> modelScaffoldKey) {
//THIS WORKS! But is depreciated
//modelScaffoldKey.currentState!.showSnackBar(snackBarDetails);
//This shows a snackbar on the app main scaffold and the scaffold for bottomsheet scaffold
ScaffoldMessenger.of(context).showSnackBar(snackBarDetails);
}
As the comments suggest
Using a key still works as expected.
But using scaffoldMessenger with context shows the snack bar below the bottomsheet and on top
I think this is a context issue, Pretty sure I should be using the context from the scaffold.. which I think I am??

Flutter - how to correctly create a button widget that has a spinner inside of it to isolate the setState call?

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!!!"));
}
},
);
}

Flutter How to pass single data from Stream to another Screen and also got updated

I was making an app with a streambuilder that listen to firestore snapshot, and creating a list of item from it, and when i click one of item it would navigate me to new screen with the detail of the item and i could also modify some info of the item there, the problem arise when i tried to update the info of item in firestore, the first screen with streambuilder got rebuild with updated data while the detail page didnt get updated, the flow is more or less like this :
and here is my simplified code:
First Screen where i navigate and pass the asyncsnapshot and index to detail screen
class _SalesOrderPenyediaState extends State<SalesOrderPenyedia> {
Widget itemTile(context, SalesOrder element, AsyncSnapshot orderSnap, int index) {
//NAVIGATE TO DETAIL SCREEN
GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => StreamBuilder<List<SalesOrder>>(
stream: orderStreams,
builder: (context, snapshot) {
return SalesOrderDetailPenyedia(
streamOrder: orderSnap,
index: index,
);
}
),
fullscreenDialog: true,
);}
child : Container()}
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream: orderStreams,
builder: (context, AsyncSnapshot<List<SalesOrder>> snapshot) {
ListView(
children: snapshot.data
.asMap()
.map((index, item) => MapEntry(
index, itemTile(context, item, snapshot, index)))
.values
.toList())
}
}),
),
],
),
Detail Page
class SalesOrderDetailPenyedia extends StatefulWidget {
AsyncSnapshot<List<SalesOrder>> streamOrder;
int index;
SalesOrderDetailPenyedia({ this.index, this.streamOrder});
#override
State<StatefulWidget> createState() => SalesOrderDetailState();
}
class SalesOrderDetailState extends State<SalesOrderDetailPenyedia>{
SalesOrder salesOrder;
OrderServices orderServices = OrderServices();
#override
Widget build(BuildContext context) {
salesOrder = widget.streamOrder.data[widget.index];
return Scaffold(
body : Column()//where i show and update my data
)
}
I am new to flutter and been stuck to this for a few day, any advice will be really appreciated
I don't see exactly the problem but passing down the asyncSnapshot to the details page is not very useful as it may lose the reference to the first streamBuilder builder function it came from.
So the best way would be to create a stream (best it would be a BehaviorSubject from the rxDart lib) and then add to it with the data you want to pass and then pass that stream to the Details page.
Once the update finishes, the firebaseResponse should either return the new data or u manually refresh it and at that time pass it down to the Details widget via the same stream.
i can see a point in trying to pass down the same stream to the details page and save on request times (if u have the data necessary to show the items list already) but it is very messy in my opinion, u probably should take another approach, the one i mentioned or an other much simpler one.

Flutter exception `setState() or markNeedsBuild() called during build` by calling setState in a NotificationListener callback

I'm having an hard time trying to figure out how to update a piece of a view based on a child view "event"... let me explain:
I have a screen which is composed by a Scaffold, having as a body a custom widget which calls a rest api to get the data to display (it makes use of a FutureBuilder), then it dispatch a Notification (which basically wraps the Flutter's AsynchSnapshot) that should be used in order to update the floatingActionButton (at least in my mind :P).
This is the build method of the screen:
#override
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: onMyNotification,
child: Scaffold(
appBar: MyAppBar(),
body: MyRemoteObjectView(),
floatingActionButton: MyFloatingButton(),
),
);
}
The view is rendered perfectly, the data is retrieved from the server and displayed by MyRemoteObjectView and the notification is successfully dispatched and received, BUT as soon as I call setState() in my callback, I get the exception:
setState() or markNeedsBuild() called during build.
This is the callback (defined in the same class of the build method above):
bool onMyNotification(MyNotification notification) {
AsyncSnapshot snapshot = notification.snapshot;
if (snapshot.connectionState == ConnectionState.done) {
setState(() {
// these flags are used to customize the appearance and behavior of the floating button
_serverHasBeenCalled = true;
_modelDataRetrieved = snapshot.hasData;
});
}
return true;
}
This is the point in which I send the notification (build method of MyRemoteObjectView's state):
#override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: getData(),
builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
MyNotification(snapshot).dispatch(context);
// ...
The point is: how and when should I tell Flutter to redraw the floating button (and/or other widgets)? (because of course without the setState I don't have the exception but the button is not refreshed)
Am I getting the whole thing wrong? Is there an easier way to do it? Let me know
After FutureBuilder is built, it waits for future to return a value. After it is complete, you're calling setState and then FutureBuilder would be built again and so on, resulting in infinite repaint loop.
Are you sure that you need FutureBuilder and NotificationListener in this case? You should probably do it in initState of your StatefulWidget like this:
#override
void initState() {
super.initState();
getData().then((data) {
setState(() {
_serverHasBeenCalled = true;
_modelDataRetrieved = true;
});
});
}
You can also store Future in a state and pass it to FutureBuilder.

How to wait for an alert to pop up before proceeding without making it necessary for the user to tap to proceed?

I'm a beginner to Flutter, and in my current (first) project I have an alert that I want to pop up before the next bit of code runs.
If I use await I have found that it then requires the user to tap on the screen (outside the bounds of the alert) in order for the program to continue. Is there any way to just make sure the alert is present before proceeding without requiring the tap?
Here is my alert method:
myAlert(BuildContext context) {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return new AlertDialog(
title: Text('Some text'),
);
},
);
}
I call the method and then in the next line call another method which does some intensive computations:
myAlert(context);
compute();
Both methods are called on the press of a button (I don't know if this would be relevant in any way, so just putting it out there).
I want compute() to be called only after the alert appears. As of right now, compute() begins running before the alert pops up.
Thanks!
This is a bit confusing, but if you want compute to run when creating the dialog, run it inside the builder (a better option might be to create a separate widget and do it in his initState).
myAlert(BuildContext context) {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return new MyAlertDialog();
},
);
}
my_alert_dialog.dart
class MyAlertDialog extends StatefulWidget {
#override
_MyAlertDialogState createState() => _MyAlertDialogState();
}
class _MyAlertDialogState extends State<MyAlertDialog> {
#override
void initState() {
compute()//HERE u can du your compute
super.initState();
}
#override
Widget build(BuildContext context) {
return //Build here your Dialog Content...
}
}
If you wanna wait for the dialog close and then execute your compute, use await.
myAlert(BuildContext context) async{
return await showDialog<void>(
context: context,
builder: (BuildContext context) {
return new AlertDialog(
title: Text('Some text'),
);
},
);
}