diff --git a/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart b/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart index 3acf15b9509..24b781e8134 100644 --- a/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart +++ b/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart @@ -109,7 +109,7 @@ class MockDartToolingApi extends DartToolingApiImpl { final Stream log; /// Simulates executing a VS Code command requested by the embedded panel. - Future initialize() async { + void initialize() { connectDevices(); } diff --git a/packages/devtools_app_shared/README.md b/packages/devtools_app_shared/README.md index f88d1b68866..013faee65ee 100644 --- a/packages/devtools_app_shared/README.md +++ b/packages/devtools_app_shared/README.md @@ -111,7 +111,7 @@ void main() { // Use the [connectedState] notifier to listen for connection updates. serviceManager.connectedState.addListener(() { - if (connectedState.value.connected) { + if (serviceManager.connectedState.value.connected) { print('Manager connected to VM service'); } else { print('Manager not connected to VM service'); diff --git a/packages/devtools_app_shared/example/service_example.dart b/packages/devtools_app_shared/example/service_example.dart new file mode 100644 index 00000000000..5e017743192 --- /dev/null +++ b/packages/devtools_app_shared/example/service_example.dart @@ -0,0 +1,67 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: unused_local_variable + +import 'dart:async'; + +import 'package:devtools_app_shared/service.dart'; +import 'package:devtools_app_shared/service_extensions.dart' as extensions; +import 'package:devtools_shared/service.dart'; +import 'package:flutter/foundation.dart'; +import 'package:vm_service/vm_service.dart'; + +void main() async { + final serviceManager = ServiceManager(); + + // Example: use [connectedState] to listen for connection updates. + serviceManager.connectedState.addListener(() { + if (serviceManager.connectedState.value.connected) { + print('Manager connected to VM service'); + } else { + print('Manager not connected to VM service'); + } + }); + + // Example: establish a vm service connection. + // To get a [VmService] object from a vm service URI, consider importing + // `package:devtools_shared/service.dart` from `package:devtools_shared`. + const someVmServiceUri = 'http://127.0.0.1:60851/fH-kAEXc7MQ=/'; + final finishedCompleter = Completer(); + final vmService = await connect( + uri: Uri.parse(someVmServiceUri), + finishedCompleter: finishedCompleter, + createService: ({ + // ignore: avoid-dynamic, code needs to match API from VmService. + required Stream /*String|List*/ inStream, + required void Function(String message) writeMessage, + required Uri connectedUri, + }) { + return VmService(inStream, writeMessage); + }, + ); + + await serviceManager.vmServiceOpened( + vmService, + onClosed: finishedCompleter.future, + ); + + /// Example: Get a service extension state. + final ValueListenable performanceOverlayEnabled = + serviceManager.serviceExtensionManager.getServiceExtensionState( + extensions.performanceOverlay.extension, + ); + + // Example: Set a service extension state. + await serviceManager.serviceExtensionManager.setServiceExtensionState( + extensions.performanceOverlay.extension, + enabled: true, + value: true, + ); + + // Example: Access isolates. + final myIsolate = serviceManager.isolateManager.mainIsolate.value; + + // Etc. +} diff --git a/packages/devtools_app_shared/example/ui/common_example.dart b/packages/devtools_app_shared/example/ui/common_example.dart new file mode 100644 index 00000000000..f9cdd34393a --- /dev/null +++ b/packages/devtools_app_shared/example/ui/common_example.dart @@ -0,0 +1,31 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:devtools_app_shared/ui.dart' as devtools_shared_ui; +import 'package:flutter/material.dart'; + +class ExampleWidget extends StatelessWidget { + const ExampleWidget({super.key}); + + @override + Widget build(BuildContext context) { + return devtools_shared_ui.RoundedOutlinedBorder( + child: Column( + children: [ + const devtools_shared_ui.AreaPaneHeader( + roundedTopBorder: false, + includeTopBorder: false, + title: Text('This is a section header'), + ), + Expanded( + child: Text( + 'Foo', + style: Theme.of(context).subtleTextStyle, // Shared style + ), + ), + ], + ), + ); + } +} diff --git a/packages/devtools_app_shared/example/ui/dialog_example.dart b/packages/devtools_app_shared/example/ui/dialog_example.dart new file mode 100644 index 00000000000..6b39a855ac6 --- /dev/null +++ b/packages/devtools_app_shared/example/ui/dialog_example.dart @@ -0,0 +1,38 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:devtools_app_shared/ui.dart' as devtools_shared_ui; +import 'package:flutter/material.dart'; + +/// Example of using a [DevToolsDialog] widget from +/// 'package:devtools_app_shared/ui.dart'. +class MyDialog extends StatelessWidget { + const MyDialog({super.key}); + + @override + Widget build(BuildContext context) { + return devtools_shared_ui.DevToolsDialog( + title: const devtools_shared_ui.DialogTitleText('My Cool Dialog'), + content: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Here is the body of my dialog.'), + SizedBox(height: devtools_shared_ui.denseSpacing), + Text('It communicates something important'), + ], + ), + actions: [ + devtools_shared_ui.DialogTextButton( + onPressed: () { + // Do someting and then remove the dialog. + Navigator.of(context).pop(devtools_shared_ui.dialogDefaultContext); + }, + child: const Text('DO SOMETHING'), + ), + const devtools_shared_ui.DialogCancelButton(), + ], + ); + } +} diff --git a/packages/devtools_app_shared/example/ui/split_example.dart b/packages/devtools_app_shared/example/ui/split_example.dart new file mode 100644 index 00000000000..0c671003732 --- /dev/null +++ b/packages/devtools_app_shared/example/ui/split_example.dart @@ -0,0 +1,73 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:devtools_app_shared/ui.dart' as devtools_shared_ui; +import 'package:flutter/material.dart'; + +/// Example of using the [Split] widget from +/// 'package:devtools_app_shared/ui.dart' with two children laid across a +/// horizontal axis. +/// +/// This example does not specify the [Split.splitters] parameter, so a +/// default splitter is used. +class SplitExample extends StatelessWidget { + const SplitExample({super.key}); + + @override + Widget build(BuildContext context) { + return devtools_shared_ui.Split( + axis: Axis.horizontal, + initialFractions: const [0.3, 0.7], + minSizes: const [50.0, 100.0], + children: const [ + Text('Left side'), + Text('Right side'), + ], + ); + } +} + +/// Example of using the [Split] widget from +/// 'package:devtools_app_shared/ui.dart' with three children laid across a +/// vertical axis. +/// +/// This example uses custom splitters. +class MultiSplitExample extends StatelessWidget { + const MultiSplitExample({super.key}); + + @override + Widget build(BuildContext context) { + return devtools_shared_ui.Split( + axis: Axis.vertical, + initialFractions: const [0.3, 0.3, 0.4], + minSizes: const [50.0, 50.0, 100.0], + splitters: const [ + CustomSplitter(), + CustomSplitter(), + ], + children: const [ + Text('Top'), + Text('Middle'), + Text('Bottom'), + ], + ); + } +} + +class CustomSplitter extends StatelessWidget implements PreferredSizeWidget { + const CustomSplitter({super.key}); + + static const _size = 50.0; + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: _size, + child: Icon(Icons.front_hand), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(_size); +} diff --git a/packages/devtools_app_shared/example/utils/auto_dispose_example.dart b/packages/devtools_app_shared/example/utils/auto_dispose_example.dart new file mode 100644 index 00000000000..95943d6e113 --- /dev/null +++ b/packages/devtools_app_shared/example/utils/auto_dispose_example.dart @@ -0,0 +1,91 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/widgets.dart'; + +/// This is an example of a [StatefulWidget] that uses the [AutoDisposeMixin] on +/// its state. +/// +/// [AutoDisposeMixin] is exposed by 'package:devtools_app_shared/utils.dart'. +class MyStatefulWidget extends StatefulWidget { + const MyStatefulWidget({super.key, required this.someNotifier}); + + final ValueNotifier someNotifier; + + @override + State createState() => _MyStatefulWidgetState(); +} + +// This is a State class that mixes in [AutoDisoposeMixin]. +class _MyStatefulWidgetState extends State + with AutoDisposeMixin { + late final MyController controller; + late String foo; + + @override + void initState() { + super.initState(); + _init(); + } + + @override + void didUpdateWidget(MyStatefulWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.someNotifier != widget.someNotifier) { + _init(); + } + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + void _init() { + // This kicks off initialization in [controller] which uses the + // [AutoDisposeControllerMixin]. + controller = MyController(widget.someNotifier)..init(); + + // Cancel any existing listeners in situations like this where we could be + // "re-initializing" in [didUpdateWidget]. + cancelListeners(); + + foo = widget.someNotifier.value; + + // Adds a listener to [widget.someNotifier] that will be automatically + // disposed as part of this stateful widget lifecycle. + addAutoDisposeListener(widget.someNotifier, () { + setState(() { + foo = widget.someNotifier.value; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Text(foo); + } +} + +/// This is an example of a controller that uses the +/// [AutoDisposeControllerMixin] exposed by +/// 'package:devtools_app_shared/utils.dart'. +/// +/// When [dispose] is called on this controller, any listeners or stream +/// subscriptions added using the [AutoDisposeControllerMixin] will be disposed +/// or canceled. +class MyController extends DisposableController + with AutoDisposeControllerMixin { + MyController(this.notifier); + + final ValueNotifier notifier; + + void init() { + addAutoDisposeListener(notifier, () { + // Do something. + }); + } +} diff --git a/packages/devtools_app_shared/example/utils/globals_example.dart b/packages/devtools_app_shared/example/utils/globals_example.dart new file mode 100644 index 00000000000..1e22e557fa9 --- /dev/null +++ b/packages/devtools_app_shared/example/utils/globals_example.dart @@ -0,0 +1,30 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:devtools_app_shared/utils.dart'; + +void main() { + setAndAccessGlobal(); +} + +/// This method demonstrates setting and accessing globals, which is +/// functionality exposed by 'package:devtools_app_shared/utils.dart'. +void setAndAccessGlobal() { + // Creates a globally accessible variable (`globals[ServiceManager]`); + setGlobal(MyCoolClass, MyCoolClass()); + // Access the variable directory from [globals]. + final coolClassFromGlobals = globals[MyCoolClass] as MyCoolClass; + coolClassFromGlobals.foo(); + + // OR (recommended) access the global from a top level getter. + coolClass.foo(); +} + +MyCoolClass get coolClass => globals[MyCoolClass] as MyCoolClass; + +class MyCoolClass { + void foo() { + print('foo'); + } +} diff --git a/packages/devtools_app_shared/example/utils/list_example.dart b/packages/devtools_app_shared/example/utils/list_example.dart new file mode 100644 index 00000000000..8fb6838d3e3 --- /dev/null +++ b/packages/devtools_app_shared/example/utils/list_example.dart @@ -0,0 +1,35 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + useListValueNotifier(); +} + +/// This is an example of using the [ListValueNotifier] that is exposed from +/// 'package:devtools_app_shared/utils.dart'. +/// +/// A [ListValueNotifier] will holds a list object, and will notify listeners +/// on modifications to the list. +/// +/// This should be used in place of ValueNotifier> when list +/// updates should notify listeners, and not just changing the notifier's value +/// with a new list. +void useListValueNotifier() { + final myListNotifier = ListValueNotifier([1, 2, 3]); + + // These calls will notify all listeners of [myListNotifier]. + myListNotifier.add(4); + myListNotifier.removeAt(0); + // ... + + // As opposed to: + final myValueNotifierWithAList = ValueNotifier>([1, 2, 3]); + + // These calls will not notify listeners of [myValueNotifierWithAList] + myValueNotifierWithAList.value.add(4); + myValueNotifierWithAList.value.removeAt(0); +} diff --git a/packages/devtools_app_shared/example/utils/utils_example.dart b/packages/devtools_app_shared/example/utils/utils_example.dart new file mode 100644 index 00000000000..bc8c8dc7653 --- /dev/null +++ b/packages/devtools_app_shared/example/utils/utils_example.dart @@ -0,0 +1,49 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:devtools_app_shared/utils.dart'; + +/// This example demonstrates using shared utility methods from +/// 'package:devtools_app_shared/utils.dart'. +void main() { + helperExample(); + immediateValueNotifierExample(); +} + +/// This method demonstrates using a helper methods [pluralize] and +/// [equalsWithinEpsilon] provided by 'package:devtools_app_shared/utils.dart'. +/// +/// Other helper methods in this file can be used in a similar manner, as they +/// are documented. +void helperExample() { + pluralize('dog', 1); // 'dog' + pluralize('dog', 2); // 'dogs' + pluralize('dog', 0); // 'dogs' + + pluralize('index', 1, plural: 'indices'); // 'index' + pluralize('index', 2, plural: 'indices'); // 'indices' + + // Note: the [defaultEpsilon] this method uses is equal to 1 / 1000. + // [defaultEpsilon] is also exposed by 'utils.dart'. + equalsWithinEpsilon(1.111, 1.112); // true + equalsWithinEpsilon(1.111, 1.113); // false +} + +/// This method demonstrates using an [ImmediateValueNotifier] from +/// 'package:devtools_app_shared/utils.dart'. +void immediateValueNotifierExample() { + final fooNotifier = ImmediateValueNotifier(0); + + var count = 0; + fooNotifier.addListener(() { + count++; + }); + + print('count: $count'); // count = 1, since the listener is called immediately + + // change the value of the notifier to trigger the listener. + fooNotifier.value = 1; + + print('count: $count'); // count = 2 +} diff --git a/packages/devtools_app_shared/lib/src/ui/dialogs.dart b/packages/devtools_app_shared/lib/src/ui/dialogs.dart index b75ae00928f..928aacd4c28 100644 --- a/packages/devtools_app_shared/lib/src/ui/dialogs.dart +++ b/packages/devtools_app_shared/lib/src/ui/dialogs.dart @@ -10,6 +10,60 @@ import 'theme/theme.dart'; const dialogDefaultContext = 'dialog'; +/// A standardized dialog for use in DevTools. +/// +/// It normalizes dialog layout, spacing, and look and feel. +class DevToolsDialog extends StatelessWidget { + const DevToolsDialog({ + super.key, + Widget? title, + required this.content, + this.includeDivider = true, + this.scrollable = true, + this.actions, + this.actionsAlignment, + }) : titleContent = title ?? const SizedBox(); + + static const contentPadding = 24.0; + + final Widget titleContent; + final Widget content; + final bool includeDivider; + final bool scrollable; + final List? actions; + final MainAxisAlignment? actionsAlignment; + + @override + Widget build(BuildContext context) { + return PointerInterceptor( + child: AlertDialog( + scrollable: scrollable, + title: Column( + children: [ + titleContent, + includeDivider + ? const PaddedDivider( + padding: EdgeInsets.only(bottom: denseRowSpacing), + ) + : const SizedBox(height: defaultSpacing), + ], + ), + contentPadding: const EdgeInsets.fromLTRB( + contentPadding, + 0, + contentPadding, + contentPadding, + ), + content: content, + actions: actions, + actionsAlignment: actionsAlignment, + buttonPadding: const EdgeInsets.symmetric(horizontal: defaultSpacing), + ), + ); + } +} + +/// A [Text] widget styled for dialog titles. class DialogTitleText extends StatelessWidget { const DialogTitleText(this.text, {super.key}); @@ -136,59 +190,6 @@ class DialogHelpText extends StatelessWidget { } } -/// A standardized dialog for use in DevTools. -/// -/// It normalizes dialog layout, spacing, and look and feel. -class DevToolsDialog extends StatelessWidget { - const DevToolsDialog({ - super.key, - Widget? title, - required this.content, - this.includeDivider = true, - this.scrollable = true, - this.actions, - this.actionsAlignment, - }) : titleContent = title ?? const SizedBox(); - - static const contentPadding = 24.0; - - final Widget titleContent; - final Widget content; - final bool includeDivider; - final bool scrollable; - final List? actions; - final MainAxisAlignment? actionsAlignment; - - @override - Widget build(BuildContext context) { - return PointerInterceptor( - child: AlertDialog( - scrollable: scrollable, - title: Column( - children: [ - titleContent, - includeDivider - ? const PaddedDivider( - padding: EdgeInsets.only(bottom: denseRowSpacing), - ) - : const SizedBox(height: defaultSpacing), - ], - ), - contentPadding: const EdgeInsets.fromLTRB( - contentPadding, - 0, - contentPadding, - contentPadding, - ), - content: content, - actions: actions, - actionsAlignment: actionsAlignment, - buttonPadding: const EdgeInsets.symmetric(horizontal: defaultSpacing), - ), - ); - } -} - /// A TextButton used to close a containing dialog (Close). class DialogCloseButton extends StatelessWidget { const DialogCloseButton({super.key, this.onClose, this.label = 'CLOSE'});