Is it a bad practice if my bloc's state is a list of widgets?
Asked Answered
C

1

6

I am using flutter_bloc package. Is it a bad practice if I have a bloc whose state is a list of widgets? I use this bloc to push (add) and pop (remove) widgets/mini-screens from a list. I use this list the body of a popup menu, which has something like an embedded navigation. The last member of the list is the widget that is displayed in the popover.

Every time I push or pop, I emit a new state. The bloc is useful since that way I can call push or pop from anywhere in the widgets/mini-screens I display in my popover. Please let me know if my use-case is clear or if you need further details. Thanks.

Here are the relevant pieces of code:

Custom stack (where E will be of type Widget):

class PopoverStack<E> {
  PopoverStack() : _storage = <E>[];
  final List<E> _storage;

  void push(E element) {
    _storage.add(element);
  }

  void pop() {
    _storage.removeLast();
  }

  E get last => _storage.last;

  bool get isEmpty => _storage.isEmpty;

  bool get isNotEmpty => !isEmpty;

  PopoverStack.of(PopoverStack<E> stack) : _storage = List<E>.of(stack._storage);
}

Bloc for stack (PopoverPage is an abstract class widgets will extend):

class PopoverCardStackBloc extends Cubit<PopoverStack<PopoverPage>> {
  PopoverCardStackBloc(PopoverStack<PopoverPage> popoverStack) : super(popoverStack);

  void push(PopoverPage element) {
    emit(PopoverStack.of(state..push(element)));
  }

  void pop() {
    emit(PopoverStack.of(state..pop()));
  }
}

Popover body (here you'll see places where I use state.last as Widget):

class PopoverCardBody extends StatelessWidget {
  const PopoverCardBody({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<PopoverCardStackBloc, PopoverStack<PopoverPage>>(
      builder: (context, state) {
        state;
        return Material(
          color: Colors.transparent,
          borderRadius: BorderRadius.circular(16),
          child: ClipRRect(
            borderRadius: BorderRadius.circular(16),
            child: AnimatedContainer(
              decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)),
              height: state.last.height,
              width: 429,
              duration: const Duration(milliseconds: 200),
              curve: Curves.decelerate,
              child: Column(
                children: [
                  Container(
                    height: 72,
                    padding: const EdgeInsets.all(16),
                    color: AppColors.backgroundLight.withOpacity(.5),
                    child: CenteredTitleBar(
                      title: state.last.title,
                      leadingChild: GestureDetector(
                        behavior: HitTestBehavior.opaque,
                        onTap: state.last.showBackButton
                            ? () {
                                context.read<PopoverCardStackBloc>().pop();
                              }
                            : () {
                                BookingCard.tooltip.close();
                              },
                        child: state.last.showBackButton
                            ? const Icon(
                                Icons.chevron_left,
                                color: Colors.white,
                                size: 24,
                              )
                            : const Text(
                                'Close',
                                style: TextStyle(
                                  color: AppColors.textWhite,
                                  fontSize: 16,
                                  fontWeight: FontWeight.w400,
                                ),
                              ),
                      ),
                      trailingChild: _buildActionButton(context),
                    ),
                  ),
                  Expanded(
                    flex: 80,
                    child: Container(
                      width: double.infinity,
                      padding: const EdgeInsets.all(16),
                      child: state.last as Widget,
                    ),
                  )
                ],
              ),
            ),
          ),
        );
      },
    );
  }

  Widget _buildActionButton(BuildContext context) {
    switch (context.read<PopoverCardStackBloc>().state.last.editButtonType) {
      case StackActionButtonType.NONE:
        return const SizedBox.shrink();
      case StackActionButtonType.EDIT:
        return MockActionButton(
          labelPadding: const EdgeInsets.only(right: 16, left: 16, top: 7, bottom: 9),
          backgroundColor: AppColors.accentButton,
          borderRadius: BorderRadius.circular(8),
          splashColor: AppColors.transparent,
          label: 'Edit',
          textStyle: const TextStyle(
            color: AppColors.textWhite,
            fontSize: 16,
            fontWeight: FontWeight.w600,
          ),
          onTap: () {
            context.read<PopoverCardStackBloc>().push(const EditReservationPage());
          },
        );
      case StackActionButtonType.SAVE:
        return MockActionButton(
          labelPadding: const EdgeInsets.only(right: 16, left: 16, top: 7, bottom: 9),
          backgroundColor: AppColors.accentButton,
          borderRadius: BorderRadius.circular(8),
          splashColor: AppColors.transparent,
          label: 'Save',
          textStyle: const TextStyle(
            color: AppColors.textWhite,
            fontSize: 16,
            fontWeight: FontWeight.w600,
          ),
          onTap: () {
            //TODO: make request with PopoverEditCardStackBloc state to update booking when api is ready.
            BookingCard.tooltip.close();
          },
        );
    }
  }
}

These classes are just here for the one who wants to understand the approach more, however there shouldn't be anything wrong with them. The question is more about what is the correct way to tackle the use case described.

Codi answered 25/5, 2022 at 17:56 Comment(2)
I am not sure, but it seems like a cool use case for RxDart. Hard to say without the code though.Anzus
yes. A bloc should determine state, not the view to display it.Atomism
M
7

It is a bad practice to have widgets in a bloc. Your bloc should not contain any widgets and imports from the Flutter framework. A bloc should only have Dart code and remain platform/environment independent.

This is the first rule of bloc architecture and the reason why engineers develop it at Google. They were trying to use the same business logic code for both Flutter and AngularDart apps and came up with BloC architecture. You can watch this video, :) how it all started: https://www.youtube.com/watch?v=kn0EOS-ZiIc

And check this link about bloc architecture: https://bloclibrary.dev/#/architecture

Matador answered 26/5, 2022 at 15:15 Comment(2)
It should be 'platform/environment independent' that was what I was looking for in an answer. Thanks @Suat Özkaya.Codi
You might want to check this video: youtube.com/watch?v=PLHln7wHgPE - the talk of Paolo Soares, who came up with the idea behind the bloc pattern.Rafiq

© 2022 - 2024 — McMap. All rights reserved.