Same BLoC event not triggering more than one time - flutter

I've HomeBloc from HomeView class. Basic UI architecture is as following:
HomeView extends StatelessWidget {
BlocProvider( child: HomeBody(), bloc: new HomeBloc() )
}
HomeBody extends StatefulWidget {
createState => HomeBodyState()
}
HomeBodyState extends State {
BlocBuilder(
bloc: BlocProvider.of<HomeBloc>()
child: Container(
child: Column(
children: [
BlocProvider( child: CashFlowView, bloc: new HomeBloc() )
]
)
)
)
}
CashFlowView extends StatefulWidget {
createState => CashFlowState()
}
CashFlowState extends State {
BlocBuilder(
bloc: BlocProvider.of<HomeBloc>()
child: Container(
child: Column(
children: [
ChipGroupWidget(
onClick => BlocProvider.of().add(event) // <----- Problem is here
)
]
)
)
)
}
Whole code can be found in this repository.
Problem is when any Chip inside my ChipGroup is tapped, a callback function is called in CashFlowState. Inside that, a bloc event is added to bloc with some data. But it is triggering only for first time. What's wrong in my code?

From the bloc library documentation
Extend your state class with equatable and after each event emit a new state (same type) with the new properyties (data)

In flutter bloc pattern system, the UI is rebuilt only when the state changes. If in any case when a fired event triggers the same state currently the bloc is in, the build function won't be called again, i.e, the UI won't be rebuilt.
In your case, when for the first time the CashFlowState is yielded, the whole code works just fine. But then the same state is yielded again, the event is triggered, but the build function isn't being called again, because the state never changed.
What you have to do is, create two different states along with two different events. Lets say, chipTappedEvent will yield chipTappedState, and chipResetEvent will yield chipResetState.
In the beginning you can use chipResetState or any other state as initial state. Then when the user taps on a chip, just trigger the chipTappedEvent which should yield chipTappedState.
In your listener, listen for the state chipTappedState and do what you have to do. Then immediately trigger the chipResetEvent which should yield chipResetState. In this way, when the user taps the chip again, the yielded state will be chipTappedState which will be different from chipResetState, so the build function will be called again.

Related

When Super is called in Flutter Bloc?

I have the following code of a Bloc:
class BetBloc extends Bloc<BetEvent, BetState>{
BetBloc() : super(BetInitial() ){
on<LoadBet>(
(event, emit) async {
return;
}
}
);
and in main.dart and other sub-screens I use MultiBlocProvider in the build method
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
BetBloc()..add(LoadBet()),
)
],
child: ...
I can't understand when and why super(BetInitial() ) sets the state to BetInitial. Because every time I change screen the state is reset to BetInitial, even if I don't throw an Event.
Hope I explained well enough
In bloc super() is used to define the initial state of an app. This means what will be the state of an app when you will not call any event or not emit any state. For more info about the bloc go through the bloc documentation.
https://bloclibrary.dev/#/

Consumer not updating with notifyListeners()

I have created a simple Widget Tree of my app in flutter that adresses the issue.
The issue is that when I call the method in my SleepSessionData which is a ChangeNotifier class, the consumer does not get fired.
There is no issue if the method containing notifyListeners() is called within the same widget, but when it is two sibling widgets the consumer does not update unless a setState is called.
And a image of the screen for reference
SleepSessionData extends ChangeNotifier
void updateSelectedPlaylists(List<String> selectedPlaylists) {
this.selectedPlaylists = selectedPlaylists;
notifyListeners();
_saveData();
}
PlaylistSelectorWidget
void selectedIndex(int index) {
...
context.read<SleepSessionData>().updateSelectedPlaylists(_selectedPlaylistIds);
setState(() {});
}
In my SettingsWidget I tried using both Consumer Widget and and watch() method. But it is as the notifyListeners() does not trigger the consumer/watcher. If i run a setState() (triggered by user input) the value from the the ChangeNotifier updates.
Here's my MultiProvider in main.dart
child: MultiProvider(
providers: [
ChangeNotifierProvider<MySleepData>(create: (_) => MySleepData()),
ChangeNotifierProvider<DirectoryData>(create: (_) => DirectoryData()),
FutureProvider<UserData>(
create: (_) => LocalPersistence.getUserData(),
initialData: UserData()),
FutureProvider<SleepSessionData>(
create: (_) => LocalPersistence.getSleepSessionData(),
initialData: SleepSessionData(),
)
],
You shouldn't wrap your ChangeNotifier inside the FutureProvider in order to make it works properly.
When you wrap the ChangeNotifier inside the FutureProvider it breaks the process of adding listeners to the ChangeNotifiers somehow. You always get 0 listeners when wrapping ChangeNotifier with FutureProvider. (You can verify this by debugging and check the listeners property of your ChangeNotifier instance.) That's why when calling notifyListeners(), the widgets don't rerender since they're not the listener of your ChangeNotifier instance.
So the solution to your problem would be:
Use ChangeNotifier in main.dart
child: MultiProvider(
providers: [
ChangeNotifierProvider<MySleepData>(create: (_) => MySleepData()),
ChangeNotifierProvider<DirectoryData>(create: (_) => DirectoryData()),
ChangeNotifierProvider<UserData>(create: (_) => UserData()),
ChangeNotifierProvider<SleepSessionData>(create: (_) => SleepSessionData()),
],
In the SessionSetupPage, you get the data from your local store and load the changes to SleepSessionData
loadData(SleepSessionData sessionData) async {
final data = await LocalPersistence.getData();
sessionData.setData(data);
}
Widget build(BuildContext context) {
loadData(context.read<SleepSessionData>());
// ..
}
Then the code in your PlaylistSelector and SettingsWidget should work whenever notifyListeners() is called since the listeners are bound correctly now.
so when the future completes it will overwrite this value
This is not totally the root cause of the issue though. Even if we create the SleepSessionData instance beforehand and return the same instance inside the FutureProvider, the problem still persists.
You only have a FutureProvider that holds the SleepSessionData so that will only update once when the future completes.
If you want the ui to update on changes of the result of the future then you need to listen to those changes, for instance by using a ChangeNotifierProvider.
Additionally you are giving the FutureProvider an initial data with a new instance of your SleepSessionData so when the future completes it will overwrite this value meaning that any actions you do on the initial value will be discarded.
So a better way of doing this would be to not use a FutureProvider but use a ChangeNotifierProvider and start the future to load the data within your SleepSessionData class.

Triggering Widget Rebuilds with Provider's context.read<T>() Method

According to Flutter's documentation and this example, as I'm understanding it, a key difference between the Provider package's context.read<T> and context.watch<T> methods relate to triggering widget rebuilds. You can call context.watch<T>() in a build method of any widget to access current state, and to ask Flutter to rebuild your widget anytime the state changes. You can't use context.watch<T>() outside build methods, because that often leads to subtle bugs. Instead, they say, use context.read<T>(), which gets the current state but doesn't ask Flutter for future rebuilds.
I tried making this simple app:
class MyDataNotifier extends ChangeNotifier {
String _testString = 'test';
// getter
String get testString => _testString;
// update
void updateString(String aString) {
_testString = aString;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => MyDataNotifier(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text(context.read<MyDataNotifier>().testString),
),
body: Container(
child: Level1(),
),
),
);
}
}
class Level1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (val) {
context.read<MyDataNotifier>().updateString(val);
},
),
Text(context.read<MyDataNotifier>().testString),
],
);
}
}
All the calls are to counter.read<T>(). The app's state changes, but the UI is not rebuilt with the new value. I have to change one of the calls to counter.watch<T>() to get the state to rebuild.
On the other hand, in DZone's simple example, the UI rebuilds, and all the calls are to context.read().
What's different between their code and mine? Why can't I rebuild with counter.read() calls?
TLDR: after a quick glance, the DZone article looks like it has a bug.
Longer answer
context.watch<Foo>() does 2 things:
return the instance of the state from the tree
mark context as dependent on Foo
context.read<Foo>() only does 1).
Whenever your UI depends on Foo, you should use context.watch, since this appropriately informs Flutter about that dependency, and it will be rebuilt properly.
In general, it boils down to this rule of thumb:
Use context.watch in build() methods, or any other method that returns a Widget
Use context.read in onPressed handlers (and other related functions)
The main reason people seem to use context.read inappropriately is for performance reasons. In general, preferring context.read over context.watch for performance is an anti-pattern. Instead, you should use context.select if you want to limit how often a widget rebuilds. This is most useful whenever you have a value that changes often.
Imagine you have the following state:
class FooState extends ChangeNotifier {
// imagine this us updated very often
int millisecondsSinceLastTap;
// updated less often
bool someOtherProperty = false;
}
If you had a widget that displays someOtherProperty, context.watch could cause many unnecessary rebuilds. Instead, you can use context.select only depend on a processed part of the state:
// read the property, rebuild only when someOtherProperty changes
final property = context.select((FooState foo) => foo.someOtherProperty);
return Text('someOtherProperty: $property');
Even with a frequently updating value, if the output of the function provided to select doesn't change, the widget won't rebuild:
// even though millisecondsSinceLastTap may be updating often,
// this will only rebuild when millisecondsSinceLastTap > 1000 changes
final value = context.select((FooState state) => state.millisecondsSinceLastTap > 1000);
return Text('${value ? "more" : "less"} than 1 second...');

Flutter + Bloc 6.0.6. BlocBuilder's "builder" callback isn't provided with the same state as the "buildWhen" callback

I'm building a tic-tak-toe app and I decided to learn BLoC for Flutter along. I hava a problem with the BlocBuilder widget.
As I think about it. Every time Cubit/Bloc that the bloc builder widget listens to emits new state the bloc builder goes through this routine:
Call buildWhen callback passing previous state as the previous parameter and the newly emitted state as the current parameter.
If the buildWhen callback returned true then rebuild.
During rebuilding call the builder callback passing given context as context parameter and the newly emitted state as state parameter. This callback returns the widget that we return.
So the conclusion is that the current parameter of the buildWhen call is always equal to the state parameter of the builder call. But in practice it's different:
BlocBuilder<GameCubit, GameState>(
buildWhen: (previous, current) => current is SetSlotSignGameState && (current as SetSlotSignGameState).slotPosition == widget.pos,
builder: (context, state) {
var sign = (state as SetSlotSignGameState).sign;
// Widget creation goes here...
},
);
In the builder callback, it throws:
The following _CastError was thrown building BlocBuilder<GameCubit, GameState>(dirty, state:
_BlocBuilderBaseState<GameCubit, GameState>#dc100):
type 'GameState' is not a subtype of type 'SetSlotSignGameState' in type cast
The relevant error-causing widget was:
BlocBuilder<GameCubit, GameState>
The method where I emit the states that is in the GameCubit class:
// [pos] is the position of the slot clicked
void setSlotSign(Vec2<int> pos) {
// Some code
emit(SetSlotSignGameState(/* Parameter representing the sign that is being placed in the slot*/, pos));
// Some code
emit(TurnChangeGameState());
}
Briefly about types of states. SetSlotSignGameState is emitted when a user taps on a slot in the tic-tac-toe grid and the slot is empty. So this state means that we need to change sign in some slot. TurnChangeGameState is emitted when we need to give the turn to the next player.
Temporary solution. For now I fixed it by saving the state from buildWhen callback in a private field of the widget's state and then using it from the builder. BlocListener also has this problem but there I can just move the check from listenWhen callback into listen callback. The disadvantage of this solution is that it's very inelegant and inconvenient.
buildWhen is bypassed (not even called) on initial state OR when Flutter requests a rebuild.
I have created a small "test" to emphasize that:
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(BlocTestApp());
}
class BlocTestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider<TestCubit>(
// Create the TestCubit and call test() event right away
create: (context) => TestCubit()..test(),
child: BlocBuilder<TestCubit, String>(
buildWhen: (previous, current) {
print("Call buildWhen(previous: $previous, current: $current)");
return false;
},
builder: (context, state) {
print("Build $state");
return Text(state);
},
),
),
);
}
}
class TestCubit extends Cubit<String> {
TestCubit() : super("Initial State");
void test() {
Future.delayed(Duration(seconds: 2), () {
emit("Test State");
});
}
}
OUTPUT:
I/flutter (13854): Build Initial State
I/flutter (13854): Call buildWhen(previous: Initial State, current: Test State)
As can be seen from output the initial state is built right away without calling buildWhen. Only when the state changes buildWhen is examined.
Other References
This behavior is also outlined here by the creator of Flutter Bloc library (#felangel):
This is expected behavior because there are two reasons for a
BlocBuilder to rebuild:
The bloc state changed
Flutter marked the widget as needing to be rebuilt.
buildWhen will prevent builds triggered by 1 but not by 2. In
this case, when the language changes, the whole widget tree is likely
being rebuilt which is why the BlocBuilder is rebuilt despite
buildWhen.
Possible solution
In your situation, based on the little code you revealed, is better to store the entire Tic-Tac-Toe configuration in the state and use BLOC events to alter it. In this way you do not need that buildWhen condition.
OR make the check inside the builder function if the logic let you do that (this is the most common used solutions with BLOC).
To respond to you question (if not clear so far :D): Sadly, you can not rely on buildWhen to filter the state types sent to builder function.
Could you please check if SetSlotSignGameState extends the abstract class GameState

Flutter - Keep page static throughout lifecycle of app?

I have created an AppDrawer widget to wrap my primary drawer navigation and reference it in a single place, like so:
class AppDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Drawer(
child: new ListView(
children: <Widget>[
new ListTile(
title: new Text("Page1"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => Page1.singleInstance));
}
),
new ListTile(
title: new Text("Page2"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new Page2("Page 2")));
}
),
]
),
);
}
}
I have also created a custom AppScaffold widget, which simply returns a consistent AppBar, my custom AppDrawer, and body:
class AppScaffold extends StatelessWidget {
final Widget body;
final String pageTitle;
AppScaffold({this.body, this.pageTitle});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(title: new Text(pageTitle), backgroundColor: jet),
drawer: AppDrawer(),
body: body
);
}
}
I have created two pages: Page1, and Page2. They are simple right now, and look something like this:
class Page1 extends StatelessWidget {
final String pageText;
Page1(this.pageText);
static Page1 get singleInstance => Page1("Page1");
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeCustomWidget())
],
)
),
);
}
}
class Page2 extends StatelessWidget {
final String pageText;
Page2(this.pageText);
#override
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeOtherCustomWidget())
],
)
),
);
}
}
When I run my app, I can see the navbar and drawer correctly. I can click on the links in the drawer to navigate between my pages. However, each time I navigate to a page, all of the widgets on that page get reset to their initial state. I want to ensure that the widgets do not get reset. Another way to think of this is: I only want one instance of each page throughout the lifecycle of the app, instead of creating them new whenever a user navigates to them.
I tried creating a static instance of Page1 that the Drawer uses when the onTap event is fired, but this does not work. Am I thinking about this incorrectly? Do I need to convert to a Stateful widget?
Oh, you're in for a treat... This will be kinda long (sorry) but please read all of it before making decisions and taking action - I promise I am saving you time.
There are many different solutions to this problem, but in general what you're asking about is state management (which is really software engineering, more info here - Understanding state management, and why you never will).
I'll try my best to explain what is happening in your specific case...
Problem:
Think of Navigator as a List of application states, which you can manipulate via its various methods (i.e. pop(), push(), etc.), with this in mind it is clear what is happening - on a button press you're actually removing the current state (page) and right after that you're pushing a new instance of your state (page).
Solution(s):
As I said, there are many solutions to this problem, for example, you may be tempted to store the state (the changes you made to a particular "page") somewhere in a var and inject that var when navigating between "pages", when creating a new instance of that page, but you'll soon run into other problems. This is why I don't think anyone can provide a simple solution to this problem...
First, may I suggest you some useful reads on the matter:
Flutter official docs on state management - When you get to the "Options" section of this, the fun part begins and can quickly get overwhelming, but fear not :P
Be sure to read the medium article mentioned in the start of my answer too, I found it really helpful.
These reads will be more than enough to help you make a decision, plus there are a ton of articles on Medium and YouTube videos touching on the matter of state management with Flutter (even some from the authors of the framework) - just search for "State management with Flutter".
Now my own personal opinion:
If it's a really simple use case and you don't plan to grow (which is almost never the case, trust me), you can just use StatefulWidgets in combination with setState() and maybe InheritedWidget (for dependency injection down the tree, or like React guys call it "lifting state up"). Or instead of the above, maybe have a look at scoped_model, which kinda abstracts all of this for you (tho, I haven't played with it).
What I use right now for a real world project is bloc and flutter_bloc (BLoC = Business Logic Component), I will not get into the details of it, but basically it takes the idea of scoped_model one step further, without over-complicating abstractions. bloc is responsible for abstracting away the "business logic" of your application and flutter_bloc to "inject" the state in your UI and react to state changes (official Flutter position on the matter is that UI = f(State)).
A BLoC has an input and an output, it takes in events as an input (can be user input, or other, any type of event really) and produces a state. In summary that's it about bloc.
A great way to get started is BLoC's official documentation. I highly recommend it. Just go through everything.
(p.s. This may be my personal opinion, but in the end state management in Flutter is all based on some form of using InheritedWidget and setState() in response to user input or other external factors that should change the application state, so I think the BLoC pattern is really on point with abstracting those :P)