Skip to content

Commit a1cc38f

Browse files
committed
Add event listener lifecycle management hooks
Introduces startEventListening and stopEventListening hooks to EventEmitter and implements them in Menu, MenuItem, and TrayIcon for proper native event listener registration and cleanup. This improves resource management and ensures native listeners are only active when needed. Also adds the 'meta' package dependency for @Protected annotations and removes unused imports from the tray_icon_example.
1 parent 854436d commit a1cc38f

5 files changed

Lines changed: 175 additions & 52 deletions

File tree

examples/tray_icon_example/lib/main.dart

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import 'package:flutter/material.dart' hide Image;
2-
import 'package:flutter/services.dart';
32
import 'package:nativeapi/nativeapi.dart';
4-
import 'dart:convert';
5-
6-
import 'package:path/path.dart' as path;
7-
import 'dart:io';
83

94
class TrayIconData {
105
final int id;

packages/nativeapi/lib/src/foundation/event_emitter.dart

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
11
import 'dart:async';
22

3+
import 'package:meta/meta.dart';
4+
35
import 'event.dart';
46

5-
/// Mixin that provides event emission capabilities.
7+
/// Mixin that provides event emission capabilities with lifecycle management.
68
/// Classes that use this mixin can easily add listener management
79
/// and event dispatching functionality.
810
///
9-
/// Example usage:
11+
/// The mixin automatically manages event listening lifecycle:
12+
/// - `startEventListening()` is called when the first listener is added
13+
/// - `stopEventListening()` is called when the last listener is removed
14+
///
15+
/// Subclasses can override these methods to manage platform-specific resources:
1016
///
1117
/// ```dart
1218
/// class MyClass with EventEmitter {
19+
/// @override
20+
/// void startEventListening() {
21+
/// // Start platform event monitoring
22+
/// }
23+
///
24+
/// @override
25+
/// void stopEventListening() {
26+
/// // Stop platform event monitoring
27+
/// }
28+
///
1329
/// void doSomething() {
1430
/// // Emit an event synchronously
1531
/// emitSync(MyEvent("some data"));
@@ -35,16 +51,34 @@ mixin EventEmitter {
3551
/// Counter for generating unique listener IDs
3652
int _nextListenerId = 0;
3753

54+
/// Called when the first listener is added.
55+
/// Subclasses can override this to start platform-specific event monitoring.
56+
@protected
57+
void startEventListening() {}
58+
59+
/// Called when the last listener is removed.
60+
/// Subclasses can override this to stop platform-specific event monitoring.
61+
@protected
62+
void stopEventListening() {}
63+
3864
/// Add a typed event listener for a specific event type.
3965
///
4066
/// Returns a unique listener ID that can be used to remove the listener.
4167
int addListener<T extends Event>(EventListener<T> listener) {
4268
final eventType = T;
4369
final listenerId = _nextListenerId++;
4470

71+
// Check if this is the first listener
72+
final wasEmpty = totalListenerCount == 0;
73+
4574
_listeners.putIfAbsent(eventType, () => <int, EventListener>{});
4675
_listeners[eventType]![listenerId] = listener;
4776

77+
// Call hook when transitioning from 0 to 1+ listeners
78+
if (wasEmpty) {
79+
startEventListening();
80+
}
81+
4882
return listenerId;
4983
}
5084

@@ -61,6 +95,10 @@ mixin EventEmitter {
6195
bool removeListener(int listenerId) {
6296
for (final eventListeners in _listeners.values) {
6397
if (eventListeners.remove(listenerId) != null) {
98+
// Check if this was the last listener
99+
if (totalListenerCount == 0) {
100+
stopEventListening();
101+
}
64102
return true;
65103
}
66104
}
@@ -81,6 +119,8 @@ mixin EventEmitter {
81119
/// emitter.removeAllListeners(MyCustomEvent);
82120
/// ```
83121
void removeAllListeners<T extends Event>([Type? eventType]) {
122+
final hadListeners = totalListenerCount > 0;
123+
84124
if (eventType != null) {
85125
final listeners = _listeners[eventType];
86126
if (listeners != null) {
@@ -92,6 +132,11 @@ mixin EventEmitter {
92132
}
93133
_listeners.clear();
94134
}
135+
136+
// Call hook if we had listeners and now have none
137+
if (hadListeners && totalListenerCount == 0) {
138+
stopEventListening();
139+
}
95140
}
96141

97142
/// Get the number of listeners registered for a specific event type.
@@ -159,8 +204,15 @@ mixin EventEmitter {
159204
/// Dispose of the event emitter and clean up resources.
160205
/// Classes using this mixin should call this method when disposing.
161206
void disposeEventEmitter() {
207+
final hadListeners = totalListenerCount > 0;
208+
162209
// Clear all listeners
163210
_listeners.clear();
211+
212+
// Call hook if we had listeners
213+
if (hadListeners) {
214+
stopEventListening();
215+
}
164216
}
165217
}
166218

packages/nativeapi/lib/src/menu.dart

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ class MenuItem
3030
static bool _callbacksInitialized = false;
3131

3232
late final native_menu_item_t _nativeHandle;
33+
34+
// Store listener IDs for cleanup
35+
int? _clickedListenerId;
36+
int? _submenuOpenedListenerId;
37+
int? _submenuClosedListenerId;
3338

3439
MenuItem(String label, [MenuItemType type = MenuItemType.normal]) {
3540
final labelPtr = label.toNativeUtf8().cast<Char>();
@@ -39,6 +44,12 @@ class MenuItem
3944
);
4045
ffi.calloc.free(labelPtr);
4146

47+
// Store instance in static map using handle address as key
48+
_instances[_nativeHandle.address] = this;
49+
}
50+
51+
@override
52+
void startEventListening() {
4253
// Initialize callbacks once
4354
if (!_callbacksInitialized) {
4455
_clickedCallback =
@@ -56,30 +67,53 @@ class MenuItem
5667
_callbacksInitialized = true;
5768
}
5869

59-
// Store instance in static map using handle address as key
60-
_instances[_nativeHandle.address] = this;
61-
62-
// Register listeners for each event type with native callbacks
63-
bindings.native_menu_item_add_listener(
70+
// Register listeners for each event type with native callbacks and store IDs
71+
_clickedListenerId = bindings.native_menu_item_add_listener(
6472
_nativeHandle,
6573
native_menu_item_event_type_t.NATIVE_MENU_ITEM_EVENT_CLICKED,
6674
_clickedCallback.nativeFunction,
6775
_nativeHandle,
6876
);
69-
bindings.native_menu_item_add_listener(
77+
_submenuOpenedListenerId = bindings.native_menu_item_add_listener(
7078
_nativeHandle,
7179
native_menu_item_event_type_t.NATIVE_MENU_ITEM_EVENT_SUBMENU_OPENED,
7280
_submenuOpenedCallback.nativeFunction,
7381
_nativeHandle,
7482
);
75-
bindings.native_menu_item_add_listener(
83+
_submenuClosedListenerId = bindings.native_menu_item_add_listener(
7684
_nativeHandle,
7785
native_menu_item_event_type_t.NATIVE_MENU_ITEM_EVENT_SUBMENU_CLOSED,
7886
_submenuClosedCallback.nativeFunction,
7987
_nativeHandle,
8088
);
8189
}
8290

91+
@override
92+
void stopEventListening() {
93+
// Remove native listeners using stored IDs
94+
if (_clickedListenerId != null) {
95+
bindings.native_menu_item_remove_listener(
96+
_nativeHandle,
97+
_clickedListenerId!,
98+
);
99+
_clickedListenerId = null;
100+
}
101+
if (_submenuOpenedListenerId != null) {
102+
bindings.native_menu_item_remove_listener(
103+
_nativeHandle,
104+
_submenuOpenedListenerId!,
105+
);
106+
_submenuOpenedListenerId = null;
107+
}
108+
if (_submenuClosedListenerId != null) {
109+
bindings.native_menu_item_remove_listener(
110+
_nativeHandle,
111+
_submenuClosedListenerId!,
112+
);
113+
_submenuClosedListenerId = null;
114+
}
115+
}
116+
83117
// Static callback functions for FFI
84118
static void _nativeOnClickedEvent(
85119
Pointer<Void> event,
@@ -199,24 +233,7 @@ class MenuItem
199233
// Remove instance from static map
200234
_instances.remove(_nativeHandle.address);
201235

202-
// // Remove native listeners
203-
// bindings.native_menu_item_remove_listener(
204-
// _nativeHandle,
205-
// native_menu_item_event_type_t.NATIVE_MENU_ITEM_EVENT_CLICKED,
206-
// _clickedCallback.nativeFunction,
207-
// );
208-
// bindings.native_menu_item_remove_listener(
209-
// _nativeHandle,
210-
// native_menu_item_event_type_t.NATIVE_MENU_ITEM_EVENT_SUBMENU_OPENED,
211-
// _submenuOpenedCallback.nativeFunction,
212-
// );
213-
// bindings.native_menu_item_remove_listener(
214-
// _nativeHandle,
215-
// native_menu_item_event_type_t.NATIVE_MENU_ITEM_EVENT_SUBMENU_CLOSED,
216-
// _submenuClosedCallback.nativeFunction,
217-
// );
218-
219-
// Dispose event emitter
236+
// Dispose event emitter (will call stopEventListening if needed)
220237
disposeEventEmitter();
221238

222239
// Destroy native handle
@@ -239,10 +256,20 @@ class Menu
239256
_closedCallback;
240257

241258
static bool _callbacksInitialized = false;
259+
260+
// Store listener IDs for cleanup
261+
int? _openedListenerId;
262+
int? _closedListenerId;
242263

243264
Menu([native_menu_t? nativeHandle]) {
244265
_nativeHandle = nativeHandle ?? bindings.native_menu_create();
245266

267+
// Store instance in static map using handle address as key
268+
_instances[_nativeHandle.address] = this;
269+
}
270+
271+
@override
272+
void startEventListening() {
246273
// Initialize callbacks once
247274
if (!_callbacksInitialized) {
248275
_openedCallback =
@@ -256,24 +283,40 @@ class Menu
256283
_callbacksInitialized = true;
257284
}
258285

259-
// Store instance in static map using handle address as key
260-
_instances[_nativeHandle.address] = this;
261-
262-
// Register listeners for each event type with native callbacks
263-
bindings.native_menu_add_listener(
286+
// Register listeners for each event type with native callbacks and store IDs
287+
_openedListenerId = bindings.native_menu_add_listener(
264288
_nativeHandle,
265289
native_menu_event_type_t.NATIVE_MENU_EVENT_OPENED,
266290
_openedCallback.nativeFunction,
267291
_nativeHandle,
268292
);
269-
bindings.native_menu_add_listener(
293+
_closedListenerId = bindings.native_menu_add_listener(
270294
_nativeHandle,
271295
native_menu_event_type_t.NATIVE_MENU_EVENT_CLOSED,
272296
_closedCallback.nativeFunction,
273297
_nativeHandle,
274298
);
275299
}
276300

301+
@override
302+
void stopEventListening() {
303+
// Remove native listeners using stored IDs
304+
if (_openedListenerId != null) {
305+
bindings.native_menu_remove_listener(
306+
_nativeHandle,
307+
_openedListenerId!,
308+
);
309+
_openedListenerId = null;
310+
}
311+
if (_closedListenerId != null) {
312+
bindings.native_menu_remove_listener(
313+
_nativeHandle,
314+
_closedListenerId!,
315+
);
316+
_closedListenerId = null;
317+
}
318+
}
319+
277320
// Static callback functions for FFI
278321
static void _nativeOnOpenedEvent(
279322
Pointer<Void> event,
@@ -374,13 +417,7 @@ class Menu
374417
// Remove instance from static map
375418
_instances.remove(_nativeHandle.address);
376419

377-
// // Remove native listeners
378-
// bindings.native_menu_remove_listener(
379-
// _nativeHandle,
380-
// _openedCallback.nativeFunction,
381-
// );
382-
383-
// Dispose event emitter
420+
// Dispose event emitter (will call stopEventListening if needed)
384421
disposeEventEmitter();
385422

386423
// Destroy native handle

0 commit comments

Comments
 (0)