diff --git a/examples/todos/lib/controllers/todos.dart b/examples/todos/lib/controllers/todos.dart index 0bd5f103..2bacf0b1 100644 --- a/examples/todos/lib/controllers/todos.dart +++ b/examples/todos/lib/controllers/todos.dart @@ -18,7 +18,7 @@ final todosControllerProvider = Provider( class TodosController { TodosController({ List initialTodos = const [], - }) : todos = ListSignal(initialTodos); + }) : todos = ListSignal(initialTodos, name: 'todos'); // The list of todos final ListSignal todos; @@ -26,11 +26,13 @@ class TodosController { /// The list of completed todos late final completedTodos = Computed( () => todos.where((todo) => todo.completed).toList(), + name: 'completedTodos', ); /// The list of incomplete todos late final incompleteTodos = Computed( () => todos.where((todo) => !todo.completed).toList(), + name: 'incompleteTodos', ); /// Add a todo diff --git a/examples/todos/lib/widgets/todos_body.dart b/examples/todos/lib/widgets/todos_body.dart index e45519cb..8638055a 100644 --- a/examples/todos/lib/widgets/todos_body.dart +++ b/examples/todos/lib/widgets/todos_body.dart @@ -6,7 +6,9 @@ import 'package:solidart_example/domain/todo.dart'; import 'package:solidart_example/widgets/todos_list.dart'; import 'package:solidart_example/widgets/toolbar.dart'; -final todosFilterProvider = Provider((context) => Signal(TodosFilter.all)); +final todosFilterProvider = Provider( + (context) => Signal(TodosFilter.all, name: 'todosFilter'), +); class TodosBody extends StatefulWidget { const TodosBody({super.key}); diff --git a/examples/todos/lib/widgets/toolbar.dart b/examples/todos/lib/widgets/toolbar.dart index 5047bcd7..33640806 100644 --- a/examples/todos/lib/widgets/toolbar.dart +++ b/examples/todos/lib/widgets/toolbar.dart @@ -17,12 +17,17 @@ class _ToolbarState extends State { /// All the derived signals, they will react only when the `length` property /// changes - late final allTodosCount = Computed(() => todosController.todos().length); + late final allTodosCount = Computed( + () => todosController.todos().length, + name: 'allTodosCount', + ); late final incompleteTodosCount = Computed( () => todosController.incompleteTodos().length, + name: 'incompleteTodosCount', ); late final completedTodosCount = Computed( () => todosController.completedTodos().length, + name: 'completedTodosCount', ); @override diff --git a/packages/solidart_devtools_extension/lib/main.dart b/packages/solidart_devtools_extension/lib/main.dart index 597b52e4..66096bfd 100644 --- a/packages/solidart_devtools_extension/lib/main.dart +++ b/packages/solidart_devtools_extension/lib/main.dart @@ -100,18 +100,21 @@ class SignalData { class _SignalsState extends State { late final StreamSubscription? sub; + late final StreamSubscription? isolateEventSub; final selectedSignalId = Signal(null); final searchController = SearchController(); final searchText = Signal(''); final filterType = Signal(null); final showDisposed = Signal(true); + final showOnlyActiveDuplicates = Signal(false); final signals = MapSignal({}); late final filteredSignals = Computed(() { final lowercasedSearch = searchText.value.toLowerCase(); final type = filterType.value; final viewDisposed = showDisposed.value; - return signals.value.entries + final onlyActiveDuplicates = showOnlyActiveDuplicates.value; + final filtered = signals.value.entries .where( (entry) => entry.value.name.toString().toLowerCase().contains( @@ -121,7 +124,27 @@ class _SignalsState extends State { ) .where((entry) => type == null || entry.value.type == type) .where((entry) => viewDisposed || !entry.value.disposed) + .where((entry) { + if (!onlyActiveDuplicates) return true; + // Count signals with same name + final sameName = signals.value.values + .where((s) => s.name == entry.value.name); + // Show only if: has duplicates AND is active + return sameName.length > 1 && !entry.value.disposed; + }) .toList(); + + // Sort by lastUpdate (newest first), then by disposed status (active first) + filtered.sort((a, b) { + final disposedCompare = a.value.disposed ? 1 : -1; + final disposedCompare2 = b.value.disposed ? 1 : -1; + if (disposedCompare != disposedCompare2) { + return disposedCompare.compareTo(disposedCompare2); + } + return b.value.lastUpdate.compareTo(a.value.lastUpdate); + }); + + return filtered; }); @override @@ -158,6 +181,14 @@ class _SignalsState extends State { ); } }); + + // Listen for hot restart events to clear signals + isolateEventSub = vmService.onIsolateEvent.listen((event) { + if (event.kind == 'IsolateReload' || event.kind == 'IsolateStart') { + signals.clear(); + } + }); + searchController.addListener( () => searchText.value = searchController.text, ); @@ -166,7 +197,15 @@ class _SignalsState extends State { @override void dispose() { sub?.cancel(); + isolateEventSub?.cancel(); searchController.dispose(); + selectedSignalId.dispose(); + searchText.dispose(); + filterType.dispose(); + showDisposed.dispose(); + showOnlyActiveDuplicates.dispose(); + signals.dispose(); + filteredSignals.dispose(); super.dispose(); } @@ -206,30 +245,33 @@ class _SignalsState extends State { spacing: 2, crossAxisAlignment: CrossAxisAlignment.start, children: [ - ShadInput( - placeholder: Text('Search signals'), - controller: searchController, - trailing: Show( - when: () => searchText.value.isNotEmpty, - builder: (context) { - return ShadIconButton( - onPressed: searchController.clear, - width: 20, - height: 20, - padding: EdgeInsets.zero, - decoration: const ShadDecoration( - secondaryBorder: ShadBorder.none, - secondaryFocusedBorder: ShadBorder.none, - ), - icon: const Icon(Icons.clear, size: 14), - ); - }, - ), - ), - const SizedBox(height: 8), Row( + spacing: 8, children: [ - Flexible( + Expanded( + flex: 2, + child: ShadInput( + placeholder: Text('Search signals'), + controller: searchController, + trailing: Show( + when: () => searchText.value.isNotEmpty, + builder: (context) { + return ShadIconButton( + onPressed: searchController.clear, + width: 20, + height: 20, + padding: EdgeInsets.zero, + decoration: const ShadDecoration( + secondaryBorder: ShadBorder.none, + secondaryFocusedBorder: ShadBorder.none, + ), + icon: const Icon(Icons.clear, size: 14), + ); + }, + ), + ), + ), + Expanded( child: SignalBuilder( builder: (context, _) { return ShadSelect( @@ -254,22 +296,39 @@ class _SignalsState extends State { }, ), ), - SizedBox( - height: 20, - child: const ShadSeparator.vertical(), - ), - Flexible( + ], + ), + const SizedBox(height: 8), + Row( + spacing: 8, + children: [ + Expanded( child: SignalBuilder( builder: (context, _) { return ShadCheckbox( value: showDisposed.value, label: const Text('Show disposed'), - padding: EdgeInsets.only(left: 4), onChanged: (v) => showDisposed.value = v, ); }, ), ), + Expanded( + child: SignalBuilder( + builder: (context, _) { + return ShadCheckbox( + value: showOnlyActiveDuplicates.value, + label: const Text('Active duplicates'), + onChanged: (v) => showOnlyActiveDuplicates.value = v, + ); + }, + ), + ), + ShadButton.outline( + onPressed: () => signals.clear(), + size: ShadButtonSize.sm, + child: const Text('Clear All'), + ), ], ), const SizedBox(height: 4), @@ -278,7 +337,7 @@ class _SignalsState extends State { return Padding( padding: const EdgeInsets.only(left: 8), child: Text( - '${filteredSignals.value.length} visible of ${signals.value.length}', + '${filteredSignals.value.length} visible of ${signals.value.length}${showOnlyActiveDuplicates.value ? ' (duplicates only)' : ''}', style: shadTheme.textTheme.muted, ), ); @@ -299,15 +358,7 @@ class _SignalsState extends State { itemCount: filteredSignals.value.length, padding: EdgeInsets.symmetric(horizontal: 4), itemBuilder: (BuildContext context, int index) { - final sortedSignals = filteredSignals.value - ..sort( - (a, b) => b.value.lastUpdate.compareTo( - a.value.lastUpdate, - ), - ) - ..sort((a, b) => a.value.disposed ? 1 : -1); - - final entry = sortedSignals.elementAt(index); + final entry = filteredSignals.value[index]; final name = entry.value.name; final signal = entry.value; return SignalBuilder(