Connected Widget Pattern

This is something that I have found myself doing really often in my Flutter code base lately. If you are you are familiar with react-redux patterns on the web (or react-native) this would be probably obvious for you.

Most of my Dart files (with Flutter widgets) right now have at least two widgets.

First is the widget that I actually need! It is either stateless or state-full, has all the necessary presentation components, like lists, buttons, texts, images etc. But it does not have ANY logic and it does NOT access outside data.

Of course as with everything in programming there are exceptions. I do access theme data on this level and also do some simple logic like decide when things should be visible or active.

Other than those exception everything comes in as parameters! From… you guess it… The second widget in that file that has “Connected” suffix and always is stateless. On this level I am connecting everything together. This is the place when I am accessing my state management layer. Also this is the place where I do access my translations and custom theme data.

Wire Up!

For most part Connected widgets do not have any parameters, since most of the data needed comes from the state or context. When you intend to create a Connected list item, then it makes sense to take item index or even item it self as a parameter 😉

This way we get a pure widget that is library quality, it is easily extract-able to a different project or to a custom library project. This also means that it is easier to test, you don’t need to provide all of the external dependencies in order to write tests for it. From my perspective it also encourages you to put your business logic somewhere else, not in the UI component. This also encourages to do not hardcode labels, but extract them into parameters and eventually to translate them.

Secondly, with Connected widgets you have a clear layer where you connect you widget to data. This is a simple data binding in form get this information from state (or context) and put it on this widget. It is simple extract-and-assign layer that IMO not necessarily needs to be tested.

Show me the code!

Lets assume you have to list people invited for the party. The task is to implement a widget that would show their first and last name and have a button to remove given person form the list.

First lets implement pure widget for it:

class GuestListTile extends StatelessWidget {
  const GuestListTile({
    Key key,
    @required this.person,
    @required this.onRemove,
  }) : super(key: key);

  final Person person;
  final Function(Person) onRemove;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(person.name),
      subtitle: Text(person.surname),
      leading: IconButton(
        icon: Icon(Icons.delete),
        onPressed: () => onRemove(person),
      ),
    );
  }
}

That is a simple code, one can argue that it will be easier to fire your action directly form onPressed of IconButton and do not bother about another layer. But then it would increase complexity in your widget tests, since you need to also provide your bloc/provider/mobx/redux state manager around this widget and probably test changes outside of the GuestListTile widget. Instead:

class GuestListTileConnected extends StatelessWidget {
  const GuestListTileConnected({
    Key key,
    @required this.person,
  }) : super(key: key);

  final Person person;

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<BlocA, BlocAState>(
      builder: (context, state) => GuestListTile(
        person: person,
        onRemove: state.removePersoFromList,
      ),
    );
  }
}

I assume here that you Bloc would have a removePersonFromList method that accepts person as a parameter. Hope you get the idea.

Now you can easily write widget tests for GuestListTile and use the Connected version on the upper layer of UI.

What do you think about this approach? Are you already using it or will you give it a try?

Leave a Reply

Your email address will not be published. Required fields are marked *