diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart index 69ee38c28f9..019244c4d67 100644 --- a/packages/flutter_tools/lib/src/base/utils.dart +++ b/packages/flutter_tools/lib/src/base/utils.dart @@ -97,9 +97,9 @@ String getSizeAsMB(int bytesLength) { /// removed, and calculate a diff of changes when a new list of items is /// available. class ItemListNotifier { - ItemListNotifier(): _items = {}; + ItemListNotifier(): _items = {}, _isPopulated = false; - ItemListNotifier.from(List items) : _items = Set.of(items); + ItemListNotifier.from(List items) : _items = Set.of(items), _isPopulated = true; Set _items; @@ -111,6 +111,11 @@ class ItemListNotifier { List get items => _items.toList(); + bool _isPopulated; + + /// Returns whether the list has been populated. + bool get isPopulated => _isPopulated; + void updateWithNewList(List updatedList) { final Set updatedSet = Set.of(updatedList); @@ -118,9 +123,10 @@ class ItemListNotifier { final Set removedItems = _items.difference(updatedSet); _items = updatedSet; + _isPopulated = true; - addedItems.forEach(_addedController.add); removedItems.forEach(_removedController.add); + addedItems.forEach(_addedController.add); } void removeItem(T item) { diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index b681df851b4..630abe646c9 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -480,18 +480,15 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { @protected @visibleForTesting - ItemListNotifier? deviceNotifier; + final ItemListNotifier deviceNotifier = ItemListNotifier(); Timer? _timer; Future> pollingGetDevices({Duration? timeout}); void startPolling() { - if (_timer == null) { - deviceNotifier ??= ItemListNotifier(); - // Make initial population the default, fast polling timeout. - _timer = _initTimer(null, initialCall: true); - } + // Make initial population the default, fast polling timeout. + _timer ??= _initTimer(null, initialCall: true); } Timer _initTimer(Duration? pollingTimeout, {bool initialCall = false}) { @@ -499,7 +496,7 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { return Timer(initialCall ? Duration.zero : _pollingInterval, () async { try { final List devices = await pollingGetDevices(timeout: pollingTimeout); - deviceNotifier!.updateWithNewList(devices); + deviceNotifier.updateWithNewList(devices); } on TimeoutException { // Do nothing on a timeout. } @@ -546,32 +543,28 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { DeviceDiscoveryFilter? filter, bool resetCache = false, }) async { - if (deviceNotifier == null || resetCache) { + if (!deviceNotifier.isPopulated || resetCache) { final List devices = await pollingGetDevices(timeout: timeout); // If the cache was populated while the polling was ongoing, do not // overwrite the cache unless it's explicitly refreshing the cache. - if (resetCache) { - deviceNotifier = ItemListNotifier.from(devices); - } else { - deviceNotifier ??= ItemListNotifier.from(devices); + if (!deviceNotifier.isPopulated || resetCache) { + deviceNotifier.updateWithNewList(devices); } } // If a filter is provided, filter cache to only return devices matching. if (filter != null) { - return filter.filterDevices(deviceNotifier!.items); + return filter.filterDevices(deviceNotifier.items); } - return deviceNotifier!.items; + return deviceNotifier.items; } Stream get onAdded { - deviceNotifier ??= ItemListNotifier(); - return deviceNotifier!.onAdded; + return deviceNotifier.onAdded; } Stream get onRemoved { - deviceNotifier ??= ItemListNotifier(); - return deviceNotifier!.onRemoved; + return deviceNotifier.onRemoved; } void dispose() => stopPolling(); diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 15b718ab85a..bdf55e15b8c 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -102,8 +102,6 @@ class IOSDevices extends PollingDeviceDiscovery { return; } - deviceNotifier ??= ItemListNotifier(); - // Start by populating all currently attached devices. _updateCachedDevices(await pollingGetDevices()); _updateNotifierFromCache(); @@ -127,10 +125,8 @@ class IOSDevices extends PollingDeviceDiscovery { @visibleForTesting Future onDeviceEvent(XCDeviceEventNotification event) async { - final ItemListNotifier? notifier = deviceNotifier; - if (notifier == null) { - return; - } + final ItemListNotifier notifier = deviceNotifier; + Device? knownDevice; for (final Device device in notifier.items) { if (device.id == event.deviceIdentifier) { @@ -186,10 +182,8 @@ class IOSDevices extends PollingDeviceDiscovery { /// Updates notifier with devices found in the cache that are determined /// to be connected. void _updateNotifierFromCache() { - final ItemListNotifier? notifier = deviceNotifier; - if (notifier == null) { - return; - } + final ItemListNotifier notifier = deviceNotifier; + // Device is connected if it has either an observed usb or wifi connection // or it has not been observed but was found as connected in the cache. final List connectedDevices = _cachedPolledDevices.values.where((Device device) { diff --git a/packages/flutter_tools/test/general.shard/base_utils_test.dart b/packages/flutter_tools/test/general.shard/base_utils_test.dart index 42f8f6cb563..4d241af9bf7 100644 --- a/packages/flutter_tools/test/general.shard/base_utils_test.dart +++ b/packages/flutter_tools/test/general.shard/base_utils_test.dart @@ -36,5 +36,27 @@ void main() { expect(removedItems.first, 'aaa'); expect(removedItems[1], 'bbb'); }); + + test('becomes populated when item is added', () async { + final ItemListNotifier list = ItemListNotifier(); + expect(list.isPopulated, false); + expect(list.items, isEmpty); + + // Becomes populated when a new list is added. + list.updateWithNewList(['a']); + expect(list.isPopulated, true); + expect(list.items, ['a']); + + // Remain populated even when the last item is removed. + list.removeItem('a'); + expect(list.isPopulated, true); + expect(list.items, isEmpty); + }); + + test('is populated by default if initialized with list of items', () async { + final ItemListNotifier list = ItemListNotifier.from(['a']); + expect(list.isPopulated, true); + expect(list.items, ['a']); + }); }); } diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart index e5f31eeef7d..c1a62a17cac 100644 --- a/packages/flutter_tools/test/general.shard/device_test.dart +++ b/packages/flutter_tools/test/general.shard/device_test.dart @@ -1089,6 +1089,47 @@ void main() { ); }); }); + + group('PollingDeviceDiscovery', () { + final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f'); + + testWithoutContext('initial call to devices returns the correct list', () async { + final List deviceList = [device1]; + final TestPollingDeviceDiscovery testDeviceDiscovery = TestPollingDeviceDiscovery(deviceList); + + // Call `onAdded` to make sure that calling `onAdded` does not affect the + // result of `devices()`. + final List addedDevice = []; + final List removedDevice = []; + testDeviceDiscovery.onAdded.listen(addedDevice.add); + testDeviceDiscovery.onRemoved.listen(removedDevice.add); + + final List devices = await testDeviceDiscovery.devices(); + expect(devices.length, 1); + expect(devices.first.id, device1.id); + }); + + testWithoutContext('call to devices triggers onAdded', () async { + final List deviceList = [device1]; + final TestPollingDeviceDiscovery testDeviceDiscovery = TestPollingDeviceDiscovery(deviceList); + + // Call `onAdded` to make sure that calling `onAdded` does not affect the + // result of `devices()`. + final List addedDevice = []; + final List removedDevice = []; + testDeviceDiscovery.onAdded.listen(addedDevice.add); + testDeviceDiscovery.onRemoved.listen(removedDevice.add); + + final List devices = await testDeviceDiscovery.devices(); + expect(devices.length, 1); + expect(devices.first.id, device1.id); + + await pumpEventQueue(); + + expect(addedDevice.length, 1); + expect(addedDevice.first.id, device1.id); + }); + }); } class TestDeviceManager extends DeviceManager { @@ -1203,3 +1244,24 @@ class ThrowingPollingDeviceDiscovery extends PollingDeviceDiscovery { @override List get wellKnownIds => []; } + + +class TestPollingDeviceDiscovery extends PollingDeviceDiscovery { + TestPollingDeviceDiscovery(this._devices) : super('test'); + + final List _devices; + + @override + Future> pollingGetDevices({ Duration? timeout }) async { + return _devices; + } + + @override + bool get supportsPlatform => true; + + @override + bool get canListAnything => true; + + @override + List get wellKnownIds => []; +} diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index 9b3593f67c8..56dd46709c6 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -659,7 +659,7 @@ void main() { await iosDevices.startPolling(); expect(xcdevice.getAvailableIOSDevicesCount, 1); - expect(iosDevices.deviceNotifier!.items, isEmpty); + expect(iosDevices.deviceNotifier.items, isEmpty); expect(xcdevice.deviceEventController.hasListener, isTrue); xcdevice.deviceEventController.add( @@ -670,9 +670,9 @@ void main() { ), ); await added.future; - expect(iosDevices.deviceNotifier!.items.length, 2); - expect(iosDevices.deviceNotifier!.items, contains(device1)); - expect(iosDevices.deviceNotifier!.items, contains(device2)); + expect(iosDevices.deviceNotifier.items.length, 2); + expect(iosDevices.deviceNotifier.items, contains(device1)); + expect(iosDevices.deviceNotifier.items, contains(device2)); expect(iosDevices.eventsReceived, 1); iosDevices.resetEventCompleter(); @@ -684,9 +684,9 @@ void main() { ), ); await iosDevices.receivedEvent.future; - expect(iosDevices.deviceNotifier!.items.length, 2); - expect(iosDevices.deviceNotifier!.items, contains(device1)); - expect(iosDevices.deviceNotifier!.items, contains(device2)); + expect(iosDevices.deviceNotifier.items.length, 2); + expect(iosDevices.deviceNotifier.items, contains(device1)); + expect(iosDevices.deviceNotifier.items, contains(device2)); expect(iosDevices.eventsReceived, 2); iosDevices.resetEventCompleter(); @@ -698,9 +698,9 @@ void main() { ), ); await iosDevices.receivedEvent.future; - expect(iosDevices.deviceNotifier!.items.length, 2); - expect(iosDevices.deviceNotifier!.items, contains(device1)); - expect(iosDevices.deviceNotifier!.items, contains(device2)); + expect(iosDevices.deviceNotifier.items.length, 2); + expect(iosDevices.deviceNotifier.items, contains(device1)); + expect(iosDevices.deviceNotifier.items, contains(device2)); expect(iosDevices.eventsReceived, 3); xcdevice.deviceEventController.add( @@ -711,7 +711,7 @@ void main() { ), ); await removed.future; - expect(iosDevices.deviceNotifier!.items, [device2]); + expect(iosDevices.deviceNotifier.items, [device2]); expect(iosDevices.eventsReceived, 4); iosDevices.resetEventCompleter(); @@ -777,7 +777,7 @@ void main() { xcdevice.devices.add([]); await iosDevices.startPolling(); - expect(iosDevices.deviceNotifier!.items, isEmpty); + expect(iosDevices.deviceNotifier.items, isEmpty); expect(xcdevice.deviceEventController.hasListener, isTrue); iosDevices.dispose(); diff --git a/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart b/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart index e7e4fae67a0..4960fe97f4c 100644 --- a/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/proxied_devices/proxied_devices_test.dart @@ -539,11 +539,11 @@ void main() { proxiedDevices.startPolling(); - final ItemListNotifier? deviceNotifier = proxiedDevices.deviceNotifier; + final ItemListNotifier deviceNotifier = proxiedDevices.deviceNotifier; expect(deviceNotifier, isNotNull); final List devicesAdded = []; - deviceNotifier!.onAdded.listen((Device device) { + deviceNotifier.onAdded.listen((Device device) { devicesAdded.add(device); });