Skip to content

Commit e850b9a

Browse files
Defer MixpanelFlutterPlugin registration and initialization to prevent ANRs (#191)
* make onAttachedEngine ligtweight and lazily create MethodChannel * update gitignore * update README with DeepWiki badge * nullify context in onDetachedFromEngine * tweak README * use proper generic ArrayList<>() in MixpanelFlutterHelper.java * extract methodchannel setup to helper * document and enhance safeJsify * ensure all JavaScript interop conversions go through the safeJsify function * copilot suggestions * use debugPrint instead of print
1 parent 6deedc8 commit e850b9a

File tree

5 files changed

+117
-35
lines changed

5 files changed

+117
-35
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
.pub/
66
.idea
77
build/
8-
.cxx
8+
.cxx
9+
CLAUDE.md

README.md

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
2-
3-
41
<div align="center" style="text-align: center">
52
<img src="https://user-images.githubusercontent.com/71290498/231855731-2d3774c3-dc41-4595-abfb-9c49f5f84103.png" alt="Mixpanel Flutter SDK" height="150"/>
63
</div>
74

8-
95
# Table of Contents
106

117
<!-- MarkdownTOC -->
8+
129
- [Introduction](#introduction)
1310
- [Quick Start Guide](#quick-start-guide)
14-
- [Install Mixpanel](#1-install-mixpanel)
15-
- [Initialize Mixpanel](#2-initialize-mixpanel)
16-
- [Send Data](#3-send-data)
17-
- [Check for Success](#4-check-for-success)
11+
- [Install Mixpanel](#1-install-mixpanel)
12+
- [Initialize Mixpanel](#2-initialize-mixpanel)
13+
- [Send Data](#3-send-data)
14+
- [Check for Success](#4-check-for-success)
1815
- [I want to know more!](#i-want-to-know-more)
1916

2017
<!-- /MarkdownTOC -->
2118

22-
2319
# Introduction
20+
2421
Welcome to the official Mixpanel Flutter SDK.
2522
The Mixpanel Flutter SDK is an open-source project, and we'd love to see your contributions!
2623
We'd also love for you to come and work with us! Check out **[Jobs](https://mixpanel.com/jobs/#openings)** for details
@@ -30,32 +27,47 @@ We'd also love for you to come and work with us! Check out **[Jobs](https://mixp
3027
Check out our **[official documentation](https://developer.mixpanel.com/docs/flutter)** for more in depth information on installing and using Mixpanel on Flutter.
3128

3229
## 1. Install Mixpanel
30+
3331
### Prerequisites
32+
3433
- [Setup development environment for Flutter](https://flutter.dev/docs/get-started/install)
34+
3535
### Steps
36-
1. Depend on it \
37-
Add this to your package's pubspec.yaml file:
36+
37+
1. Depend on it \
38+
Add this to your package's pubspec.yaml file:
39+
3840
```
3941
dependencies:
4042
mixpanel_flutter: ^1.x.x # set this to your desired version
4143
```
44+
4245
2. Install it \
43-
You can install packages from the command line:
46+
You can install packages from the command line:
47+
4448
```
4549
$ flutter pub get
4650
```
51+
4752
3. Import it \
48-
Now in your Dart code, you can use:
53+
Now in your Dart code, you can use:
54+
4955
```
5056
import 'package:mixpanel_flutter/mixpanel_flutter.dart';
5157
```
58+
5259
#### Flutter Web Support
53-
Please add the following snippet to your `web/index.html` inside `<head></head>` in your Flutter project.
60+
61+
Please add the following snippet to your `web/index.html` inside `<head></head>` in your Flutter project.
62+
5463
```
5564
<script src="./assets/packages/mixpanel_flutter/assets/mixpanel.js"></script>
5665
```
66+
5767
## 2. Initialize Mixpanel
68+
5869
To start tracking with the SDK you must first initialize with your project token. To initialize the SDK, first add `import 'package:mixpanel_flutter/mixpanel_flutter.dart';` and call `Mixpanel.init(token, trackAutomaticEvents);` with your project token and automatic events setting as it's arguments. You can find your token in [project settings](https://mixpanel.com/settings/project).
70+
5971
```dart
6072
import 'package:mixpanel_flutter/mixpanel_flutter.dart';
6173
...
@@ -73,43 +85,49 @@ class _YourClassState extends State<YourClass> {
7385
}
7486
...
7587
```
88+
7689
Once you've called this method once, you can access `mixpanel` throughout the rest of your application.
7790

7891
## 3. Send Data
92+
7993
Once you've initialized the SDK, Mixpanel will <a href="https://mixpanel.com/help/questions/articles/which-common-mobile-events-can-mixpanel-collect-on-my-behalf-automatically" target="_blank">automatically collect common mobile events</a>. You can enable/disable automatic collection through your project settings.
8094
With the `mixpanel` object created in [the last step](#2-initialize-mixpanel) a call to `track` is all you need to send additional events to Mixpanel.
95+
8196
```dart
8297
// Track with event-name
8398
mixpanel.track('Sent Message');
8499
// Track with event-name and property
85100
mixpanel.track('Plan Selected', properties: {'Plan': 'Premium'});
86101
```
87-
You're done! You've successfully integrated the Mixpanel Flutter SDK into your app. To stay up to speed on important SDK releases and updates, star or watch our repository on [Github](https://github.com/mixpanel/mixpanel-flutter).
102+
103+
You're done! You've successfully integrated the Mixpanel Flutter SDK into your app. To stay up to speed on important SDK releases and updates, star or watch our repository on [GitHub](https://github.com/mixpanel/mixpanel-flutter).
104+
88105
## 4. Check for Success
89-
[Open up Events in Mixpanel](https://mixpanel.com/report/events) to view incoming events.
106+
107+
[Open up Events in Mixpanel](https://mixpanel.com/report/events) to view incoming events.
90108
Once data hits our API, it generally takes ~60 seconds for it to be processed, stored, and queryable in your project.
91109

92-
👋 👋 Tell us about the Mixpanel developer experience! [https://www.mixpanel.com/devnps](https://www.mixpanel.com/devnps) 👍 👎
110+
👋 👋 Tell us about the Mixpanel developer experience! [https://www.mixpanel.com/devnps](https://www.mixpanel.com/devnps) 👍 👎
93111

94112
# FAQ
95113

96114
**I want to stop tracking an event/event property in Mixpanel. Is that possible?**
97115

98-
Yes, in Lexicon, you can intercept and drop incoming events or properties. Mixpanel won’t store any new data for the event or property you select to drop. [See this article for more information](https://help.mixpanel.com/hc/en-us/articles/360001307806#dropping-events-and-properties).
116+
Yes, in Lexicon, you can intercept and drop incoming events or properties. Mixpanel won’t store any new data for the event or property you select to drop. [See this article for more information](https://help.mixpanel.com/hc/en-us/articles/360001307806#dropping-events-and-properties).
99117

100118
**I have a test user I would like to opt out of tracking. How do I do that?**
101119

102-
Mixpanel’s client-side tracking library contains the [optOutTracking()](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/optOutTracking.html) method, which will set the user’s local opt-out state to “true” and will prevent data from being sent from a user’s device. More detailed instructions can be found in the section, [Opting users out of tracking](https://developer.mixpanel.com/docs/flutter#opting-users-out-of-tracking).
120+
Mixpanel’s client-side tracking library contains the [optOutTracking()](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/optOutTracking.html) method, which will set the user’s local opt-out state to “true” and will prevent data from being sent from a user’s device. More detailed instructions can be found in the section, [Opting users out of tracking](https://developer.mixpanel.com/docs/flutter#opting-users-out-of-tracking).
103121

104122
**Why aren't my events showing up?**
105123

106-
First, make sure your test device has internet access. To preserve battery life and customer bandwidth, the Mixpanel library doesn't send the events you record immediately. Instead, it sends batches to the Mixpanel servers every 60 seconds while your application is running, as well as when the application transitions to the background. You can call [flush()](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/flush.html) manually if you want to force a flush at a particular moment.
124+
First, make sure your test device has internet access. To preserve battery life and customer bandwidth, the Mixpanel library doesn't send the events you record immediately. Instead, it sends batches to the Mixpanel servers every 60 seconds while your application is running, as well as when the application transitions to the background. You can call [flush()](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/flush.html) manually if you want to force a flush at a particular moment.
107125

108126
```
109127
mixpanel.flush();
110128
```
111129

112-
If your events are still not showing up after 60 seconds, check if you have opted out of tracking. You can also enable Mixpanel debugging and logging, it allows you to see the debug output from the Mixpanel library. To enable it, call [setLoggingEnabled](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/setLoggingEnabled.html) to true, then run your iOS project with Xcode or android project with Android Studio. The logs should be available in the console.
130+
If your events are still not showing up after 60 seconds, check if you have opted out of tracking. You can also enable Mixpanel debugging and logging, it allows you to see the debug output from the Mixpanel library. To enable it, call [setLoggingEnabled](https://mixpanel.github.io/mixpanel-flutter/mixpanel_flutter/Mixpanel/setLoggingEnabled.html) to true, then run your iOS project with Xcode or android project with Android Studio. The logs should be available in the console.
113131

114132
```
115133
mixpanel.setLoggingEnabled(true);
@@ -121,12 +139,15 @@ No, Mixpanel does not use IDFA so it does not require user permission through th
121139

122140
**If I use Mixpanel, how do I answer app privacy questions for the App Store?**
123141

124-
Please refer to our [Apple App Developer Privacy Guidance](https://mixpanel.com/legal/app-store-privacy-details/)
142+
Please refer to our [Apple App Developer Privacy Guidance](https://mixpanel.com/legal/app-store-privacy-details/)
125143

126144
# I want to know more!
127145

128146
No worries, here are some links that you will find useful:
129-
* **[Sample app](https://github.com/mixpanel/mixpanel-flutter/tree/main/example)**
130-
* **[Full API Reference](https://developer.mixpanel.com/docs/flutter)**
147+
148+
- **[Sample app](https://github.com/mixpanel/mixpanel-flutter/tree/main/example)**
149+
- **[Full API Reference](https://developer.mixpanel.com/docs/flutter)**
150+
151+
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/mixpanel/mixpanel-flutter)
131152

132153
Have any questions? Reach out to Mixpanel [Support](https://help.mixpanel.com/hc/en-us/requests/new) to speak to someone smart, quickly.

android/src/main/java/com/mixpanel/mixpanel_flutter/MixpanelFlutterHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ static public Map<String, Object> toMap(JSONObject object) throws JSONException
3333
}
3434

3535
static public List<Object> toList(JSONArray array) throws JSONException {
36-
List<Object> list = new ArrayList();
36+
List<Object> list = new ArrayList<>();
3737
for (int i = 0; i < array.length(); i++) {
3838
list.add(fromJson(array.get(i)));
3939
}

android/src/main/java/com/mixpanel/mixpanel_flutter/MixpanelFlutterPlugin.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class MixpanelFlutterPlugin implements FlutterPlugin, MethodCallHandler {
3131
private MixpanelAPI mixpanel;
3232
private Context context;
3333
private JSONObject mixpanelProperties;
34+
private FlutterPluginBinding flutterPluginBinding;
3435

3536
private static final Map<String, Object> EMPTY_HASHMAP = new HashMap<>();
3637

@@ -43,10 +44,9 @@ public MixpanelFlutterPlugin(Context context) {
4344

4445
@Override
4546
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
46-
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "mixpanel_flutter",
47-
new StandardMethodCodec(new MixpanelMessageCodec()));
48-
context = flutterPluginBinding.getApplicationContext();
49-
channel.setMethodCallHandler(this);
47+
// Store references for lazy initialization to avoid ANR during plugin registration
48+
this.flutterPluginBinding = flutterPluginBinding;
49+
this.context = flutterPluginBinding.getApplicationContext();
5050
}
5151

5252
@Override
@@ -180,7 +180,18 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
180180
}
181181
}
182182

183+
private void initializeMethodChannel() {
184+
if (channel == null && flutterPluginBinding != null) {
185+
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "mixpanel_flutter",
186+
new StandardMethodCodec(new MixpanelMessageCodec()));
187+
channel.setMethodCallHandler(this);
188+
}
189+
}
190+
183191
private void handleInitialize(MethodCall call, Result result) {
192+
// Lazy initialization of MethodChannel to avoid ANR
193+
initializeMethodChannel();
194+
184195
final String token = call.argument("token");
185196
if (token == null) {
186197
throw new RuntimeException("Your Mixpanel Token was not set");
@@ -519,6 +530,13 @@ private void handleGroupUnionProperty(MethodCall call, Result result) {
519530

520531
@Override
521532
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
522-
channel.setMethodCallHandler(null);
533+
if (channel != null) {
534+
channel.setMethodCallHandler(null);
535+
channel = null;
536+
}
537+
flutterPluginBinding = null;
538+
context = null;
539+
mixpanel = null;
540+
mixpanelProperties = null;
523541
}
524542
}

lib/mixpanel_flutter_web.dart

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,52 @@
11
import 'dart:async';
22
import 'dart:js_interop';
33

4+
import 'package:flutter/foundation.dart';
45
import 'package:flutter/services.dart';
56
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
67
import 'package:mixpanel_flutter/web/mixpanel_js_bindings.dart';
78

9+
/// Safely converts Dart values to JavaScript-compatible types for web interop.
10+
///
11+
/// This function handles the conversion of various Dart types to their JavaScript
12+
/// equivalents using the appropriate JS interop methods.
13+
///
14+
/// **Accepted input types:**
15+
/// - `JSAny` - Returned as-is to avoid double conversion
16+
/// - `Map` - Converted using `.jsify()` to a JavaScript object
17+
/// - `List` - Converted using `.jsify()` to a JavaScript array
18+
/// - `DateTime` - Converted using `.jsify()` to a JavaScript Date object
19+
/// - `bool` - Converted using `.toJS` to a JavaScript boolean
20+
/// - `num` (int/double) - Converted using `.toJS` to a JavaScript number
21+
/// - `String` - Converted using `.toJS` to a JavaScript string
22+
/// - Any other type - Logs a warning and returns null to prevent JS interop issues
23+
///
24+
/// **Return value:**
25+
/// Returns a `JSAny?` which represents the JavaScript-compatible value.
26+
/// The return type is nullable to handle cases where the input cannot be
27+
/// converted or is already null.
28+
///
29+
/// **Null handling:**
30+
/// - If the input value is `null`, it is explicitly checked and returned immediately
31+
/// - The function is null-safe and will not throw on null inputs
32+
///
33+
/// **Example usage:**
34+
/// ```dart
35+
/// Convert a Map to JavaScript object
36+
/// var jsObj = safeJsify({'key': 'value', 'count': 42});
37+
///
38+
/// Convert a List to JavaScript array
39+
/// var jsArray = safeJsify([1, 2, 3, 'four']);
40+
///
41+
/// Handles null gracefully
42+
/// var jsNull = safeJsify(null); // Returns null
43+
/// ```
844
JSAny? safeJsify(dynamic value) {
9-
if (value is Map) {
45+
if (value == null) {
46+
return null;
47+
} else if (value is JSAny) {
48+
return value;
49+
} else if (value is Map) {
1050
return value.jsify();
1151
} else if (value is List) {
1252
return value.jsify();
@@ -19,7 +59,9 @@ JSAny? safeJsify(dynamic value) {
1959
} else if (value is String) {
2060
return value.toJS;
2161
} else {
22-
return value;
62+
debugPrint('[Mixpanel] Warning: Unsupported type for JS conversion: ${value.runtimeType}. '
63+
'Value will be ignored. Supported types are: Map, List, DateTime, bool, num, String, JSAny, and null.');
64+
return null;
2365
}
2466
}
2567

@@ -156,7 +198,7 @@ class MixpanelFlutterPlugin {
156198
Map<Object?, Object?> args = call.arguments as Map<Object?, Object?>;
157199
String token = args['token'] as String;
158200
dynamic config = args['config'];
159-
init(token, safeJsify(config) ?? <String, dynamic>{}.jsify());
201+
init(token, safeJsify(config ?? <String, dynamic>{}));
160202
}
161203

162204
void handleSetServerURL(MethodCall call) {
@@ -309,7 +351,7 @@ class MixpanelFlutterPlugin {
309351
Map<Object?, Object?> args = call.arguments as Map<Object?, Object?>;
310352
dynamic properties = args['properties'];
311353
double amount = args['amount'] as double;
312-
people_track_charge(amount, safeJsify(properties) ?? <String, dynamic>{}.jsify());
354+
people_track_charge(amount, safeJsify(properties ?? <String, dynamic>{}));
313355
}
314356

315357
void handleClearCharge() {

0 commit comments

Comments
 (0)