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

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

Related

setState vs StreamProvider

I'm a Flutter newbie and I have a basic understanding question:
I built a google_maps widget where I get the location of the user and also would like to show the current location in a TextWidget.
So I'm calling a function in initState querying the stream of the geolocator package:
class _SimpleMapState extends State<SimpleMap> {
Position userPosStreamOutput;
void initPos() async {
userPosStream = geoService.getStreamLocation();
userPosStream.listen((event) {
print('event:$event');
setState(() {
userPosStreamOutput = event;
});
});
setState(() {});
}
#override
void initState() {
initPos();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold( //(very simplified)
body: Text(userPosStreamOutput.toString()),
This works just fine. But would it make sense to use a Streamprovider instead? Why?
Thanks
Joerg
An advantage to use StreamProvider is to optimize the process of re-rendering. As setState is called to update userPosStreamOutput value, the build method will be called each time your stream yields an event.
With a StreamProvider, you can apply the same logic of initializing a stream but then, you will use a Consumer which will listen to new events incoming and provide updated data to children widgets without triggering other build method calls.
Using this approach, the build method will be called once and it also makes code more readable in my opinion.
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamProvider(
create: (_) => geoService.getStreamLocation(),
initialData: null,
child: Consumer<Position>(
builder: (context, userPosStreamOutput, _) {
return Text(userPosStreamOutput.toString());
},
),
),
);
}

Flutter reuse state from StatefulWidget on rebuild

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.

How to create and update the value of Dynamic Widgets through Flutter Provider

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!

Provider and flutter_bloc setState() or markNeedsBuild() called during build

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?

How to run a function inside a child stateful widget from parent widget?

I am trying to run a function(with arguments) inside two-levels down StateFul widget, by clicking a button in the parent of the parent of that child(after having all widgets built, so not inside the constructor). just like in the image below:
More details is that I created a Carousal which has Cards inside, published here.
I created it with StreamBuilder in mind(this was the only use case scenario that I used it for so far), so once the stream send an update, the builder re-create the whole Carousal, so I can pass the SELECTED_CARD_ID to it.
But now I need to trigger the selection of the carousal's Cards programmatically, or in another word no need for two construction based on the snapshot's data like this:
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return SelectableCarousal(
selectedId: snapshot.data.toInt(),
onTap: //Update the stream
//some props...,
);
} else {
return SelectableCarousalLoading(
selectedId: null,
onTap: //Update the stream
//some props...,
);
}
},
);
But instead, I'm trying to have something like this so I can use it for others use cases:
Widget myCarousal = SelectableCarousal(
selectedId: null,
onTap: //Update the stream
//some props...,
);
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
// Then when data ready I can update
// the selection by calling two-level down function
if (snapshot.hasData) {
myCarousal.selectById(3);
}
// Build same carousal in all cases.
return myCarousal;
},
);
so this led me to my original question "How to run a function(with arguments) inside two-levels down StateFul widget?".
I appreciate any help. thanks a lot.
I was able to solve that challenge using the BLoC & Stream & StreamSubscription, see the image below:
Inside the Homepage screen:
///...Inside Homepage screen level-0
RaisedButton(
child: Text('Update value in the BLoC'),
onPressed: () {
bloc.changeSelectedState(isSel);
},
),
//...
inside the BLoC:
class Bloc {
final BehaviorSubject<bool> _isSelectedStreamController = BehaviorSubject<bool>();
// Retrieve data from stream
Stream<bool> get isSelectedStream => _isSelectedStreamController.stream;
// Add data to stream
Function(bool) get changeSelectedState => _isSelectedStreamController.sink.add;
void dispose() {
_isSelectedStreamController.close();
}
}
final bloc = Bloc();
Inside any widget in any level as long as it can reach the bloc:
// This inside the two-levels down stateful widget..
StreamSubscription isSelectedSubscription;
Stream isSelectedStream = bloc.isSelectedStream;
isSelectedSubscription = isSelectedStream.listen((value) {
// Set flag then setState so can show the border.
setState(() {
isSelected = value;
});
});
//...other code
#override
Widget build(BuildContext context) {
return Container(
decoration: isSelected
? BoxDecoration(
color: Colors.deepOrangeAccent,
border: Border.all(
width: 2,
color: Colors.amber,
),
)
: null,
//...other code
);
}
so the new design of my widget includes the BLoC as a main part of it, see the image:
and...works like a charm with flexible and clean code and architecture ^^