I am using StreamBuilder to display a loading progress bar. It is possible on reaching ConnectionState.done - run setState() for updating my widgets that are outside of StreamBuilder?.
If I try to call setState() from a widget, I get an error
setState() or markNeedsBuild() called during build
case ConnectionState.done:
children = <Widget>[
Icon(
Icons.info,
color: Colors.blue,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('\$${snapshot.data} (closed)'),
)
];
//I'm trying to trigger an update
setState((){
isOffsetLoading = true;
});
break;
From thhe docs:
Streambuilder: https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
Widget that builds itself based on the latest snapshot of interaction with a Stream.
setState: https://api.flutter.dev/flutter/widgets/State/setState.html
Calling setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.
The error means that you are calling setState while the widget is building which will cause the widget to rebuild repeatedly.
You can update isOffsetLoading and call setState in a separate function and call the function instead in your code in the question.
Related
I have a working code before null safety flutter upgrade. But after the migration, the same code doesn't work.
I had a simple horizontal swipe card, but now something force the swipe to stay on the first position or rebuild. When I remove didChangeDependencies (function I use to load when data change) the swipe is OK. I think when data is load by didChangeDependenciesit refresh new Swiper.children( and force to return always to first index position.
But I can't do without didChangeDependencies, how can I do ?
Here is the package https://pub.dev/packages/flutter_swiper_null_safety/example
Here is my code:
#override
void didChangeDependencies() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
}
new Swiper.children(
viewportFraction: 0.8,
scale: 0.6,
autoplay: false,
loop: false,
control: new SwiperControl(
size: 25.0,
color: Color(0xffff9a7b),
disableColor: Colors.transparent ,
padding: const EdgeInsets.all (5.0),
),
children: <Widget>[
Card1()
Card2()
Card3()
]
Usually you would need to have a controller or index saved in the state that could hold the state of the downstream widget so on rebuilds the state stayed the same.
After looking at this package it doesn't appear you can pass in an Index or Controller to the widget so it will be rebuilt any time something above it on the stack is rebuilt.
Is it possible to reorganize your page so that the swiper is not under it in the stack?
I have a scrollcontroller which I need to add a listener to do some pagination functions on when the user scrolls down on a listview.
Currently, I create the scrollcontroller and a listener in the initState. I'm doing it there, because the scroll controller is actually a PrimaryScrollController and it needs context
var _scrollController = PrimaryScrollController.of(context);
Now I've run into a problem where when my page gets rebuilt for one reason or another the listview will jump to the top.
From what I understand its because on a rebuild, everything is getting rebuild however the initState isn't being run.
SO my solution is to move the scrollcontroller creation into the build method, which seems to be working just fine. However, the listener does not work, unless I also move it into the build method.
Is this ok? Or am I creating potentially many parallel listeners which can increase each time the page gets rebuilt?
If you are looking to listen to the scroll position and do some operations based on the scroll offset you can try a builder named valueListenableBuilder
ValueListenableBuilder<int>(
builder: (BuildContext context, int value, Widget? child) {
return Row(
children: <Widget>[
Text('$value'),
child!,
],
);
},
valueListenable: scrollController.offset,
child: Container(),
)
When opening the application for the first time images and stream values are not rendering until I tap on the screen or start scrolling. Made sure that the snapshot.data is printing out the correct values before returning the widget in the stream builder.
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/profile_image.png',
height: 36.0,
width: 36.0,
),
SizedBox(width: 10.0),
StreamBuilder<String>(
stream: context.read<UserBloc>().balanceStream,
builder: (context, snapshot) {
print('ConnectionState is ${snapshot.connectionState}');
switch (snapshot.connectionState) {
case ConnectionState.active:
print('Value is ${snapshot.data}');
return NonScalingTextView(
"\$ ${snapshot.data}",
style: TitleTiny,
);
break;
default:
return Container();
}
},
),
],
);
Performing some user interaction or hot reloading or hot restarting the application after first launch, the widgets render as expected.
I had an Authentication Bloc parenting the Material App. Inside I had a User Bloc created on successful Auth. I had the above issues when this was the case. The issue was only happening on the Widgets rendered based on the states emitted by the User Bloc. So rather than returning these widgets on state changes I changed my listener to navigate to these widgets on state changes. Now in order to call on my User Bloc from this navigated route, I had to move the User Bloc on top of the Material App. And doing this solved my issue.
This solved it but I still don't know what caused the former architecture to fail. If anyone can explain me, it'd be great.
My screen has an AnimatedBackground which is continuously keep looping like this
Stack(
children: [
AnimatedContainer(
duration: Duration(seconds: 2),
onEnd: () {
setState(() {
//doing some animation repeatedly
});
},
),
StreamBuilder(
//keeping rebuild because of the above setState
),
],
),
I am so stuck right now rebuild is so often, because of the setState() (the problem)
I want to use StreamBuilder inside this screen because the information is continuously changing
how not to loop StreamBuilder? how to achieve this?
Since setState({}) method updates the whole widget where you call it, you need in some way separate your widgets. The idea is that the widget where you call setState method should be lower in the widget's tree and the widget, which contains StreamBuilder is upper. As a result calling setState method won't trigger widget with StreamBuilder to rerun.
I'd like to use a GestureDetector for it's onTapDown callback but also have a nice InkWell splash effect.
Is it possible to use these two together?
If you want to unconditionally handle the pointer down event with no gesture disambiguation, you can make the InkWell a child of a Listener, and set the onPointerDown handler.
For example:
new Listener(
onPointerDown: (e) { print('onPointerDown'); },
child: new InkWell(
child: new Text('Tap me'),
onTap: () { print('onTap'); }
),
),
It might make sense to add an onTapDown handler to InkWell.
You can pass a HitTestBehavior to GestureDetector to make it "non-blocking" by setting the value to "translucent"
Or you can also trigger it yourself using Material.of(context)
InkWell uses a GestureDetector under it's hood:
https://github.com/flutter/flutter/blob/79b5e5bc8af7d9df3374dfe6141653848d1c03ac/packages/flutter/lib/src/material/ink_well.dart#L560
The reason why a GestureDetector isn't able to work well if the InkWell-Widget is, that it sets the behavior on it's internal GestureDetector to "HitTestBehavior.opaque". This prevents "event bubbling" / event capturing on parent widgets (if my understanding is correct). And because the "behavior" is final, we can't change / fix that by our own.
https://github.com/flutter/flutter/blob/79b5e5bc8af7d9df3374dfe6141653848d1c03ac/packages/flutter/lib/src/material/ink_well.dart#L566
As I mentioned in a comment above, I wrapped the InkWell within a widget (which handles other stuff too) and provided a way to pass a callback into it which is executed on the desired event.
This is my example solution:
import 'package:flutter/material.dart';
class CardPreview extends StatefulWidget {
final dynamic data;
final VoidCallback onTap;
const CardPreview(this.data, this.onTap);
CardPreview._(this.data, this.onTap);
factory CardPreview.fromData(dynamic data, VoidCallback onTap) {
return new CardPreview(data, onTap);
}
CardPreview createState() => new CardPreview(this.data, this.onTap);
}
class CardPreviewState extends State<CardPreview> {
final dynamic data;
final VoidCallback onTap;
CardPreviewState(this.data, this.onTap);
Widget buildCard() {
return Material(
color: Colors.transparent,
child: Ink(
child: InkWell(
child: null,
onTap: () {
if (this.onTap == null) {
return;
}
this.onTap();
},
),
),
);
}
#override
Widget build(BuildContext context) {
return Container(
child: buildCard(),
);
}
}
Building on RĂ©mi's suggestion of adding a HitTestBehavior.translucent behavior to your GestureDetector, this is how I would solve your issue:
Material(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (details) {
//Do something
},
child: InkWell(
onTap: () {},
child: Container(height: 20.0, width: 20.0, color: Colors.red),
),
),
),
I was able to use something similar to essentially add a onLongPressStart and onLongPressEnd to my InkWell.
InkWell has onTapDown callback now
https://api.flutter.dev/flutter/material/InkResponse/onTapDown.html
https://api.flutter.dev/flutter/material/InkWell/InkWell.html
But, GestureDetector still has more functionality than InkWell, e.g. onSecondaryTap. Thus nesting InkWell and GestureDetector is useful today.
HitTestBehavior.opaque has no impact on child-parent relationship
https://github.com/flutter/flutter/issues/18450#issuecomment-397372078
https://github.com/flutter/flutter/issues/74733#issuecomment-767859584
In short, HitTestBehavior.opaque only prevent sibling behind it from receiving events by return true in HitTest so that its direct parent won't pass on the event to its next child(in reversed order) and return true in HitTest.(Thus, sibling behind its ancestors won't receive events as well. Just like that its ancestors' behaviors are overwritten to opaque.) BUT its ancestors always DO receive events!
Remark: If A is inside B(i.e. A is a descendant of B), then B will never be behind A.
Related source:
https://api.flutter.dev/flutter/rendering/RenderBox/hitTest.html
https://api.flutter.dev/flutter/rendering/RenderProxyBoxWithHitTestBehavior/hitTest.html (RenderProxyBoxWithHitTestBehavior is used by _RenderColoredBox, which is the "color" of Container)
https://api.flutter.dev/flutter/rendering/RenderProxyBoxWithHitTestBehavior/hitTestSelf.html
https://api.flutter.dev/flutter/rendering/RenderBoxContainerDefaultsMixin/defaultHitTestChildren.html
https://api.flutter.dev/flutter/rendering/RenderStack/hitTestChildren.html (Use defaultHitTestChildren)
Acknowledgment: heavily refer to https://github.com/flutter/flutter/issues/18450#issuecomment-601865975
For onTap, only the callback of the "winner" can fire
See https://api.flutter.dev/flutter/widgets/GestureDetector-class.html
Setting GestureDetector.behavior to HitTestBehavior.opaque or HitTestBehavior.translucent has no impact on parent-child relationships: both GestureDetectors send a GestureRecognizer into the gesture arena, only one wins.
Some callbacks (e.g. onTapDown) can fire before a recognizer wins the arena, and others (e.g. onTapCancel) fire even when it loses the arena. Therefore, the parent detector in the example above may call some of its callbacks even though it loses in the arena.
This is tricky:
If winner has onTap or onTapDown, then loser's onTap will never fire, and onTapDown won't fire if the tap is too short such that gesture arena sweeping happens before onTapDown firing.
InkWell has onTap and onTapDown
https://github.com/flutter/flutter/blob/468166c713984941c0e5432b7499a1a05a0a0f61/packages/flutter/lib/src/material/ink_well.dart#L1209-L1219
Thus, to achieve best experience, all primary tap callback should be registered on InkWell. Otherwise, if GestureDetector wins, short tap won't trigger splash, else if InkWell wins, short tap won't trigger Gesture's onTapDown.