Published at

Decoupling Services in Riverpod using Event Providers

Decoupling Services in Riverpod using Event Providers

Decoupling Riverpod systems using event providers and object wrappers

Authors
  • Name
    Joachim Bülow
    Twitter
  • Cofounder and CTO at Doubble
Sharing is caring!
Table of Contents

Decoupling Services in Riverpod using Event Providers

One common challenge in Flutter applications using Riverpod is managing service coupling.

Today, I’ll share a simple pattern I use to decouple services using event providers and object wrappers.

The Problem

When building complex Flutter applications, services often become tightly coupled as requirements are squeezed in over time. Usually this happens when we want to account for side effects - either one provider invalidates other providers when they perform actions, or other providers need to watch other providers’ state to react to the state change.

The Solution: Event Providers

One solution is to create event providers that act as a communication channel between services. Here’s how it works:

enum ChatEvent {
  MessageSent,
}

/// Provider to handle chat events 
@riverpod
class ChatEvents extends _$ChatEvents {
  @override
  ObjectWrapper<ChatEvent>? build() {
    return null;
  }

  messageSent() {
    state = ChatEvent.MessageSent.toObject;
  }
}

/// Keeps simple data types always reactive when equal event types are emitted
class ObjectWrapper<T> {
  final T value;

  ObjectWrapper(this.value);
}

/// Provider which handles sending messages
@riverpod
class MessageProvider(ChatId chatId) {
    ...
    Future<void> sendMessage(String mes) {
        ...
        final res = await repository.send(mes, chatId).guard();

        if (!res.hasError) {
            ref.read(chatEventsProvider.notifier).messageSent();
        }
        ...
    }
    ...
}

Then in your UI layer, or simply in other providers, you can listen to these events:

... within a widget

ref.listen(chatEventsProvider, (prev, next) async {
  if (!mounted || next == null) {
    return;
  }

  switch (next.value) {
    case ChatEvent.MessageSent:
      // Scroll to top of screen to highlight sent message
      scrollToTop();
      break;
    default:
      // handle other chat UI events here
  }
});

Why This Works

  1. Decoupling: Services and widgets don’t need to know about each other. In our example, we just know a message was sent. This gives us semantic freedom as well - as we do not rely on specific implicit state changes to react correctly.

  2. Reactivity: React to multiple of the same events being emitted by leveraging Riverpod’s object reference comparisons

Conclusion

This pattern is similar to how RxJS works in web development, but adapted for Flutter and Riverpod. It’s a simple yet powerful way to keep your services decoupled while maintaining type safety and reactivity.

Sharing is caring!