Bloc: Why isn't there a BlocWidget?

Created on 21 Nov 2019  路  10Comments  路  Source: felangel/bloc

Plenty of my Widgets have the form:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<MyBloc, MyState>(
      builder: (context, state) {
        return MyMagic()
        }
      },
    );
  }
}

It seems to me like this is the way of handling state that's encouraged by the library. This however is a lot of boiler plate code.

I created an new BlocWidgetclass for myself:

abstract class BlocWidget<B extends Bloc<dynamic, S>, S>
    extends StatelessWidget {
  Widget handleState(BuildContext context, S state);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<B, S>(builder: (context, state) {
      return handleState(context, state);
    });
  }
}

This class reduces the boiler-plate code and seem practical to me. Is there a reason why such a class currently doesn't exist?

question

Most helpful comment

I see this as unnecessary barreling. It might seem cool for some cases, but I would rather see the "more explicit" BlocBuilder in my widget tree + it follows the same interface as StreamBuilder does.

Your case only makes sense, if you want to use the exact same widget subtree to follow your BLoC in multiple places, which is most of the time not what you want. Even with the same data, on different places in the app, I would like to generate different widgets. And therefore you would have to have multiple of those ColorBlocWidgets, which totally destroys your argument about boilerplate code.

TL;DR I do not see any valid benefits in your proposal.

All 10 comments

Hi @ChristianKleineidam 馃憢
Thanks for opening an issue!

Can you provide some more context? I'm not sure I understand the difference between BlocWidget and BlocBuilder.

Instead of:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<MyBloc, MyState>(
      builder: (context, state) {
        return MyMagic();
      }
    );
  }
}

Can't you can have:

BlocBuilder<MyBloc, MyState>(
  builder: (context, state) => MyMagic();
);

Let me know what you think?

The difference is that I can easily extend BlocWidget and overwrite handleState. As far as I can see I can't easily extend BlocBuilder.

Can you provide a concrete example? In what case would you want to extend BlocBuilder? I'm not sure I understand the use-case still. Your BlocWidget just looks like a wrapper around BlocBuilder which is already a widget. Instead of builder you expose handleState but the API seems identical.

@ChristianKleineidam, how do you write tests for your widgets which explicitly extends BlocWidget?

Example code:

class TopLevelWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        BlocBuilder<MyBloc, ColorState>(
          builder: getColorText,
        ),
        BlocBuilder<MyBloc, ColorState>(
          builder: getColorText,
        )
      ],
    );
  }
}

Widget getColorText(BuildContext context, ColorState state) {
  if (state is RedColorState) {
    return Text("RED");
  } else {
    return Text("GREEN");
  }
}

class TopLevelWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [ColorBlocWidget(), ColorBlocWidget()],
    );
  }
}

class ColorBlocWidget extends BlocWidget<MyBloc, ColorState> {
  @override
  Widget handleState(BuildContext context, ColorState state) {
    if (state is RedColorState) {
      return Text("RED");
    } else {
      return Text("GREEN");
    }
  }
}

TopLevelWidget2is much more concise then TopLevelWidget1. As far as tests go, I haven't thought deeply about the ideal way of testing but it's easy to initiate a ColorBlocWidgetand see whether the handleState-function returns the desired results. It seems to me similar to testing getColorText.
If I want to use ColorBlocWidget at more places in my code base, the gains of removed boilerplate code are even higher.

I like it, that TopLevelWidget2 doesn't have to care what kind of Widget ColorBlocWidget happens to be.

I see this as unnecessary barreling. It might seem cool for some cases, but I would rather see the "more explicit" BlocBuilder in my widget tree + it follows the same interface as StreamBuilder does.

Your case only makes sense, if you want to use the exact same widget subtree to follow your BLoC in multiple places, which is most of the time not what you want. Even with the same data, on different places in the app, I would like to generate different widgets. And therefore you would have to have multiple of those ColorBlocWidgets, which totally destroys your argument about boilerplate code.

TL;DR I do not see any valid benefits in your proposal.

I agree with tenhobi. Also I believe that your proposed solution is untestable.

As far as tests go, I haven't thought deeply about the ideal way of testing but it's easy to initiate a ColorBlocWidget and see whether the handleState-function returns the desired results.

It is not enough to check if correct state is returned. You also want to have ability to mock some parts of your logic. e.g. network calls. If you embed bloc builder inside your view/widget you lose ability to swap bloc implementation during testing.

In the case of using the ColorBlocWidget only once the amount of boilerplate seems to be the same.

I think that there are plenty of cases where I do want to reuse a widget in multiple times but even if I don't want to reuse it completely, I might want to inherent from it. If I for example have a Bloc that returns events that shift the background color of my buttons, I can create a BackgroundColorBlocWidget and extend my individual buttons from it.

@audkar : I don't see how what I wrote indicates that you could only check if the correct state is returned.
If you want to test network calls I would focus those tests on the Bloc itself.
You can easily test whether the above BlocWidget has the correct behavior given a specific state by testing it's handleState function and see whether it return what you expect.

If I want to test whether the ColorWidget really returns a Text("RED") when passed the state RedColorState() I could go:

    var colorWidget = ColorBlocWidget();
    await tester.pumpWidget(colorWidget.handleState(getContext(), RedColorState()));
    expect(find.text('RED'), findsOneWidget);

This is already concise. I can make it more concise with a helper function:

  void testBlocWidgetForState(WidgetTester tester, T, dynamic state) async{
    var widget = T();
    await tester.pumpWidget(colorWidget.handleState(getContext(), state));
  }

I also don't see how you lose the ability to swap bloc implementations as the bloc that's used is always the bloc that got injected via a BlocProvider.

@ChristianKleineidam thanks for providing more details. I think it's important to make the distinction between having "boilerplate" vs code that is easy to read/understand. I could make TopLevelWidget1 have the same number of lines of code as TopLevelWidget2 like:

class TopLevelWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<MyBloc, ColorState>(
    builder: (context, state) {
      return Column(
        children: [getColorText(state), getColorText(state)],
      );
    },
  ),
}

In my opinion, I would argue that it's much cleaner/reusable to create a StatelessWidget, ColorText

class ColorText extends StatelessWidget {
  final Color color;

  const ColorText({@required this.color});

  @override
  Widget build(BuildContext context) {
    switch (color) {
      case Color.red:
        return Text('RED');
      // etc...
    }
  } 
}

This widget has no dependency on bloc and can be reused anywhere.

Then TopLevelWidget1 becomes:

class TopLevelWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        BlocBuilder<MyBloc, ColorState>(
          builder: (context, state) => ColorText(state),
        ),
        BlocBuilder<MyBloc, ColorState>(
          builder: (context, state) => ColorText(state),
        )
      ],
    );
  }
}

You can simplify this case further by having just a single BlocBuilder

class TopLevelWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<MyBloc, ColorState>(
      builder: (_, state) {
        return Column(
          children: [ColorText(state), ColorText(state)],
        );
      }
    );
  }
}

And in this case you don't need TopLevelWidget1 anymore

BlocBuilder<MyBloc, ColorState>(
  builder: (_, state) {
    return Column(
      children: [ColorText(state), ColorText(state)],
    );
  }
);

Let me know what you think?

Closing for now but feel free to comment with additional information/feedback and I'm happy to reopen the issue 馃憤

Was this page helpful?
0 / 5 - 0 ratings