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.getBroadcastPendingIntent添加到您想要添加点击监听器的视图 
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');
        
      
            
            
            
