Describe the bug
Hello! 😄
So I have an app that uses moor
as a local database combined with bloc in order to keep the app state. The thing is that the state of a bloc (in this case the WorkoutsBloc
) is not updating when the moor database notifies a change. The bloc has a streamSubscription
to a repository which belongs to the database dao that manages the workouts.
The user can create new workouts and also update them. When updating the workout, in this case, only reordering the exercices from the selected workout, does make moor call its listeners, but then the WorkoutBloc
does not update the changes made, so it keeps the original order.
To Reproduce
I have made the app repository public so you can clone it and reproduce the issue. On opening the app for the first time it will pre-populate the database and it should not take long. Since I do not own a MacOs computer, I have only tested how it looks in Android devices, so I hope you do not run into any issues (if anything is wrong, don't doubt to tell me! It will help me get better!).
Steps to reproduce the behavior:
DashboardScreen
) you will see a card swiper.CreateWorkoutScreen
which is re-used to update the Workouts. Another way to reach this point is by clicking in the text button over the swiper (See All
) and long press the desired card.CreateWorkoutScreen
, you will be able to add or remove exercices from the workout. Just skip this screen by pressing in the upper-right button (Next
).Next
button in the upper-right corner. print
a transition
event, which it should, since the items inside the state have been changed.Expected behavior
I expect the bloc to update its state so the swipper is rebuilt and when the user long presses the card, the order is updated.
Screenshots
| | | | |
| ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| Opening the app - DashboardScreen | After long pressing the legs workout, create workout screen appears | Add sets screen after clicking on the Next
button | Moving the leg press exercise to the bottom (reordering the workout exercices) |
| |
|
|
|
| Summary screen after clicking Next
| Back to dashboard screen after clicking Finish
| And add workout screen loads again after long pressing the legs workout. As you can see the leg press exercise is not updated |
| |
|
|
Logs
$ flutter analyze
Analyzing WorkoutTracker...
No issues found! (ran in 3.1s)
Paste the output of running flutter doctor -v
here.
$ flutter doctor -v
[√] Flutter (Channel stable, 1.20.3, on Microsoft Windows [Version 10.0.18363.1016], locale en-GB)
• Flutter version 1.20.3 at C:\Users\mique\Programs\Flutter
• Framework revision 216dee60c0 (4 days ago), 2020-09-01 12:24:47 -0700
• Engine revision d1bc06f032
• Dart version 2.9.2
[√] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
• Android SDK at C:\Users\mique\AppData\Local\Android\sdk
• Platform android-29, build-tools 29.0.3
• Java binary at: C:\Users\mique\Programs\AndroidStudio\jre\bin\java
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b01)
• All Android licenses accepted.
[!] Android Studio (version 4.0)
• Android Studio at C:\Users\mique\Programs\AndroidStudio
X Flutter plugin not installed; this adds Flutter specific functionality.
X Dart plugin not installed; this adds Dart specific functionality.
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b01)
[√] Connected device (1 available)
• Android SDK built for x86 (mobile) • emulator-5554 • android-x86 • Android 10 (API 29) (emulator)
! Doctor found issues in 1 category.
Additional context
After spending long hours trying to solve the problem, I have realised that the problem comes with the state not rebuilding. I have made sure that everything was being updated properly (which does): the dao
returns the list with the new order, the bloc recieves the event with the new order and yet it does not change it 😠 !
Hi @mikededo 👋
Thanks for opening an issue!
I took a look at the repo and it looks like the re-ordering of the exercises is just modifying local state https://github.com/mikededo/WorkoutTracker/blob/ade14450f8d69021616aa9e69a13145315303d1c/lib/src/ui/widgets/create_workout/add_sets/create_workout_add_sets.dart#L78. I would recommend adding an event to the ExerciseBloc
when the order is changed in order to update the order in the bloc state. Then you can always have the correct exercise order throughout the entire app.
Closing for now but feel free to comment with additional questions/information and I'm happy to continue the conversation 👍
Hey @felangel for taking your time!
I have revised what you have told me. It is true that the state is changed locally, but that is not the issue since the new order is calculated before being inserted in the database. When going to the summary screen, after having moved one exercise, you can see that the order is has changed. This is the order that is passed to the WorkoutsBloc
and then, before inserting, the exercise order is calculated.
When the bloc revieces a WorkoutUpdated
event, it then maps it to WorkoutsLoaded
(I'm using the same logic as the Todos example).
Here is what it is recieved from the event
vs the current state
after changing one exercise:
I/flutter (5422): EVENT
I/flutter (5422): [Pull Up, Chin Up, Lateral Pulldown, Row, Biceps Curl]
I/flutter (5422): [Flat BB Bench Press, Dips, Push Ups, Bench Press]
I/flutter (5422): [Squats, Deadlift, Leg Press]
I/flutter (5422): STATE
I/flutter (5422): [Biceps Curl, Pull Up, Chin Up, Lateral Pulldown, Row]
I/flutter (5422): [Bench Press, Flat BB Bench Press, Dips, Push Ups]
I/flutter (5422): [Squats, Deadlift, Leg Press]
This is after I have moved the biceps curl exercise to the bottom. The event recieves this updated change, and the state has not changed it yet. However, when doing yield WorkoutsLoaded(event.workouts)
the state is not changed (I have tried yield WorkoutsLoaded(List<WorkoutWithExercices>.from(event.workouts))
and nothing changes). 😢
If it is very time consuming for you, do not worry, I will do as much as I can to figure out a solution!
I have tried to add the following:
Stream<WorkoutState> _mapWorkoutsUpdatedToState(
WorkoutsUpdated event,
) async* {
yield WorkoutsLoading();
yield WorkoutsLoaded(event.workouts);
}
Now, since the state changes, it does rebuild the widgets. This is a solution, yet I do not know if it is good practise, since there is no asynchronous calls in between.
So, for any reason, the error (I can't see what I'm doing wrong with the bloc) is that even though the state has changed (the state and then new state contain different lists), it returns true, which is incorrect.
Have you find youself in the same spot when working with lists? The items in the list all extend the Equatable
class, which I do not know if it is necessary.
@mikededo thanks for the update! It sounds like it might just be an equality comparison issue. Can you try returning a new instance of the workouts list?
Stream<WorkoutState> _mapWorkoutsUpdatedToState(
WorkoutsUpdated event,
) async* {
yield WorkoutsLoaded(List.of(event.workouts));
}
It still only works if I yield WorkoutsLoading
before. I had already tried using List.from()
which did not work either.
Are the items from the list suposed to extend Equatable
as well? Or it should work without it?
Yes, the items should also extend Equatable in order for the comparison to work properly
Hmm, I imagined it could be that.
I've been checking the classes generated by the moor
package, which I'm using as a backend, and it already overrides the hashCode
and the ==
operator. Does it make any difference?
No, should be fine to manually override them. Can you verify that the new list is not equal to the old list?
assert(event.workouts != state.workouts)
It does return true.
I/flutter (12831): Event: WorkoutsUpdated(workouts: 3)
I/flutter (12831): true
Just to clarify, it returns true
meaning the two instances are equal? If so that's the issue and you just need to investigate why
No, it is the result of assert(event.workouts != state.workouts)
, which mean that they are different.
can you verify that the entire new state is different from the previous?
assert(WorkoutsLoaded(List.of(event.workouts)) != state);
In this case it does throw an assert exception. So the states are equal? 🤔
yeah which likely means that props are incorrectly overridden in your state class
Is there a way I can debug it? Or what would you recommend?
I would recommend removing your props one by one from the state props override and see when the states are considered different. Then you'll know that the commented prop is the one which is causing the issue and can investigate whether it correctly overrides ==
and hashCode
.
And, when a state has a list of items, can I use the spread operator to add all items to the props list?
@mikededo you don't need to, you can just pass the entire list to props
It takes into account the order of the items on the list, doesn't it?
When comparing [Pull Up, Row, Lateral Pulldown, Chin Up]
with [Chin Up, Pull Up, Row, Lateral Pulldown]
returns true.
hmm strange will try to reproduce right now
When comparing
[Pull Up, Row, Lateral Pulldown, Chin Up]
with[Chin Up, Pull Up, Row, Lateral Pulldown]
returns true.
Here I'm just priting one of the properties of the object, the comparison is done with the lists.
@mikededo I wasn't able to reproduce the issue (Equatable does take order into account when evaluating equality). I would recommend tracing through each of the items in the list (Pull Up
, Row
, etc...) and check the class implementations to see if they override equality comparisons properly. Somewhere down the chain one or more instances are not properly overriding ==
and hashCode
.
Since the classes are generated and they all override ==
and hashCode
, I may be doing something wrong.
I will take a look, thanks for your patience. 💯
Well, I have tried comparing everything and all returned false, but still the state returns true 😭
@mikededo can you share the snippets for you comparison tests? I can try to dig in and see if there's anything I can spot
Sure. Here, before the yield
statement, is were I was using the following snippets.
Checking if dates are equal
for (var a in event.calendar) {
for (var c in (state as WorkoutCalendarLoaded).calendar) {
print(c.dates);
print(a.dates);
print(c.dates == a.dates);
}
}
Checking if the entries of the exercise map from the class WorkoutWithExercices
are equal
if (state is WorkoutCalendarLoaded) {}
for (final a in event.calendar) {
for (final b in (state as WorkoutCalendarLoaded).calendar) {
print(a);
print(b);
for (final c in a.workout.exercices.entries) {
for (final d in b.workout.exercices.entries) {
print(c);
print(d);
print(c == d);
}
}
}
}
}
Checking state equality, which returns true
print(state == WorkoutCalendarLoaded(event.calendar));
Checking if the workout items from the class WorkoutWithExercices
are equal
for (final a in event.calendar) {
final cal = (state as WorkoutCalendarLoaded).calendar.firstWhere(
(i) => i == a,
);
for (int i = 0; i < a.workout.exercices.keys.length; i++) {
final item = a.workout.exercices.keys.toList()[i];
final c = cal.workout.exercices[item];
print(
a.workout.exercices[item].length.toString() + ': ' + c.length.toString(),
);
print(a.workout.exercices[item] == c);
}
}
Just to give you a brief explanation about the BLoC: it basically streams what come from the database, if needed you can find the dao in the day_dao.dart
file. Then this BLoC is provided all around the app since it is used to build the WorkoutCard
displayed in the DashboardScreen
and others. Since I have a lot _many-to-many_ relations in my database, the operations can be quite complex and I try to reuse the Stream
all around the app. That is why in the main.dart
file it is build with a Stream
coming from the WorkoutBloc
. The WorkoutBloc
should also transition into an updated state, even though I have not yet used in the app (I did before using the calendar one).
The bug only occurs when re-ordering the items. If, for instance, a new exercise is added, then it does get rebuilt and the order changes as well as the new exercise is shown. I've been checking the generated files from moor_generator
and all seem to properly override ==
and hashsCode
methods...
But, if this is very time consuming, just don't mind it! I'm sure I will find a solution anytime.
PS: I know this is very cheap debugging but I've been stuck quite a lot for now and I'm trying to move on asap since I want to start developing the other parts of the app.
Hello!
I have been doining more tests and I have not found a solution, yet! I recentrly tried making the moor
generated classes to implement the EquatableMixin
but nothing has changed... I don't really see why the states are equal 😭