bloc icon indicating copy to clipboard operation
bloc copied to clipboard

question: Cancel event in separate event handlers

Open Ravensof opened this issue 2 years ago • 6 comments

For example i have start and stop events in separated event handlers. How to cancel start event processing with stop event?

I know i can make a flag like isWorking, but is there any way to cancel event?

Ravensof avatar May 30 '22 09:05 Ravensof

@Ravensof I don't know what use cases you are trying with. By only sentences that you mentioned, you can achieve your goal by making you start and stop event class extends of a base class. And with on listen function add a stream transformer which is something like droppable in bloc_concurrency but opposite implementation with droppable. And implement the event processing function with base event class and separate start and stop event processing function by comparing the class type.

This is only a rough idea. If you need more support. Please provide some runnable code to let me know what kind of use case that you are trying to do and not trying to solve any xy problem.

aaassseee avatar Jun 03 '22 11:06 aaassseee

i have some Bloc who doing long search operation by start event. i want to be able cancel it.

this is how it works now:

@freezed
class ScannerEvent with _$ScannerEvent {
  const factory ScannerEvent.start() = _StartEvent;

  const factory ScannerEvent.stop() = _StopEvent;
}

@freezed
class ScannerState with _$ScannerState {
  const factory ScannerState.initial() = _InitialState;

  const factory ScannerState.scanning() = _ScanningState;

  const factory ScannerState.error(Object error) = _ErrorState;
}

class ScannerBloc extends Bloc<ScannerEvent, ScannerState> {
  ScannerBloc(
    this._cloudSource, {
    required this.onIncomingFile,
    required this.scanDirectories,
    this.onFinished,
  }) : super(const ScannerState.initial()) {
    on<_StartEvent>(
      (event, emit) => _start(emit).onErrorWithReport(ScannerState.error),
      transformer: droppable(),
    );

    on<_StopEvent>(
      (event, emit) => _stop(emit),
      transformer: droppable(),
    );
  }

  final CloudSource _cloudSource;
  final Iterable<CloudDirectory> scanDirectories;
  final FutureOr<void> Function(CloudFile) onIncomingFile;
  final void Function(DateTime startedTime)? onFinished;

  StreamSubscription<void>? _subscription;

  Future<void> dispose() async {
    await _stop(null);
    await _subscription?.cancel();
  }

  Future<void> _start(Emitter<ScannerState> emitter) async {
    final startedTime = DateTime.now();

    emitter(const ScannerState.scanning());

    for (final directory in scanDirectories) {
      if (emitter.isDone || state is! _ScanningState) return;

      final response = await _cloudSource.scanDirectory(directory);

      _subscription = response.stream
          .whereType<CloudFile>()
          .where((file) => FileUtils.isAudio(file.name))
          .asyncListen(onIncomingFile, cancelOnError: true);

      await _subscription
          ?.asFuture<void>()
          .onErrorWithReport((error) => emitter(ScannerState.error(error)));
    }

    if (emitter.isDone || state is! _ScanningState) return;

    emitter(const ScannerState.initial());
    onFinished?.call(startedTime);
  }

  Future<void> _stop(Emitter<ScannerState>? emitter) async {
    await _subscription?.cancel();

    emitter?.call(const ScannerState.initial());
  }
}

i cancel start event by checking _ScanningState state. but i want to only check emitter.isDone.

i just want to cancel first handler from another handler.

Ravensof avatar Jun 03 '22 11:06 Ravensof

@Ravensof What cancelation are you trying to implement? Is event stop is what you want? And are you trying to cancel the _cloudSource.scanDirectory? if so what is the return type of that function.

aaassseee avatar Jun 03 '22 23:06 aaassseee

@Ravensof Your code is still not runnable to me. However I can give some idea. If you are trying to cancel whole process of

    for (final directory in scanDirectories) {
      if (emitter.isDone || state is! _ScanningState) return;

      final response = await _cloudSource.scanDirectory(directory);

      _subscription = response.stream
          .whereType<CloudFile>()
          .where((file) => FileUtils.isAudio(file.name))
          .asyncListen(onIncomingFile, cancelOnError: true);

      await _subscription
          ?.asFuture<void>()
          .onErrorWithReport((error) => emitter(ScannerState.error(error)));
    }

You can use CancelableOperation from async which is provided by dart team. You can wrap you whole scanDirectories for loop into one CancelableOperation and cancel it by calling stop event.

Here is an interactive example which is using the demo counter app and adding delay by the counter number. User can also cancel the delay increment process by using CancelableOperation.

import 'package:async/async.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  CancelableOperation? operation;

  Future<int> delayWithNumber(int number) async {
    await Future.delayed(Duration(seconds: number));
    return number;
  }

  void _startCounter() async {
    operation = CancelableOperation.fromFuture(delayWithNumber(_counter));
    operation!.value.then((value) {
      setState(() {
        _counter++;
      });
    });
  }

  void _cancelCounter() {
    operation?.cancel();
  }

  @override
  void dispose() {
    operation?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            onPressed: _startCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
          FloatingActionButton(
            onPressed: _cancelCounter,
            tooltip: 'cancel',
            child: const Icon(Icons.cancel),
          ),
        ],
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Is that what you want?

aaassseee avatar Jun 04 '22 00:06 aaassseee

@aaassseee thanks for your answer, but that's not what im looking for. i know about future and that it cant be cancelled. my question is only about new bloc. is there any mechanism, to cancel event handler. sorry, that i cant explain it in english.

here is example bloc:

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';

class TestEvent {}

class StartEvent extends TestEvent {}

class StopEvent extends TestEvent {}

class TestState {}

class InitialState extends TestState {}

class WorkingState extends TestState {}

class TestBloc extends Bloc<TestEvent, TestState> {
  TestBloc() : super(InitialState()) {
    on<StartEvent>(
      (event, emit) async {
        for (var i = 0; i < 1000; i++) {
          emit(WorkingState());

          await Future<void>.delayed(const Duration(seconds: 1));

          // how to make it 'true' with 'StopEvent'?
          if (emit.isDone) return;
        }
      },
      transformer: droppable(),
    );

    on<StopEvent>((event, emit) {
      // do something to stop 'StartEvent'
    });
  }
}

i want to make emit.isDone in handler on<StartEvent> returns true, because of StopEvent.

it should be like restartable() transformer, but restartable can only ignore emits from old events inside current event handler

Ravensof avatar Jun 04 '22 07:06 Ravensof

@Ravensof I don't think there is a way to change emit.isDone == true by end users. Referring to the code emit.isDone is simply _isCanceled || _isCompleted in _Emitter class which is an internal class. _isCanceled only become true when bloc close, and _isCompleted only change when event handler future is done. I don't think you are using the correct flag due to both _isCanceled and _isCompleted is only change by internal class. So isDone is not what you are looking for.

aaassseee avatar Jun 04 '22 07:06 aaassseee

I too have almost same situation in my notification bloc i have 4 events and when ever NotificationsFilter event is added to the bloc i want to cancel all pending emit in the NotificationLoadEvent- is there any way to obtain this behaviour @aaassseee

`class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> { NotificationsBloc() : super( const NotificationsState(), ) { on<NotificationLoadEvent>( (event, emit) { if (event is NotificationsInitialLoad) { return _onNotificationInitialLoad(event, emit); } if (event is NotificationsLoadMore) { return _onNotificationsLoadMore(event, emit); } if (event is NotificationsRefresh) { return _onNotificationsRefresh(event, emit); } }, transformer: droppable(), );

on<NotificationsFilter>(
  _onNotificationsFilter,
  transformer: restartable(),
);

}} `

bazl-E avatar Mar 05 '23 13:03 bazl-E

Hi, @bazl-E. For this situation, I would like to say your bloc having multiple jobs within one bloc. I think you should separate service and filter into different bloc otherwise state handling will become difficult to handle in the future. Ref: single responsibility principle

aaassseee avatar Mar 06 '23 04:03 aaassseee

I found a solution related to this case https://github.com/felangel/bloc/issues/3757#issuecomment-1620507186 Hope this help 🙏

ankiimation avatar Jul 04 '23 16:07 ankiimation