Flutter – How to deal with global variables

The last blog post shows you how to use the event bus to publish and subscribe to events. In this example, there is a global variable for the event bus to simplify the example. In this blog post, I will show you how to deal with the global variable. First, I am going to show you how to pass down the information with an Inherited Widget. Secondly, I am going to show you how to do this with the Provider package. Finally, I am going to show another approach by making it immutable and putting it in a small package.

The Inherited Widget

The starting point of this example is the simple todo list from the last blog post about event based Flutter applications. In this example, I use a global variable for the event bus. In this example, I will show you how to pass the event bus with an Inherited Widget. This is the first sentence of the documentation:

Base class for widgets that efficiently propagate information down the tree.

This is exactly what I want to do, so let’s get started by creating the Inherited Widget.

class EventBusProvider extends InheritedWidget {
  const EventBusProvider({Key? key, required this.eventBus, required Widget child}) : super(key: key, child: child);

  final EventBus eventBus;

  static EventBus of(BuildContext context) {
    final EventBusProvider? result = context.dependOnInheritedWidgetOfExactType<EventBusProvider>();
    assert(result != null, 'No EventBus found in context');
    return result!.eventBus;
  }

  @override
  bool updateShouldNotify(EventBusProvider oldWidget) => eventBus != oldWidget.eventBus;
}

The most important part is the of method. Here we search for the event bus in the context and return it when it is found. You can also specify that Widget Tree should be rebuild when the value changes. In my case this should happen when the event bus changes, as otherwise the Widgets keep listening to the old event bus.

This EventBusProvider can be used by wrapping it around the Widgets in which the event bus is needed. Ofcourse, I need to supply the event bus to this Widgets, so that the children can access that event bus.

class App extends StatelessWidget {

  const App({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return EventBusProvider(
      eventBus: EventBus(),
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.green,
        ),
        home: const TodoList(),
      ),
    );
  }
}

To publish events on the event bus, you can read the event bus of the context.

Widget continueButton = TextButton(
  child: const Text("Submit"),
  onPressed: () {
    EventBusProvider.of(context).fire(TodoListItemCreatedEvent(TodoListItem(uuid.v1(), controller.text)));
    Navigator.pop(context);
  },
);

A way to make you code a bit more clean, is to write an extension method on the BuildContext, such that you can access the event bus by calling context.eventBus.

extension EventBusProviderExtension on BuildContext {
  EventBus get eventBus => EventBusProvider.of(this);
}

Widget continueButton = TextButton(
  child: const Text("Submit"),
  onPressed: () {
    eventBus.context.fire(TodoListItemCreatedEvent(TodoListItem(uuid.v1(), controller.text)));
    Navigator.pop(context);
  },
);

In the Widgets that listen to the event bus, I need to wrap the call with a Future delayed, to make sure the context is fully loaded.

@override
void initState() {
  super.initState();
  Future.delayed(Duration.zero, () {
    context.eventBus.on<TodoListItemCheckedEvent>().listen((event) {
      setState(() {
        _counter++;
      });
    });
  });
}

Flutter Provider

The Inherited Widget can be quite complex for what you want to achieve. Luckily there is a package that simplifies this approach, namely the Provider package.

A wrapper around InheritedWidget to make them easier to use and more reusable.

This sounds great, so let’s take a quick look. The installation is simple as always, just add the provider package to the dependencies. The other depenendies are necessary for the todo list example.

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

Now let’s see how to use the provider in the same application. Instead of wrapping the app in the Inherited Widget, this is going to become a Provider. The Provider provides the event bus for all the children in the Widget Tree. The easiest way to provide the value is to implement the create method and return a new event bus.

@override
Widget build(BuildContext context) {
  return Provider(
    create: (_) => EventBus(),
    child: MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: const TodoList(),
    ),
  );

Now to access the event bus, the widget that needs the event bus, can read the event bus from the context and access the event bus. As you can see this also solves the problem with the timing of access during the initState method.

context.read<EventBus>().on<TodoListItemCheckedEvent>().listen((event) {
  setState(() {
    _counter++;
  });
}

To publish new events on the event bus, I can apply the same trick to read the event bus that is provided by the provider.

context.read<EventBus>().fire(TodoListItemCheckedEvent(item.id));

That is it, as you can see this option is way more simple than the InheritedWidget.

Global variabls

Lastly, global variables are not always bad. But if you are going to use global variables, always try to make them immutable. This way you make sure someone does not accidently override the current event bus and breaks everything. I try to put them in the same file, to make the dependencies more clear.

import 'package:event_bus/event_bus.dart';
import 'package:uuid/uuid.dart';

const Uuid uuid = Uuid();
final EventBus eventBus = EventBus();

Thank you reading this blog post! If you have any suggestions for those global variables, post them in the comments. As always you can find the code on Github.

Leave a Reply