|
| 1 | +# Mixpanel Flutter SDK Architecture |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The Mixpanel Flutter SDK is a cross-platform analytics plugin that provides a unified Dart API for tracking events and managing user profiles across iOS, Android, and Web platforms. The SDK uses Flutter's platform channel mechanism to communicate between Dart code and native platform implementations. |
| 6 | + |
| 7 | +## Architecture Layers |
| 8 | + |
| 9 | +### 1. Dart API Layer (`lib/mixpanel_flutter.dart`) |
| 10 | + |
| 11 | +The main entry point providing a unified interface across all platforms: |
| 12 | + |
| 13 | +```dart |
| 14 | +class Mixpanel { |
| 15 | + static final MethodChannel _channel = kIsWeb |
| 16 | + ? const MethodChannel('mixpanel_flutter') |
| 17 | + : const MethodChannel('mixpanel_flutter', StandardMethodCodec(MixpanelMessageCodec())); |
| 18 | + |
| 19 | + // Core tracking method |
| 20 | + Future<void> track(String eventName, {Map<String, dynamic>? properties}) async { |
| 21 | + await _channel.invokeMethod<void>('track', { |
| 22 | + 'eventName': eventName, |
| 23 | + 'properties': _MixpanelHelper.ensureSerializableProperties(properties) |
| 24 | + }); |
| 25 | + } |
| 26 | +} |
| 27 | +``` |
| 28 | + |
| 29 | +Key components: |
| 30 | +- **Mixpanel**: Main singleton class for event tracking |
| 31 | +- **People**: User profile management (accessed via `mixpanel.getPeople()`) |
| 32 | +- **MixpanelGroup**: Group analytics management (accessed via `mixpanel.getGroup()`) |
| 33 | + |
| 34 | +### 2. Platform Channel & Serialization |
| 35 | + |
| 36 | +#### Custom Message Codec (`lib/codec/mixpanel_message_codec.dart`) |
| 37 | + |
| 38 | +Handles serialization of complex types between Dart and native platforms: |
| 39 | + |
| 40 | +```dart |
| 41 | +class MixpanelMessageCodec extends StandardMessageCodec { |
| 42 | + static const int _kDateTime = 128; |
| 43 | + static const int _kUri = 129; |
| 44 | + |
| 45 | + @override |
| 46 | + void writeValue(WriteBuffer buffer, dynamic value) { |
| 47 | + if (value is DateTime) { |
| 48 | + buffer.putUint8(_kDateTime); |
| 49 | + buffer.putInt64(value.millisecondsSinceEpoch); |
| 50 | + } else if (value is Uri) { |
| 51 | + buffer.putUint8(_kUri); |
| 52 | + final bytes = utf8.encoder.convert(value.toString()); |
| 53 | + writeSize(buffer, bytes.length); |
| 54 | + buffer.putUint8List(bytes); |
| 55 | + } else { |
| 56 | + super.writeValue(buffer, value); |
| 57 | + } |
| 58 | + } |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +### 3. Platform Implementations |
| 63 | + |
| 64 | +#### Android Implementation |
| 65 | + |
| 66 | +**MixpanelFlutterPlugin.java**: |
| 67 | +```java |
| 68 | +public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { |
| 69 | + switch (call.method) { |
| 70 | + case "track": |
| 71 | + handleTrack(call, result); |
| 72 | + break; |
| 73 | + // ... other methods |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +private void handleTrack(MethodCall call, Result result) { |
| 78 | + String eventName = call.argument("eventName"); |
| 79 | + Map<String, Object> mapProperties = call.<HashMap<String, Object>>argument("properties"); |
| 80 | + JSONObject properties = new JSONObject(mapProperties == null ? EMPTY_HASHMAP : mapProperties); |
| 81 | + properties = MixpanelFlutterHelper.getMergedProperties(properties, mixpanelProperties); |
| 82 | + mixpanel.track(eventName, properties); |
| 83 | + result.success(null); |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +**MixpanelMessageCodec.java**: Mirrors Dart codec for Date/URI handling |
| 88 | + |
| 89 | +#### iOS Implementation |
| 90 | + |
| 91 | +**SwiftMixpanelFlutterPlugin.swift**: |
| 92 | +```swift |
| 93 | +private func handleTrack(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
| 94 | + let arguments = call.arguments as? [String: Any] ?? [String: Any]() |
| 95 | + let event = arguments["eventName"] as! String |
| 96 | + let properties = arguments["properties"] as? [String: Any] |
| 97 | + let mpProperties = MixpanelTypeHandler.mixpanelProperties(properties: properties, mixpanelProperties: mixpanelProperties) |
| 98 | + instance?.track(event: event, properties: mpProperties) |
| 99 | + result(nil) |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +**Custom codec implementation**: |
| 104 | +```swift |
| 105 | +public class MixpanelReader : FlutterStandardReader { |
| 106 | + public override func readValue(ofType type: UInt8) -> Any? { |
| 107 | + switch type { |
| 108 | + case DATE_TIME: |
| 109 | + var value: Int64 = 0 |
| 110 | + readBytes(&value, length: 8) |
| 111 | + return Date(timeIntervalSince1970: TimeInterval(value / 1000)) |
| 112 | + case URI: |
| 113 | + let urlString = readUTF8() |
| 114 | + return URL(string: urlString) |
| 115 | + default: |
| 116 | + return super.readValue(ofType: type) |
| 117 | + } |
| 118 | + } |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +#### Web Implementation |
| 123 | + |
| 124 | +**mixpanel_flutter_web.dart**: |
| 125 | +```dart |
| 126 | +void handleTrack(MethodCall call) { |
| 127 | + Map<Object?, Object?> args = call.arguments as Map<Object?, Object?>; |
| 128 | + String eventName = args['eventName'] as String; |
| 129 | + dynamic properties = args['properties']; |
| 130 | + Map<String, dynamic> props = { |
| 131 | + ..._mixpanelProperties, |
| 132 | + ...(properties ?? {}) |
| 133 | + }; |
| 134 | + track(eventName, safeJsify(props)); |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +**Type conversion for web**: |
| 139 | +```dart |
| 140 | +JSAny? safeJsify(dynamic value) { |
| 141 | + if (value == null) { |
| 142 | + return null; |
| 143 | + } else if (value is Map) { |
| 144 | + return value.jsify(); |
| 145 | + } else if (value is DateTime) { |
| 146 | + return value.jsify(); |
| 147 | + } // ... other type conversions |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +## Event Flow: track() Method |
| 152 | + |
| 153 | +### 1. Initialization Flow |
| 154 | + |
| 155 | +```dart |
| 156 | +// Dart layer |
| 157 | +final mixpanel = await Mixpanel.init("YOUR_PROJECT_TOKEN", |
| 158 | + optOutTrackingDefault: false, |
| 159 | + trackAutomaticEvents: true); |
| 160 | +
|
| 161 | +// Platform channel invocation |
| 162 | +await _channel.invokeMethod<void>('initialize', { |
| 163 | + 'token': token, |
| 164 | + 'optOutTrackingDefault': optOutTrackingDefault, |
| 165 | + 'trackAutomaticEvents': trackAutomaticEvents, |
| 166 | + 'mixpanelProperties': _mixpanelProperties, // {$lib_version: '2.4.4', mp_lib: 'flutter'} |
| 167 | + 'superProperties': superProperties, |
| 168 | + 'config': config |
| 169 | +}); |
| 170 | +``` |
| 171 | + |
| 172 | +### 2. Track Event Flow |
| 173 | + |
| 174 | +``` |
| 175 | +Dart Layer (mixpanel.track("Event Name", properties: {...})) |
| 176 | + ↓ |
| 177 | +Platform Channel (invokeMethod('track', {eventName, properties})) |
| 178 | + ↓ |
| 179 | +Native Platform Handler |
| 180 | + ├── Android: MixpanelFlutterPlugin.handleTrack() |
| 181 | + ├── iOS: SwiftMixpanelFlutterPlugin.handleTrack() |
| 182 | + └── Web: MixpanelFlutterPlugin.handleTrack() |
| 183 | + ↓ |
| 184 | +Property Processing |
| 185 | + ├── Merge with library properties ($lib_version, mp_lib) |
| 186 | + ├── Type conversion (Date, URI, etc.) |
| 187 | + └── Platform-specific formatting |
| 188 | + ↓ |
| 189 | +Native SDK Call |
| 190 | + ├── Android: mixpanel.track(eventName, JSONObject) |
| 191 | + ├── iOS: instance?.track(event:properties:) |
| 192 | + └── Web: track(eventName, jsProperties) |
| 193 | + ↓ |
| 194 | +Mixpanel Servers |
| 195 | +``` |
| 196 | + |
| 197 | +### 3. Data Serialization Details |
| 198 | + |
| 199 | +#### Native Platforms (Android/iOS) |
| 200 | +- Custom codec handles DateTime and Uri objects |
| 201 | +- DateTime: Serialized as milliseconds since epoch (int64) |
| 202 | +- Uri: Serialized as UTF-8 encoded string |
| 203 | +- Complex objects (Maps, Lists) are recursively converted |
| 204 | + |
| 205 | +#### Web Platform |
| 206 | +- No custom codec needed - uses StandardMethodCodec |
| 207 | +- `safeJsify()` converts Dart types to JavaScript-compatible types |
| 208 | +- DateTime objects converted using `.jsify()` |
| 209 | +- Direct JS interop with Mixpanel JavaScript library |
| 210 | + |
| 211 | +## Key Design Decisions |
| 212 | + |
| 213 | +1. **Platform Channel Architecture**: Enables code reuse while allowing platform-specific optimizations |
| 214 | + |
| 215 | +2. **Custom Message Codec**: Ensures DateTime and Uri objects are properly serialized across platform boundaries |
| 216 | + |
| 217 | +3. **Library Properties**: Automatically injected metadata (`$lib_version`, `mp_lib`) helps with analytics segmentation |
| 218 | + |
| 219 | +4. **Async API**: All methods return Futures for consistency, even if underlying native calls are synchronous |
| 220 | + |
| 221 | +5. **Type Safety**: Platform-specific type handlers ensure proper conversion between Dart and native types |
| 222 | + |
| 223 | +6. **Web Implementation**: Uses JS interop instead of platform channels for better performance and smaller bundle size |
| 224 | + |
| 225 | +## Platform Dependencies |
| 226 | + |
| 227 | +- **Android**: Mixpanel Android SDK v8.0.3 |
| 228 | +- **iOS**: Mixpanel-swift v5.0.0 |
| 229 | +- **Web**: Mixpanel JavaScript library (loaded from CDN) |
| 230 | + |
| 231 | +## Example Usage |
| 232 | + |
| 233 | +```dart |
| 234 | +// Initialize |
| 235 | +final mixpanel = await Mixpanel.init("YOUR_PROJECT_TOKEN", |
| 236 | + trackAutomaticEvents: true); |
| 237 | +
|
| 238 | +// Track simple event |
| 239 | +await mixpanel.track("Button Clicked"); |
| 240 | +
|
| 241 | +// Track with properties |
| 242 | +await mixpanel.track("Purchase", properties: { |
| 243 | + "product": "Premium Subscription", |
| 244 | + "price": 9.99, |
| 245 | + "currency": "USD", |
| 246 | + "timestamp": DateTime.now(), |
| 247 | + "store_url": Uri.parse("https://store.example.com") |
| 248 | +}); |
| 249 | +
|
| 250 | +// Identify user |
| 251 | +await mixpanel.identify("user123"); |
| 252 | +
|
| 253 | +// Set user profile properties |
| 254 | +mixpanel.getPeople().set("name", "John Doe"); |
| 255 | +mixpanel.getPeople().set("email", "[email protected]"); |
| 256 | +
|
| 257 | +// Group analytics |
| 258 | +mixpanel.setGroup("company", "Acme Corp"); |
| 259 | +final group = mixpanel.getGroup("company", "Acme Corp"); |
| 260 | +group.set("plan", "Enterprise"); |
| 261 | +``` |
| 262 | + |
| 263 | +## Architecture Benefits |
| 264 | + |
| 265 | +1. **Unified API**: Developers write once, run everywhere |
| 266 | +2. **Type Safety**: Strong typing prevents runtime errors |
| 267 | +3. **Performance**: Native SDK usage ensures optimal performance per platform |
| 268 | +4. **Maintainability**: Clear separation of concerns between layers |
| 269 | +5. **Extensibility**: Easy to add new methods or platforms |
| 270 | + |
| 271 | +## Future Considerations |
| 272 | + |
| 273 | +1. **Null Safety**: The SDK fully supports Dart null safety |
| 274 | +2. **Platform Expansion**: Architecture supports adding new platforms (e.g., Windows, Linux) |
| 275 | +3. **Feature Parity**: Platform implementations should maintain feature parity where possible |
| 276 | +4. **Testing**: Platform-specific functionality should be tested through the example app |
0 commit comments