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?
- Simplified Interface: Clients of the analytics system don't need to know about the complexity of multiple analytics providers
- Decoupling: The application code is decoupled from specific analytics implementations
- Centralized Control: Analytics configuration and dispatching logic is centralized in one place
- 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;
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),
);
}
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);
}
This method:
- Takes a function that operates on an analytics client
- Applies that function to all registered clients
- Runs all operations concurrently
- 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'},
);
}
Code
Here is the full code for the service:
Benefits of the Facade Pattern
- Single Point of Entry: Application code only needs to interact with one service
- Concurrent Execution: Analytics events are dispatched to all clients simultaneously for better performance
- Implementation Hiding: Clients of the facade don't need to know about the dispatch mechanism. Each client handles its own implementation details.
- Error Isolation: Failures in one client don't affect others
- Extensibility: New analytics providers can be added without changing existing code based on build configuration.
- 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(),
];
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(),
];
}
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);
}
}
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));
}
}
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)