diff --git a/lib/widgets/common/action_controls/togglers/mute.dart b/lib/widgets/common/action_controls/togglers/mute.dart index fa9a2a693..caff7729e 100644 --- a/lib/widgets/common/action_controls/togglers/mute.dart +++ b/lib/widgets/common/action_controls/togglers/mute.dart @@ -4,6 +4,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; @@ -25,9 +26,10 @@ class MuteToggler extends StatelessWidget { @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: controller?.canMuteNotifier ?? ValueNotifier(false), - builder: (context, canDo, child) { + return NullableValueListenableBuilder( + valueListenable: controller?.canMuteNotifier, + builder: (context, value, child) { + final canDo = value ?? false; return StreamBuilder( stream: controller?.volumeStream ?? Stream.value(1.0), builder: (context, snapshot) { @@ -66,9 +68,10 @@ class MuteTogglerCaption extends StatelessWidget { @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: controller?.canMuteNotifier ?? ValueNotifier(false), - builder: (context, canDo, child) { + return NullableValueListenableBuilder( + valueListenable: controller?.canMuteNotifier, + builder: (context, value, child) { + final canDo = value ?? false; return StreamBuilder( stream: controller?.volumeStream ?? Stream.value(1.0), builder: (context, snapshot) { diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index 53f5e1870..1e083a3ce 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -6,6 +6,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -151,9 +152,10 @@ class SafeCutoutArea extends StatelessWidget { return ValueListenableBuilder( valueListenable: AvesApp.cutoutInsetsNotifier, builder: (context, cutoutInsets, child) { - return ValueListenableBuilder( - valueListenable: animation ?? ValueNotifier(1), - builder: (context, factor, child) { + return NullableValueListenableBuilder( + valueListenable: animation, + builder: (context, value, child) { + final double factor = value ?? 1.0; final effectiveInsets = cutoutInsets * factor; return Padding( padding: effectiveInsets, diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index 439d67a7b..901ffe79e 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -7,6 +7,7 @@ import 'package:aves/widgets/common/map/leaflet/latlng_tween.dart' as llt; import 'package:aves/widgets/common/map/leaflet/scale_layer.dart'; import 'package:aves/widgets/common/map/leaflet/tile_layers.dart'; import 'package:aves_map/aves_map.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -172,8 +173,8 @@ class _EntryLeafletMapState extends State> with TickerProv rotate: true, alignment: Alignment.bottomCenter, ), - ValueListenableBuilder( - valueListenable: widget.dotLocationNotifier ?? ValueNotifier(null), + NullableValueListenableBuilder( + valueListenable: widget.dotLocationNotifier, builder: (context, dotLocation, child) => MarkerLayer( markers: [ if (dotLocation != null) @@ -214,9 +215,10 @@ class _EntryLeafletMapState extends State> with TickerProv final corner2 = overlayEntry.bottomRight; if (corner1 == null || corner2 == null) return const SizedBox(); - return ValueListenableBuilder( - valueListenable: widget.overlayOpacityNotifier ?? ValueNotifier(1), - builder: (context, overlayOpacity, child) { + return NullableValueListenableBuilder( + valueListenable: widget.overlayOpacityNotifier, + builder: (context, value, child) { + final double overlayOpacity = value ?? 1.0; return OverlayImageLayer( overlayImages: [ OverlayImage( diff --git a/lib/widgets/viewer/overlay/thumbnail_preview.dart b/lib/widgets/viewer/overlay/thumbnail_preview.dart index f1d656a92..5a64c3db3 100644 --- a/lib/widgets/viewer/overlay/thumbnail_preview.dart +++ b/lib/widgets/viewer/overlay/thumbnail_preview.dart @@ -24,7 +24,7 @@ class ViewerThumbnailPreview extends StatefulWidget { } class _ViewerThumbnailPreviewState extends State { - final ValueNotifier _entryIndexNotifier = ValueNotifier(0); + late final ValueNotifier _entryIndexNotifier; final Debouncer _debouncer = Debouncer(delay: ADurations.viewerThumbnailScrollDebounceDelay); List get entries => widget.entries; @@ -34,7 +34,7 @@ class _ViewerThumbnailPreviewState extends State { @override void initState() { super.initState(); - _entryIndexNotifier.value = widget.displayedIndex; + _entryIndexNotifier = ValueNotifier(widget.displayedIndex); _entryIndexNotifier.addListener(_onScrollerIndexChanged); } diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index 9c0f610ea..79cdd6595 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -28,6 +28,7 @@ import 'package:aves/widgets/viewer/action/entry_action_delegate.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves_model/aves_model.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -438,15 +439,18 @@ class _ViewerButtonRowContentState extends State { Widget? child; void onPressed() => actionDelegate.onActionSelected(context, action); - ValueListenableBuilder _buildFromListenable(ValueListenable? enabledNotifier) { - return ValueListenableBuilder( - valueListenable: enabledNotifier ?? ValueNotifier(false), - builder: (context, canDo, child) => IconButton( - icon: child!, - onPressed: canDo ? onPressed : null, - focusNode: focusNode, - tooltip: action.getText(context), - ), + Widget _buildFromListenable(ValueListenable? enabledNotifier) { + return NullableValueListenableBuilder( + valueListenable: enabledNotifier, + builder: (context, value, child) { + final canDo = value ?? false; + return IconButton( + icon: child!, + onPressed: canDo ? onPressed : null, + focusNode: focusNode, + tooltip: action.getText(context), + ); + }, child: action.getIcon(), ); } diff --git a/lib/widgets/viewer/page_entry_builder.dart b/lib/widgets/viewer/page_entry_builder.dart index 97fcb5db0..b50c7fb3b 100644 --- a/lib/widgets/viewer/page_entry_builder.dart +++ b/lib/widgets/viewer/page_entry_builder.dart @@ -1,6 +1,7 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:flutter/widgets.dart'; class PageEntryBuilder extends StatelessWidget { @@ -20,8 +21,8 @@ class PageEntryBuilder extends StatelessWidget { stream: controller != null ? controller.infoStream : Stream.value(null), builder: (context, snapshot) { final multiPageInfo = controller?.info; - return ValueListenableBuilder( - valueListenable: controller?.pageNotifier ?? ValueNotifier(null), + return NullableValueListenableBuilder( + valueListenable: controller?.pageNotifier, builder: (context, page, child) { final pageEntry = multiPageInfo?.getPageEntryByIndex(page); return builder(pageEntry); diff --git a/plugins/aves_services_google/lib/src/map.dart b/plugins/aves_services_google/lib/src/map.dart index cd306c546..58d48a037 100644 --- a/plugins/aves_services_google/lib/src/map.dart +++ b/plugins/aves_services_google/lib/src/map.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:aves_map/aves_map.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:latlong2/latlong.dart' as ll; @@ -172,12 +173,13 @@ class _EntryGoogleMapState extends State> with WidgetsBindi final interactive = context.select((v) => v.interactive); final overlayEntry = widget.overlayEntry; - return ValueListenableBuilder( - valueListenable: widget.dotLocationNotifier ?? ValueNotifier(null), + return NullableValueListenableBuilder( + valueListenable: widget.dotLocationNotifier, builder: (context, dotLocation, child) { - return ValueListenableBuilder( - valueListenable: widget.overlayOpacityNotifier ?? ValueNotifier(1), - builder: (context, overlayOpacity, child) { + return NullableValueListenableBuilder( + valueListenable: widget.overlayOpacityNotifier, + builder: (context, value, child) { + final double overlayOpacity = value ?? 1.0; return LayoutBuilder( builder: (context, constraints) { _sizeNotifier.value = constraints.biggest; diff --git a/plugins/aves_services_google/pubspec.lock b/plugins/aves_services_google/pubspec.lock index 004fffbf0..5f1359a21 100644 --- a/plugins/aves_services_google/pubspec.lock +++ b/plugins/aves_services_google/pubspec.lock @@ -30,6 +30,13 @@ packages: relative: true source: path version: "0.0.1" + aves_utils: + dependency: "direct main" + description: + path: "../aves_utils" + relative: true + source: path + version: "0.0.1" characters: dependency: transitive description: diff --git a/plugins/aves_services_google/pubspec.yaml b/plugins/aves_services_google/pubspec.yaml index c072898a4..f787173c4 100644 --- a/plugins/aves_services_google/pubspec.yaml +++ b/plugins/aves_services_google/pubspec.yaml @@ -12,6 +12,8 @@ dependencies: path: ../aves_map aves_services: path: ../aves_services + aves_utils: + path: ../aves_utils device_info_plus: google_api_availability: google_maps_flutter: diff --git a/plugins/aves_services_huawei/lib/src/map.dart b/plugins/aves_services_huawei/lib/src/map.dart index a51a0354f..b53555ac0 100644 --- a/plugins/aves_services_huawei/lib/src/map.dart +++ b/plugins/aves_services_huawei/lib/src/map.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:aves_map/aves_map.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:flutter/material.dart'; import 'package:huawei_map/huawei_map.dart'; import 'package:latlong2/latlong.dart' as ll; @@ -146,12 +147,13 @@ class _EntryHmsMapState extends State> { final interactive = context.select((v) => v.interactive); // final overlayEntry = widget.overlayEntry; - return ValueListenableBuilder( - valueListenable: widget.dotLocationNotifier ?? ValueNotifier(null), + return NullableValueListenableBuilder( + valueListenable: widget.dotLocationNotifier, builder: (context, dotLocation, child) { - return ValueListenableBuilder( - valueListenable: widget.overlayOpacityNotifier ?? ValueNotifier(1), - builder: (context, overlayOpacity, child) { + return NullableValueListenableBuilder( + valueListenable: widget.overlayOpacityNotifier, + builder: (context, value, child) { + // final double overlayOpacity = value ?? 1.0; return HuaweiMap( initialCameraPosition: CameraPosition( bearing: bounds.rotation, diff --git a/plugins/aves_services_huawei/pubspec.lock b/plugins/aves_services_huawei/pubspec.lock index dddfff5fa..efcf6e786 100644 --- a/plugins/aves_services_huawei/pubspec.lock +++ b/plugins/aves_services_huawei/pubspec.lock @@ -37,6 +37,13 @@ packages: relative: true source: path version: "0.0.1" + aves_utils: + dependency: "direct main" + description: + path: "../aves_utils" + relative: true + source: path + version: "0.0.1" characters: dependency: transitive description: diff --git a/plugins/aves_services_huawei/pubspec.yaml b/plugins/aves_services_huawei/pubspec.yaml index 1016be8e0..794aff2b1 100644 --- a/plugins/aves_services_huawei/pubspec.yaml +++ b/plugins/aves_services_huawei/pubspec.yaml @@ -14,6 +14,8 @@ dependencies: path: ../aves_platform_meta aves_services: path: ../aves_services + aves_utils: + path: ../aves_utils # cf https://github.com/HMS-Core/hms-flutter-plugin/pull/296 huawei_hmsavailability: git: diff --git a/plugins/aves_utils/lib/aves_utils.dart b/plugins/aves_utils/lib/aves_utils.dart index c2f027c4e..153778df2 100644 --- a/plugins/aves_utils/lib/aves_utils.dart +++ b/plugins/aves_utils/lib/aves_utils.dart @@ -1,6 +1,6 @@ library aves_utils; -export 'src/change_notifier.dart'; export 'src/colors.dart'; +export 'src/listenable.dart'; export 'src/optional_event_channel.dart'; export 'src/vector_utils.dart'; diff --git a/plugins/aves_utils/lib/src/change_notifier.dart b/plugins/aves_utils/lib/src/change_notifier.dart deleted file mode 100644 index 1e0b0b9f5..000000000 --- a/plugins/aves_utils/lib/src/change_notifier.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/foundation.dart'; - -// `ChangeNotifier` wrapper to call `notify` without constraint -class AChangeNotifier extends ChangeNotifier { - void notify() { - // why is this protected? - super.notifyListeners(); - } -} diff --git a/plugins/aves_utils/lib/src/listenable.dart b/plugins/aves_utils/lib/src/listenable.dart new file mode 100644 index 000000000..75701cd6d --- /dev/null +++ b/plugins/aves_utils/lib/src/listenable.dart @@ -0,0 +1,55 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +// `ChangeNotifier` wrapper to call `notify` without constraint +class AChangeNotifier extends ChangeNotifier { + void notify() { + // why is this protected? + super.notifyListeners(); + } +} + +// contrary to standard `ValueListenableBuilder`, this widget allows providing a null listenable +class NullableValueListenableBuilder extends StatefulWidget { + final ValueListenable? valueListenable; + final ValueWidgetBuilder builder; + final Widget? child; + + const NullableValueListenableBuilder({ + super.key, + required this.valueListenable, + required this.builder, + this.child, + }); + + @override + State createState() => _NullableValueListenableBuilderState(); +} + +class _NullableValueListenableBuilderState extends State> { + ValueNotifier? _internalValueListenable; + + ValueListenable get _valueListenable { + var listenable = widget.valueListenable; + if (listenable == null) { + _internalValueListenable ??= ValueNotifier(null); + listenable = _internalValueListenable; + } + return listenable!; + } + + @override + void dispose() { + _internalValueListenable?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _valueListenable, + builder: widget.builder, + child: widget.child, + ); + } +}