Consumer not updating with notifyListeners() - flutter

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.

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/#/

Flutter: accessing providers from other providers

For my flutter project, I am using the following multiple providers below:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<FirstProvider>(
create: (context) => FirstProvider(),
),
ChangeNotifierProvider<SecondProvider>(
create: (context) => SecondProvider(),
),
ChangeNotifierProvider<ThirdProvider>(
create: (context) => ThirdProvider(),
),
ChangeNotifierProvider<FourthProvider>(
create: (context) => FourthProvider(),
),
],
child: const MainApp(),
);
}
Because sometimes I need to either get data or call functions from different providers from another provider, I am using it like this:
//First Provider
class FirstProvider with ChangeNotifier {
void callFunctionFromSecondProvider({
required BuildContext context,
}) {
//Access the SecondProvider
final secondProvider= Provider.of<SecondProvider>(
context,
listen: false,
);
secondProvider.myFunction();
}
}
//Second Provider
class SecondProvider with ChangeNotifier {
bool _currentValue = true;
void myFunction(){
//Do something
}
}
The callFunctionFromSecondProvider()of the FirstProvider is called from a widget and it will call myFunction() successfully, most of times.
Depending on the complexity of the function, I am sometimes experiencing that I can't access the SecondProvider, presumably due to context being null, when the widget state changes.
I am reading some documents online regarding provider, and they are suggesting changenotifierproxyprovider for what I understood as 1 to 1 provider relationship.
However, in my case, one provider needs to be accessed by multiple providers and vice versa.
Question:
Is there a more appropriate way that I can approach my case where one provider can be accessed by multiple providers?
EDIT:
Accessing provider should also be able to access different variable values without creating a new instance.
Instead of passing context to the callFunctionFromSecondProvider function add the second provider as the parameter. So the function looks like the below.
Not sure this is the correct way of doing that but my context null issue was fixed this way.
void callFunctionFromSecondProvider({
required SecondProvider secondProvider,
}) {
secondProvider.myFunction();
}
}
Alright.
So it looks like Riverpod by the same author is the way to go as it addresses alot of flaws such as Provider being dependent on the widget tree, in my case, where the underlying issue came from.
—--------
For the time being, I still need to use the provider and for a quick and dirty solution, I am providing the context of not only the current widget that I am trying to access the provider, but also passing the parent context of the widget directly, so that in case a modal (for example) is closed, then any subsequent provider call can still be executed using the parent context.
Hope this helps.

Flutter Riverpod: using ref.watch in the constructor for a ChangeNotifier, how to avoid Notifier being recreated upon changes

I have a ChangeNotifier, which I use with ChangeNotifierProvider to track the state of a screen in my app. It has a constructor:
CategoryViewState(ProviderRefBase ref, int listId) {
subjects = ref.watch(otherProvider.select((value) => value.getSubjects()));
}
The problem I'm encountering is that when otherProvider.getSubjects() changes, the whole ChangeNotifier is recreated from scratch, rather than the subjects list being updated. This means the state of the page is lost.
Is there a fix or another way to do this that avoids this happening?
Just put the widget that list to the changes in a Consumer widget and watch changes inside it:
Consumer(builder: (context, ref, child) {
final subjects = ref.watch(otherProvider.select((value) => value.getSubjects()));
return YourWidget(); // Just this widget will be rebuilt
},)

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

How can I use Provider to provide a bloc to a PageView() without the child resubscribing everytime I switch page?

I am using Provider to provide my bloc to a widget called TheGroupPage via a static create method
static Widget create(BuildContext context, GroupModel group) {
final database = Provider.of<DatabaseService>(context);
return Provider(
create: (_) => GroupMembersBloc(database, group),
child: TheGroupPage(group),
dispose: (BuildContext context, GroupMembersBloc bloc) => bloc.dispose(),
);
}
That widget has a PageView() with 3 pages
PageView(children: [
TheGroupNotificationsView(),
TheGroupMembersView(group),
TheGroupSettingsView(group),
])
The group members view looks for the GroupMembersBloc
GroupMembersBloc bloc = Provider.of<GroupMembersBloc>(context);
I also tried to put listen to false but this did not work. And I want the widget to listen for any changes. The page uses that bloc's stream to draw a list of group members
class GroupMembersBloc{
StreamController<List<UserModel>> _controller = StreamController<List<UserModel>>();
Stream<List<UserModel>> get stream => _controller.stream;
GroupMembersBloc(DatabaseService database, GroupModel group)
{
_controller.addStream(database.groupMembersStream(group));
}
void dispose(){
_controller.close();
}
}
The problem is when I switch page inside the PageView() I get an error on the page after the first time it has been shown. The error says Bad state: Stream has already been listened to. how can I solve this?
That's because stream controllers allow only 1 Subscription (or 1 listener) , you could use the [StreamController<List<UserModel>>.broadcast()][1] constructor instead of StreamController>().
I ended up moving the StreamBuilder to the parent widget above the PageView() which fixed the problem.