Flutter家居小部件控制插件home_widget_vconnex的使用
Flutter家居小部件控制插件home_widget_vconnex的使用
Home Widget
HomeWidget是一个用于在Android和iOS上创建HomeScreen小部件的插件。HomeWidget本身不支持直接使用Flutter编写小部件,而是需要使用原生代码来实现。然而,它提供了一个统一的接口来发送数据、检索数据和更新小部件。
平台设置
为了正确运行,需要进行一些平台特定的设置。请查看以下如何添加对Android和iOS的支持:
iOS
添加小部件到您的应用(Xcode)
通过选择<kbd>文件</kbd> > <kbd>新建</kbd> > <kbd>目标</kbd> > <kbd>小部件扩展</kbd>来添加一个小部件扩展。
添加组ID
您需要向应用和小部件扩展添加一个组ID。
注意:为了添加组ID,您需要一个付费的Apple开发者帐户。
转到您的Apple开发者账户,并添加一个新的组。 在XCode中将此组添加到Runner和小部件扩展:<kbd>签名与功能</kbd> > <kbd>应用组</kbd> > <kbd>+</kbd>。 (要切换您的应用和扩展,请更改目标)
同步CFBundleVersion(可选)
这一步是可选的,它会同步小部件扩展的构建版本与您的应用版本,这样在上传应用时不会收到App Store Connect的版本不匹配警告。
在您的Runner(应用)目标中,转到<kbd>构建阶段</kbd> > <kbd>+</kbd> > <kbd>新建运行脚本阶段</kbd>并添加以下脚本:
generatedPath="$SRCROOT/Flutter/Generated.xcconfig"
versionNumber=$(grep FLUTTER_BUILD_NAME $generatedPath | cut -d '=' -f2)
buildNumber=$(grep FLUTTER_BUILD_NUMBER $generatedPath | cut -d '=' -f2)
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "$SRCROOT/HomeExampleWidget/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $versionNumber" "$SRCROOT/HomeExampleWidget/Info.plist"
将HomeExampleWidget
替换为您创建的小部件扩展文件夹的名称。
编写您的小部件
检查示例应用以获取小部件的实现。 有关如何为iOS 14编写小部件的详细概述,请参阅Apple开发人员文档。 为了访问从Flutter发送的数据,可以使用以下代码:
let data = UserDefaults.init(suiteName:"YOUR_GROUP_ID")
Android (Jetpack Glance)
将Jetpack Glance作为依赖项添加到您的应用Gradle文件
implementation 'androidx.glance:glance-appwidget:LATEST-VERSION'
在android/app/src/main/res/xml
中创建小部件配置
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="40dp"
android:minHeight="40dp"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="10000">
</appwidget-provider>
将WidgetReceiver添加到AndroidManifest
<receiver android:name=".glance.HomeWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/home_widget_glance_example" />
</receiver>
创建WidgetReceiver
为了自动更新,您应该继承自HomeWidgetGlanceWidgetReceiver
您的接收器应如下所示:
package es.antonborri.home_widget_example.glance
import HomeWidgetGlanceWidgetReceiver
class HomeWidgetReceiver : HomeWidgetGlanceWidgetReceiver<HomeWidgetGlanceAppWidget>() {
override val glanceAppWidget = HomeWidgetGlanceAppWidget()
}
构建您的AppWidget
class HomeWidgetGlanceAppWidget : GlanceAppWidget() {
/**
* 需要用于更新
*/
override val stateDefinition = HomeWidgetGlanceStateDefinition()
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceContent(context, currentState())
}
}
@Composable
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
// 使用数据访问保存的数据
val data = currentState.preferences
// 构建您的组合式小部件
Column(
// ...
)
}
}
Android (XML)
在android/app/src/main/res/layout
中创建小部件布局
在android/app/src/main/res/xml
中创建小部件配置
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/example_layout"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>
将WidgetReceiver添加到AndroidManifest
<receiver android:name="HomeWidgetExampleProvider" android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/home_widget_example" />
</receiver>
编写您的WidgetProvider
为了方便起见,您可以扩展HomeWidgetProvider
,这将给您访问SharedPreferences
对象中的数据的方法。
如果您不想使用这种方法,可以通过以下方式访问数据:
import es.antonborri.home_widget.HomeWidgetPlugin
...
HomeWidgetPlugin.getData(context)
这将给您访问相同的SharedPreferences
更多关于如何创建和配置Android小部件的信息,请参阅Android开发者页面上的指南。
使用
设置
iOS
对于iOS,您需要调用HomeWidget.setAppGroupId('YOUR_GROUP_ID');
如果没有这一步,您将无法在应用和小部件之间共享数据,并且对saveWidgetData
和getWidgetData
的调用将返回错误。
保存数据
为了保存数据,调用HomeWidget.saveWidgetData<String>('id', data)
。
更新小部件
为了强制重新加载HomeScreenWidget,您需要调用:
HomeWidget.updateWidget(
name: 'HomeWidgetExampleProvider',
androidName: 'HomeWidgetExampleProvider',
iOSName: 'HomeWidgetExample',
qualifiedAndroidName: 'com.example.app.HomeWidgetExampleProvider',
);
Android名称将根据qualifiedAndroidName
进行选择,如果未提供则回退到<packageName>.androidName
,如果仍然未提供,则回退到<packageName>.name
。
这个名称需要等于WidgetProvider
的类名。
iOS名称将根据iOSName
进行选择,如果未提供则回退到name
。
这个名称需要等于您小部件中指定的类型。
Android (Jetpack Glance)
如果您遵循了指南并使用了HomeWidgetGlanceWidgetReceiver
作为接收器,HomeWidgetGlanceStateDefinition
作为AppWidgetStateDefinition,currentState()
在组合视图中,currentState.preferences
用于数据访问,则无需进一步工作。
Android (XML)
调用HomeWidget.updateWidget
只会通知指定的提供程序。
要使用此提供程序更新小部件,
从提供程序更新它们,像这样:
class HomeWidgetExampleProvider : HomeWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) {
appWidgetIds.forEach { widgetId ->
val views = RemoteViews(context.packageName, R.layout.example_layout).apply {
// ...
}
// 更新小部件
appWidgetManager.updateAppWidget(widgetId, views)
}
}
}
检索数据
要检索保存在小部件中的当前数据,请调用HomeWidget.getWidgetData<String>('id', defaultValue: data)
。
交互式小部件
Android和iOS(从iOS 17开始)允许小部件具有按钮等交互元素。
Dart
- 写一个静态函数,该函数接受一个Uri作为参数。当用户点击视图时,这个函数会被调用。
@pragma("vm:entry-point")
FutureOr<void> backgroundCallback(Uri data) async {
// 处理数据
...
}
@pragma('vm:entry-point')
必须放在callback
函数上方,以避免在发布模式下被树摇动。
- 通过调用以下代码注册回调函数:
HomeWidget.registerInteractivityCallback(backgroundCallback);
iOS
- 调整您的Podfile以将
home_widget
作为依赖项添加到您的小部件扩展
target 'YourWidgetExtension' do
use_frameworks!
use_modular_headers!
pod 'home_widget', :path => '.symlinks/plugins/home_widget/ios'
end
- 为了让带有后台回调的插件能够使用,请在AppDelegate的
application
函数中添加以下代码
if #available(iOS 17, *) {
HomeWidgetBackgroundWorker.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
}
}
- 在您的App Target(Runner)中创建一个自定义
AppIntent
,确保在目标成员面板中选择您的应用和小部件扩展。
在此意图中,您应该导入home_widget
并在perform
方法中调用HomeWidgetBackgroundWorker.run(url: url, appGroup: appGroup!)
。url
和appGroup
可以硬编码或从小部件传递为参数。
import AppIntents
import Flutter
import Foundation
import home_widget
@available(iOS 16, *)
public struct BackgroundIntent: AppIntent {
static public var title: LocalizedStringResource = "HomeWidget Background Intent"
@Parameter(title: "Widget URI")
var url: URL?
@Parameter(title: "AppGroup")
var appGroup: String?
public init() {}
public init(url: URL?, appGroup: String?) {
self.url = url
self.appGroup = appGroup
}
public func perform() async throws -> some IntentResult {
await HomeWidgetBackgroundWorker.run(url: url, appGroup: appGroup!)
return .result()
}
}
- 向您的小部件添加一个按钮。这个按钮可能被版本检查封装。传递一个之前创建的
AppIntent
实例。
Button(
intent: BackgroundIntent(
url: URL(string: "homeWidgetExample://titleClicked"), appGroup: widgetGroupId)
) {
Text(entry.title).bold().font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
}.buttonStyle(.plain)
- 使用当前设置,只要应用程序仍在后台,小部件现在就是交互式的。如果您希望小部件能够唤醒应用程序,需要向您的
AppIntent
文件添加以下代码
@available(iOS 16, *)
@available(iOSApplicationExtension, unavailable)
extension BackgroundIntent: ForegroundContinuableIntent {}
这段代码告诉系统始终在应用中执行意图,而不是在附加到小部件的进程中执行。请注意,这将使用正常的主要入口点启动您的Flutter应用,这意味着您的整个应用可能会在后台运行。为了避免这种情况,您应该在runApp
中构建的第一个小部件内添加检查,以仅在应用在后台启动时执行必要的调用/设置。
Android Jetpack Glance
- 向
AndroidManifest.xml
文件添加必要的接收器和服务
<receiver android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver" android:exported="true">
<intent-filter>
<action android:name="es.antonborri.home_widget.action.BACKGROUND" />
</intent-filter>
</receiver>
<service android:name="es.antonborri.home_widget.HomeWidgetBackgroundService"
android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true"/>
- 创建一个自定义动作
class InteractiveAction : ActionCallback {
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("homeWidgetExample://titleClicked"))
backgroundIntent.send()
}
}
- 将操作作为修改器添加到视图
Text(
title,
style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold),
modifier = GlanceModifier.clickable(onClick = actionRunCallback<InteractiveAction>()),
)
Android XML
- 向
AndroidManifest.xml
文件添加必要的接收器和服务
<receiver android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver" android:exported="true">
<intent-filter>
<action android:name="es.antonborri.home_widget.action.BACKGROUND" />
</intent-filter>
</receiver>
<service android:name="es.antonborri.home_widget.HomeWidgetBackgroundService"
android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true"/>
- 将
HomeWidgetBackgroundIntent.getBroadcast
PendingIntent
添加到您想要添加点击监听器的视图
val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(
context,
Uri.parse("homeWidgetExample://titleClicked")
)
setOnClickPendingIntent(R.id.widget_title, backgroundIntent)
使用Flutter小部件的图像
在某些情况下,您可能不想重写原生框架中的UI代码。
Dart
例如,假设您有一个使用CustomPaint
配置的图表:
class LineChart extends StatelessWidget {
const LineChart({
super.key,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: LineChartPainter(),
child: const SizedBox(
height: 200,
width: 200,
),
);
}
}
重写代码以在Android和iOS上创建此图表可能会很耗时。 相反,您可以生成Flutter小部件的png文件并将其保存到Flutter应用和Home Screen Widget之间的共享容器中。
var path = await HomeWidget.renderFlutterWidget(
const LineChart(),
key: 'lineChart',
logicalSize: const Size(400, 400),
);
LineChart()
是将作为图像渲染的小部件。key
是存储文件路径的设备上的键值存储中的键,以便在原生端轻松检索。
iOS
要检索图像并在小部件中显示它,您可以使用以下SwiftUI代码:
- 在您的
TimelineEntry
结构体中添加一个属性以检索路径:
struct MyEntry: TimelineEntry {
…
let lineChartPath: String
}
- 在
getSnapshot
中从UserDefaults
获取路径:
func getSnapshot(
...
let lineChartPath = userDefaults?.string(forKey: "lineChart") ?? "No screenshot available"
- 创建一个
View
以显示图表并基于小部件的displaySize
调整图像大小:
struct WidgetEntryView : View {
…
var ChartImage: some View {
if let uiImage = UIImage(contentsOfFile: entry.lineChartPath) {
let image = Image(uiImage: uiImage)
.resizable()
.frame(width: entry.displaySize.height*0.5, height: entry.displaySize.height*0.5, alignment: .center)
return AnyView(image)
}
print("The image file could not be loaded")
return AnyView(EmptyView())
}
…
}
- 在小部件的
View
的主体中显示图表:
VStack {
Text(entry.title)
Text(entry.description)
ChartImage
}
Android (Jetpack Glance)
// 访问数据
val data = currentState.preferences
// 获取路径
val imagePath = data.getString("lineChart", null)
// 将图像添加到组合树
imagePath?.let {
val bitmap = BitmapFactory.decodeFile(it)
Image(androidx.glance.ImageProvider(bitmap), null)
}
Android (XML)
- 在您的xml文件中添加一个图像UI元素:
<ImageView
android:id="@+id/widget_image"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_below="@+id/headline_description"
android:layout_alignBottom="@+id/headline_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="-134dp"
android:layout_weight="1"
android:adjustViewBounds="true"
android:background="@android:color/white"
android:scaleType="fitCenter"
android:src="@android:drawable/star_big_on"
android:visibility="visible"
tools:visibility="visible" />
- 更新您的Kotlin代码以获取图表图像并将其放入小部件中,如果存在的话。
class NewsWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
for (appWidgetId in appWidgetIds) {
// 获取SharedPreferences引用
val widgetData = HomeWidgetPlugin.getData(context)
val views = RemoteViews(context.packageName, R.layout.news_widget).apply {
// 获取图表图像并将其放入小部件中,如果存在的话
val imagePath = widgetData.getString("lineChart", null)
val imageFile = File(imagePath)
val imageExists = imageFile.exists()
if (imageExists) {
val myBitmap: Bitmap = BitmapFactory.decodeFile(imageFile.absolutePath)
setImageViewBitmap(R.id.widget_image, myBitmap)
} else {
println("image not found!, looked @: $imagePath")
}
// 结束新代码
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
启动应用并检测哪个小部件被点击
要检测应用是否由点击小部件启动,可以调用HomeWidget.initiallyLaunchedFromHomeWidget()
。如果应用已经在后台运行,可以通过监听HomeWidget.widgetClicked
接收这些事件。这两种方法都会提供Uris,因此您可以轻松地从小部件向应用发送数据以导航到内容页面。
为了使这些方法正常工作,您需要遵循以下步骤:
iOS
在您的小部件组件中添加.widgetUrl
。
Text(entry.message)
.font(.body)
.widgetURL(URL(string: "homeWidgetExample://message?message=\(entry.message)&homeWidget"))
为了只检测小部件链接,您需要在URL中添加查询参数homeWidget
。
Android Jetpack Glance
在AndroidManifest
的Activity
部分添加IntentFilter
<intent-filter>
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
</intent-filter>
向您的小部件添加以下修饰符(从HomeWidget导入)
Text(
message,
style = TextStyle(fontSize = 18.sp),
modifier = GlanceModifier.clickable(
onClick = actionStartActivity<MainActivity>(
context,
Uri.parse("homeWidgetExample://message?message=$message")
)
)
)
Android XML
在AndroidManifest
的Activity
部分添加IntentFilter
<intent-filter>
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
</intent-filter>
在您的WidgetProvider
中使用HomeWidgetLaunchIntent.getActivity
向您的视图添加PendingIntent
val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity(
context,
MainActivity::class.java,
Uri.parse("homeWidgetExample://message?message=$message"))
setOnClickPendingIntent(R.id.widget_message, pendingIntentWithData)
背景更新
由于HomeWidget的方法是静态的,因此可以在后台使用HomeWidget来更新小部件,即使应用在后台运行也是如此。
示例应用使用了flutter_workmanager插件来实现这一点。 请遵循flutter_workmanager(或您首选的后台代码执行插件)的设置说明。最重要的是确保在iOS中注册插件,以便能够与HomeWidget插件通信。 如果是flutter_workmanager,这可以通过在AppDelegate.swift中添加以下代码来实现:
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
}
请求固定小部件
请求将小部件固定(添加)到用户的主屏幕。
HomeWidget.requestPinWidget(
name: 'HomeWidgetExampleProvider',
androidName: 'HomeWidgetExampleProvider',
qualifiedAndroidName: 'com.example.app.HomeWidgetExampleProvider',
);
此方法仅支持Android,API 26+。 如果您想检查当前设备是否支持,请使用:
HomeWidget.isRequestPinWidgetSupported();
更多关于Flutter家居小部件控制插件home_widget_vconnex的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter家居小部件控制插件home_widget_vconnex的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
home_widget_vconnex
是一个用于在 Flutter 应用中与家居小部件(Home Widget)进行交互的插件。它允许你从 Flutter 应用中更新和控制 Android 和 iOS 设备上的家居小部件。以下是如何使用 home_widget_vconnex
插件的基本步骤:
1. 添加依赖
首先,你需要在 pubspec.yaml
文件中添加 home_widget_vconnex
插件的依赖:
dependencies:
flutter:
sdk: flutter
home_widget_vconnex: ^latest_version
运行 flutter pub get
以获取依赖。
2. 配置 Android 和 iOS
Android
在 AndroidManifest.xml
文件中添加以下权限和接收器:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".MyApplication"
android:icon="@mipmap/ic_launcher"
android:label="My App">
<receiver
android:name="com.example.app.MyWidgetProvider"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/my_widget_info" />
</receiver>
</application>
</manifest>
iOS
在 Info.plist
文件中添加以下内容:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
3. 创建 Widget 布局
在 lib
目录下创建 widget
文件夹,并在其中创建 my_widget.dart
文件,定义一个简单的家居小部件:
import 'package:flutter/material.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello from Home Widget!'),
),
),
);
}
}
4. 初始化 Home Widget
在你的 main.dart
文件中初始化 home_widget_vconnex
:
import 'package:flutter/material.dart';
import 'package:home_widget_vconnex/home_widget_vconnex.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await HomeWidgetVconnex.init();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Home Widget Example'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
HomeWidgetVconnex.updateWidget('key', 'value');
},
child: Text('Update Widget'),
),
),
),
);
}
}
5. 更新 Widget
你可以使用 HomeWidgetVconnex.updateWidget
方法来更新家居小部件。例如:
HomeWidgetVconnex.updateWidget('key', 'value');
6. 处理 Widget 交互
你可以通过 HomeWidgetVconnex
处理小部件的交互事件。例如:
HomeWidgetVconnex.registerCallback('my_callback', (String data) {
print('Callback received: $data');
});
7. 运行应用
现在你可以运行应用并测试家居小部件的更新和交互功能。
8. 调试和测试
在开发和测试过程中,你可以使用 HomeWidgetVconnex.debugLog
来输出调试信息:
HomeWidgetVconnex.debugLog('Debug message');