Event-Based Flutter Example

Recently I encountered the event_bus package on pub.dev. The Event Bus package allows you to easily implement the publisher-subscriber pattern in Flutter. With thepublisher-subscriber pattern, it is possible to decouple the publisher and the subscriber. I am going to show how you could use this to create the counter example and a simple to-do list. However, be cautious with this pattern. Most of the time you are better off with Bloc or Riverpod for state management.

Setup the project

Before we can start with coding, we are going to add some dependencies to the project. Of course, we needevent_bus, but I have also added the uuid package, which we will use in the to-do list example

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  event_bus: ^2.0.0
  uuid: ^3.0.5

Do not forget to install the dependency, running the following command:

flutter pub get

That is it! We can now start with the first example.

A counter example

In this example, I will show you how to make an event-based counter in Flutter. In this event-based example, the first thing we need is an event. The only interesting event is when the counter is incremented:

class CounterIncrementedEvent {}

Now I need to be able to fire this event when the user presses the increment button. We can publish this event on the event bus. In this example, the event bus will be provided as a global variable:

import 'package:event_bus/event_bus.dart';

EventBus eventBus = EventBus();

Since there exists an event bus and an event, the button can now fire the event on the event bus:

floatingActionButton: FloatingActionButton(
  onPressed: () => eventBus.fire(CounterIncrementedEvent()),
  tooltip: 'Increment',
  child: const Icon(Icons.add),
),

Nothing happens when the user press the button, as there are no subscribers to the events yet. The subscriber can choose the events it wants to receive. In this case the CounterIncrementedEvent.

eventBus.on<CounterIncrementedEvent>().listen((event) {
  setState(() {
    _counter++;
  });
});

The StatefulWidget can register to listen to the events in the initState method. This results in the following Widget that counts the times the button is pressed:

class MyCounter extends StatefulWidget {
  const MyCounter({Key? key}) : super(key: key);

  @override
  State<MyCounter> createState() => _MyCounterState();
}

class _MyCounterState extends State<MyCounter> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    eventBus.on<CounterIncrementedEvent>().listen((event) {
      setState(() {
        _counter++;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Event Bus Counter Example"),
      ),
      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: FloatingActionButton(
        onPressed: () => eventBus.fire(CounterIncrementedEvent()),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

A simple to-do list

In this example, I will show you how to make a simple to-do list in Flutter. In this event-based example, the first thing we need is to define the events. Since this will be a simple example, the app only needs two different events:

class TodoListItemCreatedEvent {
  TodoListItem todo;

  TodoListItemCreatedEvent(this.todo);
}

class TodoListItemCheckedEvent {
  String id;

  TodoListItemCheckedEvent(this.id);
}

class TodoListItem {
  String id;
  String text;

  TodoListItem(this.id, this.text);
}

Now instead of publishing the counter increment event, we are going to use the button to open a dialog. In this dialog, the user can put some text and submit that text as his next todo.

showNewTodoDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (BuildContext context) {
      TextEditingController controller = TextEditingController();
      Widget cancelButton = TextButton(child: const Text("Cancel"), onPressed: () => Navigator.pop(context));
      Widget continueButton = TextButton(
        child: const Text("Submit"),
        onPressed: () {
          eventBus.fire(TodoListItemCreatedEvent(TodoListItem(uuid.v1(), controller.text)));
          Navigator.pop(context);
        },
      );
      return AlertDialog(
        title: const Text('Create new Todo'),
        content: TextFormField(
          controller: controller,
          decoration: const InputDecoration(
            label: Text("Item"),
          ),
        ),
        actions: [
          cancelButton,
          continueButton,
        ],
      );
    },
  );
}

As you can see, when the user presses the submit button, an event is going to be published on the event bus. To show these to-do’s there needs to be a Widget that listens to this event.

class MyTodoList extends StatefulWidget {
  const MyTodoList({Key? key}) : super(key: key);

  @override
  State<MyTodoList> createState() => _MyTodoListState();
}

class _MyTodoListState extends State<MyTodoList> {
  List<TodoListItem> todos = [];

  @override
  void initState() {
    super.initState();
    eventBus.on<TodoListItemCreatedEvent>().listen((event) {
      setState(() {
        todos.add(event.todo);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    if (todos.isEmpty) {
      return const Center(child: Text("Add a new todo to get started"));
    }
    return ListView(
      shrinkWrap: true,
      padding: const EdgeInsets.all(20.0),
      children: todos.map((item) => TodoListItemWidget(item: item)).toList(),
    );
  }
}

Now to check the to-do and publish the other event there needs to be a checkbox in the TodoListItemWidget. When the user checks the checkbox, the other event can be published on the event bus.

class TodoListItemWidget extends StatelessWidget {
  final TodoListItem item;

  const TodoListItemWidget({Key? key, required this.item}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(item.text),
      leading: Checkbox(
        value: false,
        onChanged: (bool? value) {
          eventBus.fire(TodoListItemCheckedEvent(item.id));
        },
      ),
    );
  }
}

To remove the to-do’s from the to-do list, MyTodoList can also listen to the newly fired event to remove the items from the to-do list.

class _MyTodoListState extends State<MyTodoList> {
  List<TodoListItem> todos = [];

  @override
  void initState() {
    super.initState();
    eventBus.on<TodoListItemCreatedEvent>().listen((event) {
      setState(() {
        todos.add(event.todo);
      });
    });
    eventBus.on<TodoListItemCheckedEvent>().listen((event) {
      setState(() {
        todos.removeWhere((element) => element.id == event.id);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    if (todos.isEmpty) {
      return const Center(child: Text("Add a new todo to get started"));
    }
    return ListView(
      shrinkWrap: true,
      padding: const EdgeInsets.all(20.0),
      children: todos.map((item) => TodoListItemWidget(item: item)).toList(),
    );
  }
}

A benefit of the events is, is that anyone that has an interest in those events can receive those events and handle them. For example, if there needs to be a counter of the finished to-do’s. That Widget can simply listen to the TodoListItemCheckedEvent and keep track of the finished to-do’s.

class _TotalTodosWidgetState extends State<TotalTodosWidget> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    eventBus.on<TodoListItemCheckedEvent>().listen((event) {
      setState(() {
        _counter++;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
      decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.white),
      alignment: Alignment.center,
      child: Text(
        '$_counter',
        style: const TextStyle(color: Colors.black87),
      ),
    );
  }
}

As said in the introduction always be cautious with those events. Things can become a big mess very quickly. Decoupling Widgets has many benefits. If I decide that I want an import option to import a list of to-do’s, the only thing I need to do is fire events for each of those to-dos. This is nice at the start as it is easy to keep track of all publishers and subscribers. But as your app grows, this might become a nightmare. I am curious about your thoughts on this approach. As always the code is available on Github.

Leave a Reply