DEV Community

Cover image for Flutter Analytics Service using Facade Design Pattern
Abdur Rafay Saleem
Abdur Rafay Saleem

Posted on

Flutter Analytics Service using Facade Design Pattern

Previous Article: Part 3: Implementing PostHog Analytics in Flutter

In this final part, we'll create a unified analytics service that orchestrates multiple analytics clients using the Facade design pattern, demonstrating how to manage complex subsystems through a simplified interface.

Understanding the Facade Pattern

The Facade pattern provides a unified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes the subsystem easier to use. In our analytics architecture:

  • The subsystem consists of multiple analytics clients (Logger, Server, PostHog)
  • Each client has its own complex implementation and dependencies
  • The facade (AnalyticsService) provides a single, simplified interface to use all clients

Why Use a Facade?

  1. Simplified Interface: Clients of the analytics system don't need to know about the complexity of multiple analytics providers
  2. Decoupling: The application code is decoupled from specific analytics implementations
  3. Centralized Control: Analytics configuration and dispatching logic is centralized in one place
  4. Easier Testing: The facade can be mocked or replaced easily for testing

The Analytics Facade Implementation

Our AnalyticsService implements the Facade pattern while also adhering to the AnalyticsClientBase contract:

class AnalyticsService implements AnalyticsClientBase {
  // List of analytics clients managed by the facade
  static const _clients = [
    if (AppConfig.isDebug) LoggerAnalyticsClient(),
    if (AppConfig.isStaging) ServerAnalyticsClient(...) // insert your api service or dio/http etc.
    if (AppConfig.isProd) PosthogAnalyticsClient(),
  ];

  // Singleton instance
  static const instance = AnalyticsService._();
  const AnalyticsService._();

  // Local storage for analytics preferences
  static const _collectUsageStatisticsKey = 'collectUsageStatisticsKey';
  static const _keyValueStorage = KeyValueStorageBase.instance;
Enter fullscreen mode Exit fullscreen mode

It has the following characteristics:

  • Implements the base contract to ensure the unified service contains all methods that the clients have.
  • The _clients list contains all of the client implementations we wanna use. Analytics will be sent to all these for their own handling.
  • We can add/remove the clients based on our debug/dev/prod logic.

Managing Analytics State

The facade handles analytics state management using KeyValueStorageService, which is just a wrapper around SharedPreferences. You can read about it here in a seperate guide. Here's how it handles collection:

  void _setCollectUsageStatistics(bool value) {
    _keyValueStorage.setCommon(_collectUsageStatisticsKey, value);
  }

  bool get collectUsageStatistics {
    return _keyValueStorage.getCommon(_collectUsageStatisticsKey) ?? true;
  }

@override
Future<void> toggleAnalyticsCollection(bool enabled) {
  _setCollectUsageStatistics(enabled);
  return _dispatch(
    (c) => c.toggleAnalyticsCollection(enabled),
  );
}
Enter fullscreen mode Exit fullscreen mode

The Dispatch Method

The key to our facade is the _dispatch method which is used by every overriden method in the service:

Future<void> _dispatch(
  Future<void> Function(AnalyticsClientBase client) work,
) async {
  final worksList = _clients.map(work).toList();
  await Future.wait(worksList);
}
Enter fullscreen mode Exit fullscreen mode

This method:

  1. Takes a function that operates on an analytics client
  2. Applies that function to all registered clients
  3. Runs all operations concurrently
  4. Waits for all operations to complete

Using the Facade

The facade makes analytics calls simple and consistent:

// In application code
void onUserAction() {
  AnalyticsService.instance.trackButtonPressed(
    'submit_button',
    {'screen': '/checkout'},
  );
}
Enter fullscreen mode Exit fullscreen mode

Code

Here is the full code for the service:

Benefits of the Facade Pattern

  1. Single Point of Entry: Application code only needs to interact with one service
  2. Concurrent Execution: Analytics events are dispatched to all clients simultaneously for better performance
  3. Implementation Hiding: Clients of the facade don't need to know about the dispatch mechanism. Each client handles its own implementation details.
  4. Error Isolation: Failures in one client don't affect others
  5. Extensibility: New analytics providers can be added without changing existing code based on build configuration.
  6. Easy Testing: The facade can be easily mocked for testing.

Advanced Facade Features

Adding New Analytics Providers

The facade pattern makes it easy to add new analytics providers:

static const _clients = [
  ...
  // Add new clients here
  FirebaseAnalyticsClient(),
  MixpanelAnalyticsClient(),
];
Enter fullscreen mode Exit fullscreen mode

Conditional Client Activation

The facade can conditionally activate clients based on configuration:

static List<AnalyticsClientBase> _getClients() {
  return [
    if (AppConfig.isDebug) LoggerAnalyticsClient(),
    if (AppConfig.isProd) PosthogAnalyticsClient(),
    if (AppConfig.isStaging) ServerAnalyticsClient(),
  ];
}
Enter fullscreen mode Exit fullscreen mode

Error Handling

The facade can implement centralized error handling. I haven't done this for the sole reason that I don't care about catching errors on logs. If you want to, here's how you do it:

Future<void> _dispatch(
  Future<void> Function(AnalyticsClientBase client) work,
) async {
  try {
    final worksList = _clients.map((client) async {
      try {
        await work(client);
      } catch (e) {
        // Handle individual client errors
        appLogger.error('Analytics client error', e);
      }
    }).toList();
    await Future.wait(worksList);
  } catch (e) {
    // Handle catastrophic errors
    appLogger.error('Analytics dispatch error', e);
  }
}
Enter fullscreen mode Exit fullscreen mode

Event Batching

The facade could implement event batching for better performance. However, Posthog does batch dispatching internally, so I avoided re-inventing the wheel:

class AnalyticsService implements AnalyticsClientBase {
  final _eventQueue = <AnalyticsEvent>[];

  Future<void> _batchDispatch() async {
    if (_eventQueue.isEmpty) return;

    final events = [..._eventQueue];
    _eventQueue.clear();

    await _dispatch((client) => client.trackBatchEvents(events));
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Our analytics architecture demonstrates several key software design principles:

  • How to simplify complex subsystems
  • Abstract base classes for contract enforcement
  • Interface segregation for clean client implementations
  • Maintain flexibility while providing a consistent API
  • Handle cross-cutting concerns like error handling and performance

This robust foundation allows for easy addition of new analytics providers while maintaining clean, maintainable code. The facade pattern proves particularly valuable in analytics implementations where multiple providers need to coexist without increasing complexity for the rest of the application.

Credits

If you like it, please keep following me as I will be posting about many more advanced stuff like PermissionService, LocationService, and NotificationService etc.

Top comments (0)