Skip to content

Commit

Permalink
refactor!: use library pattern (#238)
Browse files Browse the repository at this point in the history
  • Loading branch information
ABausG committed Mar 24, 2024
1 parent 8c8a79e commit b004b33
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 289 deletions.
File renamed without changes.
1 change: 0 additions & 1 deletion example/lib/main.dart
Expand Up @@ -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
Expand Down
288 changes: 4 additions & 284 deletions 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<bool?> saveWidgetData<T>(String id, T? data) {
return _channel.invokeMethod<bool>('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<bool?> 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<bool?> 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<void> 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<T?> getWidgetData<T>(String id, {T? defaultValue}) {
return _channel.invokeMethod<T>('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<bool?> setAppGroupId(String groupId) {
HomeWidget.groupId = groupId;
return _channel.invokeMethod('setAppGroupId', {'groupId': groupId});
}

/// Checks if the App was initially launched via the Widget
static Future<Uri?> initiallyLaunchedFromHomeWidget() {
return _channel
.invokeMethod<String>('initiallyLaunchedFromHomeWidget')
.then(_handleReceivedData);
}

/// Receives Updates if App Launched via the Widget
static Stream<Uri?> get widgetClicked {
return _eventChannel
.receiveBroadcastStream()
.map<Uri?>(_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<bool?> registerBackgroundCallback(
FutureOr<void> 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<bool?> registerInteractivityCallback(
FutureOr<void> Function(Uri?) callback,
) {
final args = <dynamic>[
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<RenderBox> rootElement =
RenderObjectToWidgetAdapter<RenderBox>(
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<bool>('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<List<HomeWidgetInfo>> getInstalledWidgets() async {
final List<dynamic>? result =
await _channel.invokeMethod('getInstalledWidgets');
return result
?.map(
(widget) =>
HomeWidgetInfo.fromMap(widget.cast<String, dynamic>()),
)
.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';

0 comments on commit b004b33

Please sign in to comment.