From b004b33c7b393bea9aeff5ccb61aa672516f4ab0 Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Sun, 24 Mar 2024 22:38:29 +0100 Subject: [PATCH] refactor!: use library pattern (#238) --- lib/dart_test.yaml => dart_test.yaml | 0 example/lib/main.dart | 1 - lib/home_widget.dart | 288 +----------------- lib/src/home_widget.dart | 284 +++++++++++++++++ .../home_widget_callback_dispatcher.dart | 0 lib/{ => src}/home_widget_info.dart | 0 test/background_test.dart | 2 +- test/home_widget_info_test.dart | 2 +- test/home_widget_test.dart | 2 - 9 files changed, 290 insertions(+), 289 deletions(-) rename lib/dart_test.yaml => dart_test.yaml (100%) create mode 100644 lib/src/home_widget.dart rename lib/{ => src}/home_widget_callback_dispatcher.dart (100%) rename lib/{ => src}/home_widget_info.dart (100%) diff --git a/lib/dart_test.yaml b/dart_test.yaml similarity index 100% rename from lib/dart_test.yaml rename to dart_test.yaml diff --git a/example/lib/main.dart b/example/lib/main.dart index 1ddb825f..7abd4464 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:home_widget/home_widget.dart'; -import 'package:home_widget/home_widget_info.dart'; import 'package:workmanager/workmanager.dart'; /// Used for Background Updates using Workmanager Plugin diff --git a/lib/home_widget.dart b/lib/home_widget.dart index e89a2505..f1ab38b1 100644 --- a/lib/home_widget.dart +++ b/lib/home_widget.dart @@ -1,285 +1,5 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:ui' as ui; +library home_widget; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:home_widget/home_widget_callback_dispatcher.dart'; -import 'package:home_widget/home_widget_info.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:path_provider_foundation/path_provider_foundation.dart'; - -/// A Flutter Plugin to simplify setting up and communicating with HomeScreenWidgets -class HomeWidget { - static const MethodChannel _channel = MethodChannel('home_widget'); - static const EventChannel _eventChannel = EventChannel('home_widget/updates'); - - /// The AppGroupId used for iOS Widgets - static String? groupId; - - /// Save [data] to the Widget Storage - /// - /// Returns whether the data was saved or not - static Future saveWidgetData(String id, T? data) { - return _channel.invokeMethod('saveWidgetData', { - 'id': id, - 'data': data, - }); - } - - /// Updates the HomeScreen Widget - /// - /// Android Widgets will look for [qualifiedAndroidName] then [androidName] and then for [name] - /// iOS Widgets will look for [iOSName] and then for [name] - /// - /// [qualifiedAndroidName] will use the name as is to find the WidgetProvider - /// [androidName] must match the classname of the WidgetProvider, prefixed by the package name - /// The name of the iOS Widget must match the kind specified when creating the Widget - static Future updateWidget({ - String? name, - String? androidName, - String? iOSName, - String? qualifiedAndroidName, - }) { - return _channel.invokeMethod('updateWidget', { - 'name': name, - 'android': androidName, - 'ios': iOSName, - 'qualifiedAndroidName': qualifiedAndroidName, - }); - } - - /// Determines whether pinning HomeScreen Widget is supported. - static Future isRequestPinWidgetSupported() { - return _channel.invokeMethod('isRequestPinWidgetSupported'); - } - - /// Requests to Pin (Add) the HomeScreenWidget to the User's Home Screen - /// - /// This is supported only on some Android Launchers and only with Android API 26+ - /// - /// Android Widgets will look for [qualifiedAndroidName] then [androidName] and then for [name] - /// There is no iOS alternative. - /// - /// [qualifiedAndroidName] will use the name as is to find the WidgetProvider. - /// [androidName] must match the classname of the WidgetProvider, prefixed by the package name. - static Future requestPinWidget({ - String? name, - String? androidName, - // String? iOSName, - String? qualifiedAndroidName, - }) { - return _channel.invokeMethod('requestPinWidget', { - 'name': name, - 'android': androidName, - // 'ios': iOSName, - 'qualifiedAndroidName': qualifiedAndroidName, - }); - } - - /// Returns Data saved with [saveWidgetData] - /// [id] of Data Saved - /// [defaultValue] value to use if no data was found - static Future getWidgetData(String id, {T? defaultValue}) { - return _channel.invokeMethod('getWidgetData', { - 'id': id, - 'defaultValue': defaultValue, - }); - } - - /// Required on iOS to set the AppGroupId [groupId] in order to ensure - /// communication between the App and the Widget Extension - static Future setAppGroupId(String groupId) { - HomeWidget.groupId = groupId; - return _channel.invokeMethod('setAppGroupId', {'groupId': groupId}); - } - - /// Checks if the App was initially launched via the Widget - static Future initiallyLaunchedFromHomeWidget() { - return _channel - .invokeMethod('initiallyLaunchedFromHomeWidget') - .then(_handleReceivedData); - } - - /// Receives Updates if App Launched via the Widget - static Stream get widgetClicked { - return _eventChannel - .receiveBroadcastStream() - .map(_handleReceivedData); - } - - static Uri? _handleReceivedData(dynamic value) { - if (value != null) { - if (value is String) { - try { - return Uri.parse(value); - } on FormatException { - debugPrint('Received Data($value) is not parsable into an Uri'); - } - } - return Uri(); - } else { - return null; - } - } - - /// Register a callback that gets called when clicked on a specific View in a HomeWidget - /// This enables having Interactive Widgets that can call Dart Code - /// More Info on setting this up in the README - @Deprecated('Use `registerInteractivityCallback` instead') - static Future registerBackgroundCallback( - FutureOr Function(Uri?) callback, - ) => - registerInteractivityCallback(callback); - - /// Register a callback that gets called when clicked on a specific View in a HomeWidget - /// This enables having Interactive Widgets that can call Dart Code - /// More Info on setting this up in the README - static Future registerInteractivityCallback( - FutureOr Function(Uri?) callback, - ) { - final args = [ - ui.PluginUtilities.getCallbackHandle(callbackDispatcher)?.toRawHandle(), - ui.PluginUtilities.getCallbackHandle(callback)?.toRawHandle(), - ]; - return _channel.invokeMethod('registerBackgroundCallback', args); - } - - /// Generate a screenshot based on a given widget. - /// This method renders the widget to an image (png) file with the provided filename. - /// The png file is saved to the App Group container and the full path is returned as a string. - /// The filename is saved to UserDefaults using the provided key. - static Future renderFlutterWidget( - Widget widget, { - required String key, - Size logicalSize = const Size(200, 200), - double pixelRatio = 1, - }) async { - /// finding the widget in the current context by the key. - final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary(); - - /// create a new pipeline owner - final PipelineOwner pipelineOwner = PipelineOwner(); - - /// create a new build owner - final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager()); - - try { - final RenderView renderView = RenderView( - view: ui.PlatformDispatcher.instance.implicitView!, - child: RenderPositionedBox( - alignment: Alignment.center, - child: repaintBoundary, - ), - configuration: ViewConfiguration( - size: logicalSize, - devicePixelRatio: 1.0, - ), - ); - - /// setting the rootNode to the renderview of the widget - pipelineOwner.rootNode = renderView; - - /// setting the renderView to prepareInitialFrame - renderView.prepareInitialFrame(); - - /// setting the rootElement with the widget that has to be captured - final RenderObjectToWidgetElement rootElement = - RenderObjectToWidgetAdapter( - container: repaintBoundary, - child: Directionality( - textDirection: TextDirection.ltr, - child: Column( - // image is center aligned - mainAxisAlignment: MainAxisAlignment.center, - children: [ - widget, - ], - ), - ), - ).attachToRenderTree(buildOwner); - - ///adding the rootElement to the buildScope - buildOwner.buildScope(rootElement); - - ///adding the rootElement to the buildScope - buildOwner.buildScope(rootElement); - - /// finalize the buildOwner - buildOwner.finalizeTree(); - - ///Flush Layout - pipelineOwner.flushLayout(); - - /// Flush Compositing Bits - pipelineOwner.flushCompositingBits(); - - /// Flush paint - pipelineOwner.flushPaint(); - - final ui.Image image = - await repaintBoundary.toImage(pixelRatio: pixelRatio); - - /// The raw image is converted to byte data. - final ByteData? byteData = - await image.toByteData(format: ui.ImageByteFormat.png); - - try { - late final String? directory; - - // coverage:ignore-start - if (Platform.isIOS) { - final PathProviderFoundation provider = PathProviderFoundation(); - assert( - HomeWidget.groupId != null, - 'No groupId defined. Did you forget to call `HomeWidget.setAppGroupId`', - ); - directory = await provider.getContainerPath( - appGroupIdentifier: HomeWidget.groupId!, - ); - } else { - // coverage:ignore-end - directory = (await getApplicationSupportDirectory()).path; - } - - final String path = '$directory/home_widget/$key.png'; - final File file = File(path); - if (!await file.exists()) { - await file.create(recursive: true); - } - await file.writeAsBytes(byteData!.buffer.asUint8List()); - - // Save the filename to UserDefaults if a key was provided - _channel.invokeMethod('saveWidgetData', { - 'id': key, - 'data': path, - }); - - return path; - } catch (e) { - throw Exception('Failed to save screenshot to app group container: $e'); - } - } catch (e) { - throw Exception('Failed to render the widget: $e'); - } - } - - /// On iOS, returns a list of [HomeWidgetInfo] for each type of widget currently installed, - /// regardless of the number of instances. - /// On Android, returns a list of [HomeWidgetInfo] for each instance of each widget - /// currently pinned on the home screen. - /// Returns an empty list if no widgets are pinned. - static Future> getInstalledWidgets() async { - final List? result = - await _channel.invokeMethod('getInstalledWidgets'); - return result - ?.map( - (widget) => - HomeWidgetInfo.fromMap(widget.cast()), - ) - .toList() ?? - []; - } -} +export 'package:home_widget/src/home_widget.dart'; +export 'package:home_widget/src/home_widget_callback_dispatcher.dart'; +export 'package:home_widget/src/home_widget_info.dart'; diff --git a/lib/src/home_widget.dart b/lib/src/home_widget.dart new file mode 100644 index 00000000..3ac3ba44 --- /dev/null +++ b/lib/src/home_widget.dart @@ -0,0 +1,284 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:home_widget/home_widget.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path_provider_foundation/path_provider_foundation.dart'; + +/// A Flutter Plugin to simplify setting up and communicating with HomeScreenWidgets +class HomeWidget { + static const MethodChannel _channel = MethodChannel('home_widget'); + static const EventChannel _eventChannel = EventChannel('home_widget/updates'); + + /// The AppGroupId used for iOS Widgets + static String? groupId; + + /// Save [data] to the Widget Storage + /// + /// Returns whether the data was saved or not + static Future saveWidgetData(String id, T? data) { + return _channel.invokeMethod('saveWidgetData', { + 'id': id, + 'data': data, + }); + } + + /// Updates the HomeScreen Widget + /// + /// Android Widgets will look for [qualifiedAndroidName] then [androidName] and then for [name] + /// iOS Widgets will look for [iOSName] and then for [name] + /// + /// [qualifiedAndroidName] will use the name as is to find the WidgetProvider + /// [androidName] must match the classname of the WidgetProvider, prefixed by the package name + /// The name of the iOS Widget must match the kind specified when creating the Widget + static Future updateWidget({ + String? name, + String? androidName, + String? iOSName, + String? qualifiedAndroidName, + }) { + return _channel.invokeMethod('updateWidget', { + 'name': name, + 'android': androidName, + 'ios': iOSName, + 'qualifiedAndroidName': qualifiedAndroidName, + }); + } + + /// Determines whether pinning HomeScreen Widget is supported. + static Future isRequestPinWidgetSupported() { + return _channel.invokeMethod('isRequestPinWidgetSupported'); + } + + /// Requests to Pin (Add) the HomeScreenWidget to the User's Home Screen + /// + /// This is supported only on some Android Launchers and only with Android API 26+ + /// + /// Android Widgets will look for [qualifiedAndroidName] then [androidName] and then for [name] + /// There is no iOS alternative. + /// + /// [qualifiedAndroidName] will use the name as is to find the WidgetProvider. + /// [androidName] must match the classname of the WidgetProvider, prefixed by the package name. + static Future requestPinWidget({ + String? name, + String? androidName, + // String? iOSName, + String? qualifiedAndroidName, + }) { + return _channel.invokeMethod('requestPinWidget', { + 'name': name, + 'android': androidName, + // 'ios': iOSName, + 'qualifiedAndroidName': qualifiedAndroidName, + }); + } + + /// Returns Data saved with [saveWidgetData] + /// [id] of Data Saved + /// [defaultValue] value to use if no data was found + static Future getWidgetData(String id, {T? defaultValue}) { + return _channel.invokeMethod('getWidgetData', { + 'id': id, + 'defaultValue': defaultValue, + }); + } + + /// Required on iOS to set the AppGroupId [groupId] in order to ensure + /// communication between the App and the Widget Extension + static Future setAppGroupId(String groupId) { + HomeWidget.groupId = groupId; + return _channel.invokeMethod('setAppGroupId', {'groupId': groupId}); + } + + /// Checks if the App was initially launched via the Widget + static Future initiallyLaunchedFromHomeWidget() { + return _channel + .invokeMethod('initiallyLaunchedFromHomeWidget') + .then(_handleReceivedData); + } + + /// Receives Updates if App Launched via the Widget + static Stream get widgetClicked { + return _eventChannel + .receiveBroadcastStream() + .map(_handleReceivedData); + } + + static Uri? _handleReceivedData(dynamic value) { + if (value != null) { + if (value is String) { + try { + return Uri.parse(value); + } on FormatException { + debugPrint('Received Data($value) is not parsable into an Uri'); + } + } + return Uri(); + } else { + return null; + } + } + + /// Register a callback that gets called when clicked on a specific View in a HomeWidget + /// This enables having Interactive Widgets that can call Dart Code + /// More Info on setting this up in the README + @Deprecated('Use `registerInteractivityCallback` instead') + static Future registerBackgroundCallback( + FutureOr Function(Uri?) callback, + ) => + registerInteractivityCallback(callback); + + /// Register a callback that gets called when clicked on a specific View in a HomeWidget + /// This enables having Interactive Widgets that can call Dart Code + /// More Info on setting this up in the README + static Future registerInteractivityCallback( + FutureOr Function(Uri?) callback, + ) { + final args = [ + ui.PluginUtilities.getCallbackHandle(callbackDispatcher)?.toRawHandle(), + ui.PluginUtilities.getCallbackHandle(callback)?.toRawHandle(), + ]; + return _channel.invokeMethod('registerBackgroundCallback', args); + } + + /// Generate a screenshot based on a given widget. + /// This method renders the widget to an image (png) file with the provided filename. + /// The png file is saved to the App Group container and the full path is returned as a string. + /// The filename is saved to UserDefaults using the provided key. + static Future renderFlutterWidget( + Widget widget, { + required String key, + Size logicalSize = const Size(200, 200), + double pixelRatio = 1, + }) async { + /// finding the widget in the current context by the key. + final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary(); + + /// create a new pipeline owner + final PipelineOwner pipelineOwner = PipelineOwner(); + + /// create a new build owner + final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager()); + + try { + final RenderView renderView = RenderView( + view: ui.PlatformDispatcher.instance.implicitView!, + child: RenderPositionedBox( + alignment: Alignment.center, + child: repaintBoundary, + ), + configuration: ViewConfiguration( + size: logicalSize, + devicePixelRatio: 1.0, + ), + ); + + /// setting the rootNode to the renderview of the widget + pipelineOwner.rootNode = renderView; + + /// setting the renderView to prepareInitialFrame + renderView.prepareInitialFrame(); + + /// setting the rootElement with the widget that has to be captured + final RenderObjectToWidgetElement rootElement = + RenderObjectToWidgetAdapter( + container: repaintBoundary, + child: Directionality( + textDirection: TextDirection.ltr, + child: Column( + // image is center aligned + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget, + ], + ), + ), + ).attachToRenderTree(buildOwner); + + ///adding the rootElement to the buildScope + buildOwner.buildScope(rootElement); + + ///adding the rootElement to the buildScope + buildOwner.buildScope(rootElement); + + /// finalize the buildOwner + buildOwner.finalizeTree(); + + ///Flush Layout + pipelineOwner.flushLayout(); + + /// Flush Compositing Bits + pipelineOwner.flushCompositingBits(); + + /// Flush paint + pipelineOwner.flushPaint(); + + final ui.Image image = + await repaintBoundary.toImage(pixelRatio: pixelRatio); + + /// The raw image is converted to byte data. + final ByteData? byteData = + await image.toByteData(format: ui.ImageByteFormat.png); + + try { + late final String? directory; + + // coverage:ignore-start + if (Platform.isIOS) { + final PathProviderFoundation provider = PathProviderFoundation(); + assert( + HomeWidget.groupId != null, + 'No groupId defined. Did you forget to call `HomeWidget.setAppGroupId`', + ); + directory = await provider.getContainerPath( + appGroupIdentifier: HomeWidget.groupId!, + ); + } else { + // coverage:ignore-end + directory = (await getApplicationSupportDirectory()).path; + } + + final String path = '$directory/home_widget/$key.png'; + final File file = File(path); + if (!await file.exists()) { + await file.create(recursive: true); + } + await file.writeAsBytes(byteData!.buffer.asUint8List()); + + // Save the filename to UserDefaults if a key was provided + _channel.invokeMethod('saveWidgetData', { + 'id': key, + 'data': path, + }); + + return path; + } catch (e) { + throw Exception('Failed to save screenshot to app group container: $e'); + } + } catch (e) { + throw Exception('Failed to render the widget: $e'); + } + } + + /// On iOS, returns a list of [HomeWidgetInfo] for each type of widget currently installed, + /// regardless of the number of instances. + /// On Android, returns a list of [HomeWidgetInfo] for each instance of each widget + /// currently pinned on the home screen. + /// Returns an empty list if no widgets are pinned. + static Future> getInstalledWidgets() async { + final List? result = + await _channel.invokeMethod('getInstalledWidgets'); + return result + ?.map( + (widget) => + HomeWidgetInfo.fromMap(widget.cast()), + ) + .toList() ?? + []; + } +} diff --git a/lib/home_widget_callback_dispatcher.dart b/lib/src/home_widget_callback_dispatcher.dart similarity index 100% rename from lib/home_widget_callback_dispatcher.dart rename to lib/src/home_widget_callback_dispatcher.dart diff --git a/lib/home_widget_info.dart b/lib/src/home_widget_info.dart similarity index 100% rename from lib/home_widget_info.dart rename to lib/src/home_widget_info.dart diff --git a/test/background_test.dart b/test/background_test.dart index 0dd60429..cada33d6 100644 --- a/test/background_test.dart +++ b/test/background_test.dart @@ -3,7 +3,7 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:home_widget/home_widget_callback_dispatcher.dart'; +import 'package:home_widget/home_widget.dart'; Completer completer = Completer(); const backgroundChannel = MethodChannel('home_widget/background'); diff --git a/test/home_widget_info_test.dart b/test/home_widget_info_test.dart index 1b797936..c25e91cd 100644 --- a/test/home_widget_info_test.dart +++ b/test/home_widget_info_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:home_widget/home_widget_info.dart'; +import 'package:home_widget/home_widget.dart'; void main() { group('HomeWidgetInfo', () { diff --git a/test/home_widget_test.dart b/test/home_widget_test.dart index 79f47177..e2366399 100644 --- a/test/home_widget_test.dart +++ b/test/home_widget_test.dart @@ -8,8 +8,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:home_widget/home_widget.dart'; -import 'package:home_widget/home_widget_callback_dispatcher.dart'; -import 'package:home_widget/home_widget_info.dart'; import 'package:mocktail/mocktail.dart'; // ignore: depend_on_referenced_packages