How to show error messages or do success action while using Flutter Bloc during deletion?

I have one bloc, example PostsBloc that exposes one event LoadData and one event DeleteDataById and emits state of type LoadableBlocState<Post>

The PostsBloc is using the PostsRepository which makes simple http calls to get items and delete single item.

If the delete call gets a response statusCode !== 200 I want to show error message to the frontend (scaffold). I want to show an error every time the delete error happens. Also on consecutive errors.

On the other hand, if the deletion is successful (statusCode 200), I want to remove this post from the data, show success message in the ui (scaffold) and pop the navigator context / go back (when I delete the post from the post_screen.dart itself)

The simplified code of posts_bloc.dart

import 'package:my_app/src/app/app_runner.dart';
import 'package:my_app/src/common/common.dart';
import 'package:my_app/src/features/features.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class PostsBloc
    extends Bloc<LoadableBlocEvent, LoadableBlocState<PostsBlocValue>> {
  final PostsService _postsService;

  PostsBloc(this._postsService)
      : super(const LoadableBlocState.initial()) {
    on<LoadDataEvent>((event, emit) async {
      emit(const LoadableBlocState.loading());

      try {
        final r = await _postsService.getPosts();

        if (r.statusCode == 200) {
          emit(LoadableBlocState.loaded(PostsBlocValue(
            posts: _formPosts(r.parsedBody!),
            postFolders: r.parsedBody!.folders,
          )));
        } else {
          emit(LoadableBlocState.error(r));
        }
      } catch (e, stacktrace) {
        recordError(e, stacktrace);
        emit(LoadableBlocState.error(e));
      }
    });

    on<DeleteDataByIdEvent>(
          (event, emit) async {
        final stateData = state.data;
        if (stateData == null) {
          return;
        }

        final r = await _postsService.deletePost(event.itemId);

        if (r.statusCode == 200) {
          emit(LoadableBlocState.loaded(PostsBlocValue(
            posts: stateData.posts
              ..removeWhere((e) => e.id == event.itemId),
            postFolders: stateData.postFolders,
          )));
        } else {
          emit(LoadableBlocState.otherError(
            state,
            LoadableBlocError.withCode(
              action: LoadableBlocAction.delete,
              code: r.statusCode.toString(),
            ),
          ));
        }
      },
    );
  }
}

List<Post> _formPosts(GetPostsResponse getPostsResponse) {
  List<Post> posts = [];

  for (final el in getPostsResponse.posts) {
    posts.add(
      Post(
        id: el.id,
        name: el.name,
        entries: el.entries
            .map((re) =>
            postEntryDataToPostEntry(
              re,
              exercises: getPostsResponse.exercises,
              metrics: getPostsResponse.metrics,
            ))
            .toList(),
        fromUserId: el.fromUserId,
        folderId: el.folderId,
        unitsConfig: el.unitsConfig,
        createdAt: el.createdAt,
        updatedAt: el.updatedAt,
      ),
    );
  }

  return posts;
}

class PostsBlocValue {
  final List<Post> posts;
  final List<PostFolder> postFolders;

  PostsBlocValue({required this.posts, required this.postFolders});
}

I tried to hold a state for possible deletion errors

I created a state object which can hold “non fatal” errors and the widgets listen to them

loadable_bloc_state.dart

import 'package:equatable/equatable.dart';

class LoadableBlocState<T> extends Equatable {
  final bool loading;
  final T? data;
  final LoadableBlocError? error;

  const LoadableBlocState._({
    required this.loading,
    required this.data,
    required this.error,
  });

  const LoadableBlocState.initial()
      : this._(
          loading: false,
          data: null,
          error: null,
        );

  const LoadableBlocState.loading()
      : this._(
          loading: true,
          data: null,
          error: null,
        );

  const LoadableBlocState.loaded(T data)
      : this._(
          loading: false,
          data: data,
          error: null,
        );

  LoadableBlocState.errorLoading(Object error)
      : this._(
          loading: false,
          data: null,
          error: LoadableBlocError.withError(
              action: LoadableBlocAction.fetch, error: error),
        );

  LoadableBlocState.otherError(
      LoadableBlocState current, LoadableBlocError error)
      : this._(
          loading: current.loading,
          data: current.data,
          error: error,
        );

  bool isFetchFailed() =>
      error != null && error!.action == LoadableBlocAction.fetch;

  @override
  List<Object?> get props => [this.loading, this.data, this.error, this.error];
}

class LoadableBlocError extends Equatable {
  final LoadableBlocAction action;
  final String? code;
  final Object? error;

  const LoadableBlocError._(
      {required this.action, required this.code, required this.error});

  const LoadableBlocError.withCode(
      {required LoadableBlocAction action, required String code})
      : this._(action: action, code: code, error: null);

  const LoadableBlocError.withError(
      {required LoadableBlocAction action, required Object error})
      : this._(action: action, code: null, error: error);

  @override
  List<Object?> get props => [action, code, error];
}

enum LoadableBlocAction {
  fetch,
  delete,
  create,
  update,
}

loadable_bloc_event.dart

import 'package:equatable/equatable.dart';

class LoadableBlocEvent extends Equatable {
  @override
  List<Object?> get props => [];
}

class LoadDataEvent extends LoadableBlocEvent {}

class DeleteDataByIdEvent extends LoadableBlocEvent {
  final String itemId;

  DeleteDataByIdEvent({required this.itemId});

  @override
  List<Object?> get props => [this.itemId];
}

After implementing the bloc above I listened to the emitted states from my widget in two different ways

  1. Using Bloclistener and did not work because it triggers only 1 time and not the 2nd time if the state does not change. Use-case: User clicks “delete post” button, the first call fails and scaffold is shown fine. User clicks again, it fails again, state did not change and bloclistener is not triggered
  2. I used BlocConsumer and the result was the same as BlocListener

Successful approaches using bloc anti-patterns:

I can’t find a proper solution respecting the bloc design pattern.
I am posting below some solutions that work great

Approach 1

  1. Presentation layer, example: posts_view.dart
    a) calling directly the repository
    b) if code !== 200 then show scaffold, otherwise emit bloc event to remove the post data

Question:
What do you think is the cleanest approach for supporting my use case while using blocs the way they are designed? I can’t find a single proper solution on the entire web using blocs for such a simple, wanting to listen to delete/update/insert errors and showing them every time they happen and not only the single time.

This seems simple but is more complex. Another example: You trigger post deletion of post id 3, then open post_screen for post 1.
You get error from post id 3 and show error in screen of 1. Probably have to send identifier as well. I tried that but bloclistener is trigger only one time on consecutive errors still.

As you said your problem is for the second TapOn the error message would not shown, the solution is to change state to the loading or anything else right after user clicked on the button and then start delete process, the result be and error or success

5

I created one solution that meets all the bloc pattern requirements and only dispatches events and listens to state emits.

I modified my loadable_bloc_state.dart to look like this:

import 'package:equatable/equatable.dart';

class LoadableBlocState<T> extends Equatable {
  final Action<T> action;
  final T? data;

  const LoadableBlocState._({
    required this.action,
    required this.data,
  });

  const LoadableBlocState.initial()
      : this._(
          action: const LoadingDataAction(
            inProgress: false,
            error: null,
          ),
          data: null,
        );

  const LoadableBlocState.loading()
      : this._(
          action: const LoadingDataAction(
            inProgress: true,
            error: null,
          ),
          data: null,
        );

  const LoadableBlocState.loaded(T data)
      : this._(
          action: const LoadingDataAction(
            inProgress: false,
            error: null,
          ),
          data: data,
        );

  LoadableBlocState.errorLoading(Object error)
      : this._(
          action: LoadingDataAction(
            inProgress: false,
            error: LoadableBlocError.withError(error: error),
          ),
          data: null,
        );

  bool isFetching() => action.type == ActionType.fetch && action.inProgress;

  bool isFetchFailed() =>
      action.type == ActionType.fetch && action.error != null;

  const LoadableBlocState.withAction({
    required Action<T> action,
    required T? data,
  }) : this._(action: action, data: data);

  @override
  List<Object?> get props => [
        this.action,
        this.data,
      ];
}

enum ActionType {
  fetch,
  update,
  delete,
  create,
}

abstract class Action<T> extends Equatable {
  final ActionType type;
  final bool inProgress;
  final LoadableBlocError<T>? error;

  const Action({required this.type, required this.inProgress, this.error});

  bool didSucceed() => error == null && !inProgress;

  @override
  List<Object?> get props => [type, inProgress, error];
}

class LoadingDataAction<T> extends Action<T> {
  const LoadingDataAction({required super.inProgress, required super.error})
      : super(type: ActionType.fetch);
}

class ItemAction<T, R extends Equatable> extends Action<T> {
  final String itemId;
  final R? req;

  const ItemAction._(
      {required this.itemId,
      this.req,
      required super.type,
      required super.inProgress,
      super.error});

  const ItemAction.success({
    required String itemId,
    R? req,
    required ActionType type,
  }) : this._(
          itemId: itemId,
          type: type,
          req: req,
          inProgress: false,
          error: null,
        );

  const ItemAction.error(
      {required String itemId,
      R? req,
      required ActionType type,
      required LoadableBlocError<T> error})
      : this._(
          itemId: itemId,
          type: type,
          req: req,
          inProgress: false,
          error: error,
        );

  @override
  List<Object?> get props => [itemId, req, type, inProgress, error];
}

class LoadableBlocError<T> extends Equatable {
  final String? code;
  final Object? error;

  // This timestamp can be used to trigger state changes and ensure that BlocListeners/Consumers are re-triggered. For instance, it helps in showing multiple delete error messages sequentially during consecutive retry attempts.
  final DateTime? timestamp;

  const LoadableBlocError(
      {required this.code, required this.error, this.timestamp});

  const LoadableBlocError.withCode({required String code, DateTime? timestamp})
      : this(code: code, error: null, timestamp: timestamp);

  const LoadableBlocError.withError(
      {required Object error, DateTime? timestamp})
      : this(code: null, error: error, timestamp: timestamp);

  @override
  List<Object?> get props => [code, error, timestamp];
}

This change allows for:

  1. Having a state for loading items action
  2. Having a state for item level actions (delete, fetch, create,
    update)

    • a) which has the itemId for telling which item was acted on
    • b) success or error state. You can use the error object to show proper message on the presentation layer
    • c) can store the original “request” object just in case needed for logging
    • d) inProgress state – you can show loading spinners on item levels!

The events look like loadable_bloc_event.dart

import 'package:equatable/equatable.dart';

class LoadableBlocEvent extends Equatable {
  @override
  List<Object?> get props => [];
}

class LoadDataEvent extends LoadableBlocEvent {}

class DeleteDataByIdEvent extends LoadableBlocEvent {
  final String itemId;

  DeleteDataByIdEvent({required this.itemId});

  @override
  List<Object?> get props => [this.itemId];
}

This “mini library” can be used smoothly like in the example below:

some_widget.dart triggers post deletion


BlocProvider.of<PostsBloc>(context)
              .add(DeleteDataById(itemId: widget._post.id));

posts_bloc.dart

import 'package:my_app/src/common/common.dart';
import 'package:my_app/src/features/features.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class PostsBloc
    extends Bloc<LoadableBlocEvent, LoadableBlocState<PostsBlocValue>> {
  final PostsService _postsService;

  PostsBloc(this._postsService)
      : super(const LoadableBlocState.initial()) {
    on<LoadData>((event, emit) async {
      emit(const LoadableBlocState.loading());

      try {
        final r = await _postsService.getPosts();

        if (r.statusCode == 200) {
          emit(LoadableBlocState.loaded(PostsBlocValue(
            posts: r.parsedBody!,
            postFolders: r.parsedBody!.folders,
          )));
        } else {
          emit(LoadableBlocState.errorLoading(r));
        }
      } catch (e, stacktrace) {
        recordError(e, stacktrace);
        emit(LoadableBlocState.errorLoading(r));
      }
    });

    on<DeleteData<Post>>(
          (event, emit) async {
        final stateData = state.data;
        if (stateData == null) {
          return;
        }

        try {
          final r = await _postsService.deletePost(event.item.id);

          if (r.statusCode == 200) {
            emit(LoadableBlocState.withAction(
                action: ItemAction.success(
                    itemId: event.item.id, type: ActionType.delete),
                data: PostsBlocValue(
                  posts: stateData.posts
                    ..removeWhere((e) => e.id == event.item.id),
                  postFolders: stateData.postFolders,
                )));
          } else {
            emit(LoadableBlocState.withAction(
              action: ItemAction.error(
                  itemId: event.item.id,
                  type: ActionType.delete,
                  error: LoadableBlocError.withCode(
                      code: r.statusCode.toString(),
                      timestamp: DateTime.now())),
              data: state.data,
            ));
          }
        } catch (e, stacktrace) {
          recordError(e, stacktrace);
          emit(LoadableBlocState.withAction(
            action: ItemAction.error(
                itemId: event.item.id,
                type: ActionType.delete,
                error: LoadableBlocError.withError(
                    error: e,
                    timestamp: DateTime.now())),
            data: state.data,
          ));
        }
      },
    );
  }
}

class PostsBlocValue {
  final List<Post> posts;

  PostsBlocValue({required this.posts, required this.postFolders});
}

The PostsService is just using the http package to make some REST HTTP api calls and returns parsed responses.

Presentation layer usage:
post_screen.dart is listening to actions in case of error/success and acts on it

import 'package:my_app/src/common/common.dart';
import 'package:my_app/src/features/features.dart';
import 'package:my_app/src/l10n/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class PostScreen extends StatefulWidget {
  final Post _post;

  const PostScreen(this._post, {super.key});

  @override
  State<PostScreen> createState() => _PostScreenState();
}

class _PostScreenState extends State<PostScreen> {
  UnitsConfig _userUnitsConfig = defaultUnitsConfig;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Row(
              children: [
                Text(S.of(context).post),
              ],
            )),
        body: BlocListener<PostsBloc, LoadableBlocState<PostsBlocValue>>(
          listenWhen: (previous, current) =>
          current.action.type != ActionType.fetch &&
              current.action is ItemAction &&
              (current.action as ItemAction).itemId == widget._post.id,
          listener: (context, state) {
            final action = state.action;

            if (action.type == ActionType.delete) {
              if (action.error != null) {
                FlushBarError.error(
                  context,
                  message: S.of(context).internal_server_error,
                  icon: Icons.delete,
                ).show(context);
              } else {
                if (action.didSucceed()) {
                  Navigator.pop(context);
                }
              }
            }
          },
          child: (
              Text(widget._post.name),
          ),
        ));
  }
}


Pros

  1. This solution works great and conforms to the ideal bloc architecture pattern.
  2. It also works for parallel actions. Example: Multiple items deletions at once. The events will be emitted in sync and not lost. Listeners will listen and act on them. (I made some tests and works fine in fifo fashion)

Cons

  1. in my humble opinion I find it over-engineered for such a simple functionality.
  2. It mixes events regarding collection level data loading with item level actions. (that can be ok or not it is subjective opinion). My concern is that there will be many state triggers for every item update. Example: Why should a page that renders the list of posts care about a particular action update? This page is interested only in the data array object. The performance implication is negligible though especially when using listenWhen and we compare the data array.

Other simpler and elegant “non perfectly clean bloc solutions”

You keep a simple state class and just introduce a method deletePost in posts_bloc.dart

  Fuuture<Response> deletePost(String id) async {
    final r = await _postsService.deletePost(id);
    if (r.statusCode == 200) {
      emit(LoadableBlocState.loaded(PostsBlocValue(
        posts: r.parsedBody!,
        postFolders: r.parsedBody!.folders,
      )));
    }
    
    return r;
  }

You call that and wait for the answer, then do something in the presentation layer.

This gives you all the power without the need to dispatch state

  1. You already have the item id of the item that you are acting on (delete/update)
  2. You already have the original request and no need to dispatch it in case of wanting to log etc
  3. You have the response immediately in a “sync” fashion. No need to wait for some event that contains the response or updated data.

post_screen.dart

final response = await _postsBloc.deletePost(postId);
if (response.statusCode !== 200) {
   // show some error scaffold
}

With the example above you don’t need bloclistener or overcomplicated logic to tell what item was updated, when, what action etc and avoid many state emits. But technically it is considered “anti pattern”

The loadable_bloc_state.dart will be very simple as well as it won’t require to keep track of actions.

import 'package:equatable/equatable.dart';

class LoadableBlocState<T> extends Equatable {
  final bool loading;
  final T? data;
  final Object? error;

  const LoadableBlocState._(
      {required this.loading, required this.data, required this.error});

  const LoadableBlocState.initial()
      : this._(
          loading: false,
          data: null,
          error: null,
        );

  const LoadableBlocState.loading()
      : this._(
          loading: true,
          data: null,
          error: null,
        );

  const LoadableBlocState.loaded(T data)
      : this._(
          loading: false,
          data: data,
          error: null,
        );

  const LoadableBlocState.error(Object error)
      : this._(
          loading: false,
          data: null,
          error: error,
        );

  @override
  List<Object?> get props => [this.loading, this.data, this.error];
}

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật