diff --git a/lib/src/constants/config.dart b/lib/src/constants/config.dart index 6b59243b..8c9d10df 100644 --- a/lib/src/constants/config.dart +++ b/lib/src/constants/config.dart @@ -13,6 +13,7 @@ import 'enums.dart'; class AssetPickerConfig { const AssetPickerConfig({ + this.internalExceptionHandler, this.selectedAssets, this.maxAssets = defaultMaxAssetsCount, this.pageSize = defaultAssetsPerPage, @@ -61,6 +62,9 @@ class AssetPickerConfig { 'Custom item did not set properly.', ); + /// {@macro wechat_assets_picker.ExceptionHandler} + final ExceptionHandler? internalExceptionHandler; + /// Selected assets. /// 已选中的资源 final List? selectedAssets; diff --git a/lib/src/constants/typedefs.dart b/lib/src/constants/typedefs.dart index f484d7a9..6f7fc7da 100644 --- a/lib/src/constants/typedefs.dart +++ b/lib/src/constants/typedefs.dart @@ -7,6 +7,12 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:photo_manager/photo_manager.dart' show PermissionState; +/// {@template wechat_assets_picker.ExceptionHandler} +/// Handles exceptions during internal calls. +/// 处理内部方法调用时的异常。 +/// {@endtemplate} +typedef ExceptionHandler = void Function(Object e, StackTrace s); + /// {@template wechat_assets_picker.LoadingIndicatorBuilder} /// Build the loading indicator with the given [isAssetsEmpty]. /// 根据给定的 [isAssetsEmpty] 构建加载指示器。 diff --git a/lib/src/delegates/asset_picker_builder_delegate.dart b/lib/src/delegates/asset_picker_builder_delegate.dart index 07348400..254d890a 100644 --- a/lib/src/delegates/asset_picker_builder_delegate.dart +++ b/lib/src/delegates/asset_picker_builder_delegate.dart @@ -19,6 +19,7 @@ import '../constants/enums.dart'; import '../constants/extensions.dart'; import '../constants/typedefs.dart'; import '../delegates/asset_picker_text_delegate.dart'; +import '../internal/methods.dart'; import '../internal/singleton.dart'; import '../models/path_wrapper.dart'; import '../provider/asset_picker_provider.dart'; @@ -1264,7 +1265,7 @@ class DefaultAssetPickerBuilderDelegate if (p.hasMoreToLoad) { if ((p.pageSize <= gridCount * 3 && index == length - 1) || index == length - gridCount * 3) { - p.loadMoreAssets(); + p.loadMoreAssets().catchError(handleException); } } diff --git a/lib/src/delegates/asset_picker_delegate.dart b/lib/src/delegates/asset_picker_delegate.dart index 0b1b5cb9..0f31db4b 100644 --- a/lib/src/delegates/asset_picker_delegate.dart +++ b/lib/src/delegates/asset_picker_delegate.dart @@ -8,6 +8,7 @@ import 'package:photo_manager/photo_manager.dart'; import '../constants/config.dart'; import '../constants/constants.dart'; +import '../constants/typedefs.dart'; import '../internal/methods.dart'; import '../provider/asset_picker_provider.dart'; import '../widget/asset_picker.dart'; @@ -104,6 +105,9 @@ class AssetPickerDelegate { locale: Localizations.maybeLocaleOf(context), ), ); + AssetPicker.setInternalExceptionHandler( + pickerConfig.internalExceptionHandler, + ); final List? result = await Navigator.of( context, rootNavigator: useRootNavigator, @@ -111,6 +115,7 @@ class AssetPickerDelegate { pageRouteBuilder?.call(picker) ?? AssetPickerPageRoute>(builder: (_) => picker), ); + AssetPicker.setInternalExceptionHandler(null); return result; } @@ -138,12 +143,14 @@ class AssetPickerDelegate { Key? key, bool useRootNavigator = true, AssetPickerPageRouteBuilder>? pageRouteBuilder, + ExceptionHandler? internalExceptionHandler, }) async { await permissionCheck(); final Widget picker = AssetPicker( key: key, builder: delegate, ); + AssetPicker.setInternalExceptionHandler(internalExceptionHandler); final List? result = await Navigator.of( context, rootNavigator: useRootNavigator, @@ -151,6 +158,7 @@ class AssetPickerDelegate { pageRouteBuilder?.call(picker) ?? AssetPickerPageRoute>(builder: (_) => picker), ); + AssetPicker.setInternalExceptionHandler(null); return result; } @@ -165,8 +173,8 @@ class AssetPickerDelegate { try { PhotoManager.addChangeCallback(callback); PhotoManager.startChangeNotify(); - } catch (e) { - realDebugPrint('Error when registering assets callback: $e'); + } catch (e, s) { + handleException(e, s); } } @@ -181,8 +189,8 @@ class AssetPickerDelegate { try { PhotoManager.removeChangeCallback(callback); PhotoManager.stopChangeNotify(); - } catch (e) { - realDebugPrint('Error when unregistering assets callback: $e'); + } catch (e, s) { + handleException(e, s); } } diff --git a/lib/src/internal/methods.dart b/lib/src/internal/methods.dart index 053c37c2..b1d7d2e5 100644 --- a/lib/src/internal/methods.dart +++ b/lib/src/internal/methods.dart @@ -6,10 +6,36 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; +import '../constants/typedefs.dart'; +import 'singleton.dart'; + /// Log only when debugging. /// 只在调试模式打印 void realDebugPrint(dynamic message) { if (!kReleaseMode) { - log('$message'); + log('[wechat_assets_picker] $message'); + } +} + +/// @nodoc +void handleException(Object e, StackTrace s) { + final ExceptionHandler? handler = Singleton.internalExceptionHandler; + if (handler == null) { + FlutterError.presentError( + FlutterErrorDetails( + exception: e, + stack: s, + library: 'wechat_assets_picker', + silent: true, + informationCollector: () => [ + ErrorHint( + 'Note: Use `AssetPickerConfig.internalExceptionHandler` ' + 'to handle exceptions manually.', + ), + ], + ), + ); + } else { + handler(e, s); } } diff --git a/lib/src/internal/singleton.dart b/lib/src/internal/singleton.dart index fbdc9d69..9a42c468 100644 --- a/lib/src/internal/singleton.dart +++ b/lib/src/internal/singleton.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; +import '../constants/typedefs.dart'; import '../delegates/asset_picker_text_delegate.dart'; import '../delegates/sort_path_delegate.dart'; @@ -11,12 +12,13 @@ import '../delegates/sort_path_delegate.dart'; class Singleton { const Singleton._(); + static ExceptionHandler? internalExceptionHandler; static AssetPickerTextDelegate textDelegate = const AssetPickerTextDelegate(); static SortPathDelegate sortPathDelegate = SortPathDelegate.common; /// The last scroll position where the picker scrolled. /// /// See also: - /// * [AssetPickerBuilderDelegate.keepScrollOffset] + /// * [DefaultAssetPickerBuilderDelegate.keepScrollOffset] static ScrollPosition? scrollPosition; } diff --git a/lib/src/provider/asset_picker_provider.dart b/lib/src/provider/asset_picker_provider.dart index ed431eee..093859c5 100644 --- a/lib/src/provider/asset_picker_provider.dart +++ b/lib/src/provider/asset_picker_provider.dart @@ -12,6 +12,7 @@ import 'package:provider/provider.dart'; import '../constants/constants.dart'; import '../delegates/sort_path_delegate.dart'; +import '../internal/methods.dart'; import '../internal/singleton.dart'; import '../models/path_wrapper.dart'; @@ -245,7 +246,7 @@ class DefaultAssetPickerProvider Future.delayed(initializeDelayDuration, () async { await getPaths(); await getAssetsFromCurrentPath(); - }); + }).catchError(handleException); } @visibleForTesting @@ -414,40 +415,50 @@ class DefaultAssetPickerProvider Future getThumbnailFromPath( PathWrapper path, ) async { - if (requestType == RequestType.audio) { - return null; - } - final int assetCount = path.assetCount ?? await path.path.assetCountAsync; - if (assetCount == 0) { - return null; - } - final List assets = await path.path.getAssetListRange( - start: 0, - end: 1, - ); - if (assets.isEmpty) { - return null; - } - final AssetEntity asset = assets.single; - // Obtain the thumbnail only when the asset is image or video. - if (asset.type != AssetType.image && asset.type != AssetType.video) { + try { + if (requestType == RequestType.audio) { + return null; + } + final int assetCount = path.assetCount ?? await path.path.assetCountAsync; + if (assetCount == 0) { + return null; + } + final List assets = await path.path.getAssetListRange( + start: 0, + end: 1, + ); + if (assets.isEmpty) { + return null; + } + final AssetEntity asset = assets.single; + // Obtain the thumbnail only when the asset is image or video. + if (asset.type != AssetType.image && asset.type != AssetType.video) { + return null; + } + final Uint8List? data = await asset.thumbnailDataWithSize( + pathThumbnailSize, + ); + final int index = _paths.indexWhere( + (PathWrapper p) => p.path == path.path, + ); + if (index != -1) { + _paths[index] = _paths[index].copyWith(thumbnailData: data); + notifyListeners(); + } + return data; + } catch (e, s) { + handleException(e, s); return null; } - final Uint8List? data = await asset.thumbnailDataWithSize( - pathThumbnailSize, - ); - final int index = _paths.indexWhere( - (PathWrapper p) => p.path == path.path, - ); - if (index != -1) { - _paths[index] = _paths[index].copyWith(thumbnailData: data); - notifyListeners(); - } - return data; } Future getAssetCountFromPath(PathWrapper path) async { - final int assetCount = await path.path.assetCountAsync; + final int assetCount = await path.path.assetCountAsync.catchError( + (Object e, StackTrace s) { + handleException(e, s); + return 0; + }, + ); final int index = _paths.indexWhere( (PathWrapper p) => p == path, ); diff --git a/lib/src/widget/asset_picker.dart b/lib/src/widget/asset_picker.dart index 7e83313e..2aa374cd 100644 --- a/lib/src/widget/asset_picker.dart +++ b/lib/src/widget/asset_picker.dart @@ -7,8 +7,11 @@ import 'package:flutter/services.dart'; import 'package:photo_manager/photo_manager.dart'; import '../constants/config.dart'; +import '../constants/typedefs.dart'; import '../delegates/asset_picker_builder_delegate.dart'; import '../delegates/asset_picker_delegate.dart'; +import '../internal/methods.dart'; +import '../internal/singleton.dart'; import '../provider/asset_picker_provider.dart'; import 'asset_picker_page_route.dart'; @@ -19,6 +22,11 @@ class AssetPicker extends StatefulWidget { final AssetPickerBuilderDelegate builder; + /// Update the internal exception handler to the given [exceptionHandler]. + static void setInternalExceptionHandler(ExceptionHandler? exceptionHandler) { + Singleton.internalExceptionHandler = exceptionHandler; + } + /// Provide another [AssetPickerDelegate] which override with /// custom methods during handling the picking, /// e.g. to verify if arguments are properly set during picking calls. @@ -124,7 +132,7 @@ class AssetPickerState extends State> if (mounted) { setState(() {}); } - }); + }).catchError(handleException); } @override diff --git a/lib/src/widget/builder/audio_page_builder.dart b/lib/src/widget/builder/audio_page_builder.dart index d4fce683..d65b0309 100644 --- a/lib/src/widget/builder/audio_page_builder.dart +++ b/lib/src/widget/builder/audio_page_builder.dart @@ -53,7 +53,7 @@ class _AudioPageBuilderState extends State { @override void initState() { super.initState(); - openAudioFile(); + openAudioFile().catchError(handleException); } @override @@ -76,8 +76,6 @@ class _AudioPageBuilderState extends State { _controller = VideoPlayerController.network(url!); await _controller.initialize(); _controller.addListener(audioPlayerListener); - } catch (e) { - realDebugPrint('Error when opening audio file: $e'); } finally { isLoaded = true; if (mounted) { diff --git a/lib/src/widget/builder/image_page_builder.dart b/lib/src/widget/builder/image_page_builder.dart index d75eb4ab..76ef9e34 100644 --- a/lib/src/widget/builder/image_page_builder.dart +++ b/lib/src/widget/builder/image_page_builder.dart @@ -11,6 +11,7 @@ import 'package:photo_manager/photo_manager.dart'; import 'package:video_player/video_player.dart'; import '../../delegates/asset_picker_viewer_builder_delegate.dart'; +import '../../internal/methods.dart'; import 'locally_available_builder.dart'; class ImagePageBuilder extends StatefulWidget { @@ -57,12 +58,12 @@ class _ImagePageBuilderState extends State { if (!mounted || file == null) { return; } - final VideoPlayerController c = VideoPlayerController.file( + final VideoPlayerController controller = VideoPlayerController.file( file, videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), ); - setState(() => _controller = c); - c + setState(() => _controller = controller); + controller ..initialize() ..setVolume(0) ..addListener(() { @@ -159,7 +160,7 @@ class _ImagePageBuilderState extends State { // Initialize the video controller when the asset is a Live photo // and available for further use. if (!_isLocallyAvailable && _isLivePhoto) { - _initializeLivePhoto(); + _initializeLivePhoto().catchError(handleException); } _isLocallyAvailable = true; // TODO(Alex): Wait until `extended_image` support synchronized zooming. diff --git a/lib/src/widget/builder/locally_available_builder.dart b/lib/src/widget/builder/locally_available_builder.dart index f42cd1de..b5371c4d 100644 --- a/lib/src/widget/builder/locally_available_builder.dart +++ b/lib/src/widget/builder/locally_available_builder.dart @@ -35,7 +35,7 @@ class _LocallyAvailableBuilderState extends State { @override void initState() { super.initState(); - _checkLocallyAvailable(); + _checkLocallyAvailable().catchError(handleException); } Future _checkLocallyAvailable() async { @@ -61,7 +61,7 @@ class _LocallyAvailableBuilderState extends State { setState(() {}); } } - }); + }).catchError(handleException); } _progressHandler?.stream.listen((PMProgressState s) { realDebugPrint('Handling progress: $s.'); @@ -70,6 +70,8 @@ class _LocallyAvailableBuilderState extends State { if (mounted) { setState(() {}); } + } else if (s.state == PMRequestState.failed) { + handleException(s, StackTrace.current); } }); } diff --git a/lib/src/widget/builder/video_page_builder.dart b/lib/src/widget/builder/video_page_builder.dart index f9d39225..dfa83d6c 100644 --- a/lib/src/widget/builder/video_page_builder.dart +++ b/lib/src/widget/builder/video_page_builder.dart @@ -99,9 +99,6 @@ class _VideoPageBuilderState extends State { if (widget.hasOnlyOneVideoAndMoment) { controller.play(); } - } catch (e) { - realDebugPrint('Error when initialize video controller: $e'); - hasErrorWhenInitializing = true; } finally { if (mounted) { setState(() {}); @@ -205,7 +202,12 @@ class _VideoPageBuilderState extends State { ); } if (!_isLocallyAvailable && !_isInitializing) { - initializeVideoPlayerController(); + initializeVideoPlayerController().catchError( + (Object e, StackTrace s) { + handleException(e, s); + hasErrorWhenInitializing = true; + }, + ); } if (!hasLoaded) { return const SizedBox.shrink();