trigger and wait for item creation in different BLoC - flutter

My approach below feels way to complicated for a simple thing I am trying to achive:
I have a list of Tasks that is managed by a TaskBloc. The UI lists all tasks and provides an execute button per task. For each click on that button I want to create and store an Action (basically the timestamp when the task-execution happened) and show a spinner while the action is created. I have an ActionBloc that manages the actions (e.g. creation or getting the history per task).
I am confused how to setup the communication between the BLoCs.
This is my approach so far.
The ActionsState just holds a list of all stored actions.
class ActionsState extends Equatable {
final List<Action> actions;
// ... copyWith, constructors etc.
}
Action is a simple PODO class holding an id and timestamp.
The ActionsBloc is capable of creating Actions in response to it's ActionCreationStarted event (holding a int taskId). Since the Action creation is performed in an async isolate there are also events ActionCreationSucceeded and ActionCreationFailed that are added by the isolate once the request finished. Both hold the Action that was either created or whose creation failed.
The TaskState:
class TaskState extends Equatable {
final Map<int, Task> tasks;
// ... copyWith, constructors, etc.
I added a executeStatus to the Task model to keep track of the status of the create request in the task list (a specific task cannot be executed multiple times in parallel, but only sequentially while different tasks can be executed in parallel):
enum Status { initial, loading, success, error }
class Task extends Equatable {
final int id;
final Status executeStatus;
// ...
}
I added events for the TaskBloc:
class TaskExecutionStarted extends TaskEvent {
final int taskId;
// ...
}
class TaskExecutionSucceeded extends TaskEvent {
final int taskId;
// ...
}
class TaskExecutionFailed extends TaskEvent {
final int taskId;
// ...
}
In the TaskBloc I implemented the mapEventToState for the new events to set the task status depending on the event, e.g. for TaskExecutionStarted:
Stream<TaskState> mapEventToState(TaskEvent event) async* {
// ...
if (event is TaskExecutionStarted) {
final taskId = event.taskId;
Task task = state.tasks[taskId]!;
yield state.copyWith(
tasks: {
...state.tasks,
taskId: task.copyWith(executeStatus: Status.loading),
},
);
}
// ...
}
So far this enables the UI to show a spinner per Task but the ActionBloc does not yet know that it should record a new Action for that task and the TaskBloc does not know when to stop showing the spinner.
PROBLEM
Now the part where I am lost is that I need to actually trigger the ActionBloc to create an action and get an TaskExecutionSucceeded (or ...Failed) event afterwards. I thought about using a listener on the ActionsBloc, but it only provides the state and not the events of the ActionsBloc (I would need to react to the ActionCreationSucceeded event, but listening to events of an other bloc feels like an anti-pattern (?!) and I do not even know how to set it up).
The core of the problem is, that I may listen on the ActionsBloc state but I don't know how to distinguish for which actions of the state I would need to trigger a TaskExecutionSucceeded event.
Anyway, I gave the TaskBloc a reference to ActionsBloc:
class TaskBloc extends Bloc<TaskEvent, TaskState> {
final ActionsBloc actionsBloc;
late final StreamSubscription actionsSubscription;
// ...
TaskBloc({
// ...
required this.actionsBloc,
}) : super(TaskState.initial()) {
actionsSubscription = actionsBloc.listen((state) {
/* ... ??? ... Here I don't know how to distinguish for which actions of the state
I would somehow need to trigger a `TaskExecutionSucceeded` event. */
});
};
// ...
}
For the sake of completeness, triggering creation of the Action is simple by adding the corresponding event to the ActionBloc as response to the TaskExecutionStarted:
Stream<TaskState> mapEventToState(TaskEvent event) async* {
// ...
// ... set executeStatus: Status.loading as shown above ...
// trigger creating a new action
actionsBloc.add(ActionCreationStarted(taskId: taskId));
// ...
Of course I aim at clear separation of concerns, single source of truth and other potential sources for accidential complexity regarding app state structure - but overall this approach (which still has said problem unsolved before working) feels way to complicated just to store a timestamp per action of a task and keep track of the action-creation-request.
I appreciate that you read so far (!) and I am very happy about hints towards a clean architecture for that use case.

So what we ended up doing is the following:
Introduce a lastCreatedState in ActionsState that represents the status of the last created action.
Instead of always listening to the ActionsBloc all the time we listen to its state temporarily when task execution is happening and remember the listener per event.
Once we got a change in the ActionsBloc lastCreatedState state that indicates success or failure of our task we remove the listener and react to it.
Something along the lines of this:
/// When a task is executed, we trigger an action creation and wait for the
/// [ActionsBloc] to signal success or failure for that task.
/// The mapping maps taskId => subscription.
final Map<int, StreamSubscription> _actionsSubscription = {};
Stream<TaskState> mapEventToState(TaskEvent event) async* {
// ...
// trigger creation of an action
actionsBloc.add(ActionCreationStarted(taskId: taskId));
// listen to the result
// remove any previous listeners
if (_actionsSubscription[taskId] != null) {
await _actionsSubscription[taskId]!.cancel();
}
StreamSubscription<ActionsState>? listener;
listener = actionsBloc.stream.listen((state) async {
final status = state.lastCreatedState?.status;
final doneTaskId = state.lastCreatedState?.action?.taskId;
if (doneTaskId == taskId &&
(status == Status.success || status == Status.error)) {
await listener?.cancel(); // stop listening
add(TaskExecutionDone(taskId: taskId, status: status!));
}
});
_actionsSubscription[taskId] = listener;
}
#override
Future<void> close() {
_actionsSubscription.values.forEach((s) {
s.cancel();
});
return super.close();
}
It is not perfect: It requires the pollution of the ActionsState and it requires the TaskBloc to not be disposed before all listeners have finished (or at least have other stuff that ensures the state is hydrated and synced on creation) and the polluted sate.
While the internals are a little more complicated it keeps things separated and makes using the blocs a breeze. 🌈

Related

Flutter Bloc Rx dart combineLatest2 combine function not running

I'm writing a flutter app and and using the bloc library. I have a bloc and a cubit, within the state of each is a list of ids of some other documents I need to fetch from firestore. There can be some overlap and some docs are already fetched so I want to get the list of ids from both states, compare them, and then only go to firestore for ones that exist in one but no the other.
I set a new cubit for this:
class CircleRecipesCubit extends Cubit<CircleRecipesState> {
CircleRecipesCubit({
#required RecipesBloc recipesBloc,
#required CirclesCubit circlesCubit,
}) : assert(
recipesBloc != null,
circlesCubit != null,
),
_recipesBloc = recipesBloc,
_circlesCubit = circlesCubit,
super(CircleRecipesInitial());
final RecipesBloc _recipesBloc;
final CirclesCubit _circlesCubit;
StreamSubscription _recipesSubscription;
StreamSubscription _circlesSubscription;
Future<void> getCircleRecipes() async {
// get a list of recipes the user already has loaded
List<String> userRecipesIds;
_recipesSubscription = _recipesBloc.stream.listen((RecipesState event) {
if (event is RecipesLoaded) {
userRecipesIds = event.recipes.map((e) => e.id).toList();
print('*');
print(userRecipesIds);
print('*');
}
});
// get a list of recipes in the circles
List<String> circleRecipeIds;
_circlesSubscription = _circlesCubit.stream.listen((CirclesState event) {
if (event is CirclesLoaded) {
circleRecipeIds = event.circles.fold([],
(previousValue, element) => [...previousValue, ...element.recipes]);
print('|');
print(circleRecipeIds);
print('|');
// List<String> circleOnlyRecipeIds = circleRecipeIds;
// circleRecipeIds.removeWhere((e) => userRecipesIds.contains(e));
// print(circleOnlyRecipeIds);
}
});
// reduce the list of recipes to a set of only circle recipes
//TODO
//------- Try with RX dart
Stream<RecipesState> recipesStream = _recipesBloc.stream;
Stream<CirclesState> circlesStream = _circlesCubit.stream;
Rx.combineLatest2(recipesStream, circlesStream, (
RecipesState recipesState,
CirclesState circlesState,
) {
print("This doesn't print!");
print(recipesState);
print(circlesState);
if (recipesState is RecipesLoaded) {
userRecipesIds = recipesState.recipes.map((e) => e.id).toList();
print('*');
print(userRecipesIds);
print('*');
}
if (circlesState is CirclesLoaded) {
circleRecipeIds = circlesState.circles.fold([],
(previousValue, element) => [...previousValue, ...element.recipes]);
print('|');
print(circleRecipeIds);
print('|');
// List<String> circleOnlyRecipeIds = circleRecipeIds;
// circleRecipeIds.removeWhere((e) => userRecipesIds.contains(e));
// print(circleOnlyRecipeIds);
}
// fetch the set of recipes
});
}
#override
Future<void> close() {
_recipesSubscription.cancel();
_circlesSubscription.cancel();
return super.close();
}
}
So above is my cubit - it listens to the recipesBloc and the circlesCubit. The first two expressions in the getCiricleRecipes() function are only there to prove that its hooked up correctly - when it runs those print statement print the ids I want it to from both the other bloc and the other cubit.
I need the latest values from both though at the same time to compare them - so I thought rx.combinelatest2 would be good. I give it the stream from the bloc and the cubit. But the combiner function doesn't even run even though things seem 'wired up' correctly.
Any help greatly appreciated.
Make sure both streams have already emitted at least one item.
combineLatest documentation states:
The Stream will not emit until all streams have emitted at least one item.
Since the first block (where you subscribe to _circlesCubit) prints, then most likely _recipesBloc is the culprit here.

Flutter stopwatchtimer doesn't respond to changing time

I use this package https://pub.dev/packages/stop_watch_timer in my app to keep track of the music that is playing. However if I want to change the song by changing the time on the stopwatch it says that I have to reset the timer first which I have already done. If I press the button for the second time it works. This is the code:
final StopWatchTimer _stopWatchTimer = StopWatchTimer(
mode: StopWatchMode.countUp,
onChangeRawSecond: (value) => print('onChangeRawSecond $value'),
);
void change_timer_value(int song_index) {
int new_time = TimerState(
song_index: song_index,
record_side: current_side_list(
record_sides[selectedValue], widget.album_data))
.get_start_value();
print(new_time);
_stopWatchTimer.onExecute.add(StopWatchExecute.reset);
_stopWatchTimer.setPresetSecondTime(new_time); // this is where I set new time
}
I don't know how to get around this. I have already created an issue on the creators GitHub but no response. So there's somebody who can help me here
As you mentioned in the github issue, it looks like the root cause of your issue is that the reset action takes place asynchronously, and so hasn't gone through yet by the time you try to set the time.
One way to get around this is to define your own async function which resets the stopwatch, then waits for the action to complete before returning:
Future<void> _resetTimer() {
final completer = Completer<void>();
// Create a listener that will trigger the completer when
// it detects a reset event.
void listener(StopWatchExecute event) {
if (event == StopWatchExecute.reset) {
completer.complete();
}
}
// Add the listener to the timer's execution stream, saving
// the sub for cancellation
final sub = _stopWatchTimer.execute.listen(listener);
// Send the 'reset' action
_stopWatchTimer.onExecute.add(StopWatchExecute.reset);
// Cancel the sub after the future is fulfilled.
return completer.future.whenComplete(sub.cancel);
}
Usage:
void change_timer_value(int song_index) {
int new_time = TimerState(
song_index: song_index,
record_side: current_side_list(
record_sides[selectedValue], widget.album_data))
.get_start_value();
print(new_time);
_resetTimer().then(() {
_stopWatchTimer.setPresetSecondTime(new_time);
});
}
Or (with async/await):
void change_timer_value(int song_index) async {
int new_time = TimerState(
song_index: song_index,
record_side: current_side_list(
record_sides[selectedValue], widget.album_data))
.get_start_value();
print(new_time);
await _resetTimer();
_stopWatchTimer.setPresetSecondTime(new_time);
}

how to handle multiple `loading` states with redux architecture in Flutter?

How do you guys handle multiple loading states with redux pattern in Flutter?
So here is my case:
I have 3 pages, each page calls a different API and shows a loading HUD while requesting the API.
Sub-Question#1 How do i manage these isLoading states?
If i do it in the AppState , i need to add multiple boolean properties to it, something like:
class AppState {
final bool apiOneIsLoading;
final bool apiTwoIsLoading;
final bool apiThreeIsLoading;
// other properties, bla bla...
}
However, Adding to many properties to the AppState class doesn’t sounds great I guess...
Sub-Question#2 How do I update the UI when the loading state changes?
One solution I come up with is to create actions for both loading and loaded state, like so:
class SomeMiddleware extends MiddlewareClass<AppState> {
#override
void call(Store<AppState> store, dynamic action, NextDispatcher next) {
if (action is CallAPIOneAction) {
store.dispatch(APIOneLoadingAction());
// call api one
api.request().then((result){
store.dispatch(APIOneLoadedAction())
})
}
}
}
But if I do this, I need to create 2 extra actions for each API call, is this a good idea? is it okay to change or send new actions in middleware class on the fly?
Please let me know if you have a good solution, thanks!

Alternative for ChangeNotifier that is optimized for large number of listeners?

Flutter documentation for ChangeNotifier says
ChangeNotifier is optimized for small numbers (one or two) of listeners. It is O(N) for adding and removing listeners and O(N²) for dispatching notifications (where N is the number of listeners).
Is there an alternative class available for use in Flutter if I want to design a model where there will be many number of listeners (e.g. dozens of listeners)?
Ideally, I am looking for something with less than O(N^2) for dispatching notifications where N is number of listeners.
Interestingly, when I look at the latest code/doc, it is optimized now!
It says (2021.01):
It is O(1) for adding listeners and O(N) for removing listeners and dispatching notifications (where N is the number of listeners).
Thus we can happily use it. Yeah!
For why this happens: Looking at the source code
void notifyListeners() {
assert(_debugAssertNotDisposed());
if (_listeners!.isEmpty)
return;
final List<_ListenerEntry> localListeners = List<_ListenerEntry>.from(_listeners!);
for (final _ListenerEntry entry in localListeners) {
try {
if (entry.list != null)
entry.listener();
} catch (exception, stack) {
...
}
}
}
we see it iterate through the listeners and call them.
In the old days, say even flutter 1.21, the source code looks like:
void notifyListeners() {
assert(_debugAssertNotDisposed());
if (_listeners != null) {
final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners!);
for (final VoidCallback listener in localListeners) {
try {
if (_listeners!.contains(listener))
listener();
} catch (exception, stack) {
...
}
}
}
}
Thus you see, in the old days there is double loop (a for loop + a contains check), and in the new days there is not.

how to stop firing unrelated event of event bus

My problem is with how to stop firing unrelated event of event bus. as I got this solution for Dialog box.
but it does not work in case of where one instance already initialize and try to create new instance of same class.
Just example: A below scroll panel has handler initialized. it used for document preview.
class TestScroll extends ScrollPanel
{
public TestScroll(){
}
implemented onload()
{
// eventBus.addHandler code here.
//here some preview related code
}
unload() method
{
//eventBus remove handler code
}
}
This preview has some data which contains some links that open different preview but with same class and different data structure,
Now The problem is like onUnload ( which contains code of remove handler) event does not load , because other panel opened. that does not mean previous panel unload.
So in that case, twice event handler registered. when one event fired then other event also fired.
Due to that, Preview 1 data shows properly, but after that Preview2 opened and when I close it, I find Preview1=Preview2.
so how can I handle such situation?
As per no of instance created each event fired. but I have to check some unique document id with if condition in event itself.
is there any other ways to stop unrelated event firing?
Edit:
public class Gwteventbus implements EntryPoint {
int i=0;
#Override
public void onModuleLoad() {
TestApp panel=new TestApp();
Button button=new Button("Test Event");
button.addClickHandler(new ClickHandler() {
#Override
public void onClick(ClickEvent event) {
TestApp panel=new TestApp();
int j=i;
new AppUtils().EVENT_BUS.fireEventFromSource(new AuthenticationEvent(),""+(j));
i++;
}
});
panel.add(button);
RootPanel.get().add(panel);
}
}
public class AppUtils {
public static EventBus EVENT_BUS = GWT.create(SimpleEventBus.class);
}
public class TestApp extends VerticalPanel{
String testString="";
public TestApp( ) {
AppUtils.EVENT_BUS.addHandler(AuthenticationEvent.TYPE, new AuthenticationEventHandler() {
#Override
public void onAuthenticationChanged(AuthenticationEvent authenticationEvent) {
System.out.println("helloworld"+authenticationEvent.getSource());
}
});
}
}
These are wild guesses as it's difficult to really answer it without code and a clear description.
I'm guessing you have one eventbus for all the panels. So when you register a handler it is registered with that one eventbus. In case you fire an event from one of the panels to the eventbus all panels will receive the event.
To fix this you can either create a new eventbus per panel or check who fired the event with event.getSource().
If this doesn't make sense you probably are reusing a variable or use a static variable which actually should be a new instance or none static variable.
You can use the GwtEventService-Library to fire specific events over a unique domain and every receiver that is registered at this domain receives that events then. You can handle as many different events/domains as you want.
In order to remove a handler attached to the EventBus, you must first store a reference to the HandlerRegistration returned by the addHandler method:
HandlerRegistration hr = eventBus.addHandler(new ClickHandler(){...});
Then you can remove the handler with the removeHandler method:
hr.removeHandler();
A final note worth mentioning is that when using singleton views, like is typical with MVP and GWT Activities and Places, it is best practice to make use of a ResettableEventBus. The eventBus passed to an activity's start() is just such a bus. When the ActivityManager stops the activity, it automatically removes all handlers attached to the ResettableEventBus.
I would strongly recommend reading the GWT Project's documentation on:
Activities and Places
Large scale application development and MVP