Flutter家居小部件控制插件home_widget_vconnex的使用

Flutter家居小部件控制插件home_widget_vconnex的使用

Home Widget

Pub Likes Popularity Pub Points Build Codecov

HomeWidget是一个用于在Android和iOS上创建HomeScreen小部件的插件。HomeWidget本身不支持直接使用Flutter编写小部件,而是需要使用原生代码来实现。然而,它提供了一个统一的接口来发送数据、检索数据和更新小部件。

平台设置

为了正确运行,需要进行一些平台特定的设置。请查看以下如何添加对Android和iOS的支持:

iOS
添加小部件到您的应用(Xcode)

通过选择<kbd>文件</kbd> > <kbd>新建</kbd> > <kbd>目标</kbd> > <kbd>小部件扩展</kbd>来添加一个小部件扩展。

Widget Extension

添加组ID

您需要向应用和小部件扩展添加一个组ID。

注意:为了添加组ID,您需要一个付费的Apple开发者帐户。

转到您的Apple开发者账户,并添加一个新的组。 在XCode中将此组添加到Runner和小部件扩展:<kbd>签名与功能</kbd> > <kbd>应用组</kbd> > <kbd>+</kbd>。 (要切换您的应用和扩展,请更改目标)

Build Targets

同步CFBundleVersion(可选)

这一步是可选的,它会同步小部件扩展的构建版本与您的应用版本,这样在上传应用时不会收到App Store Connect的版本不匹配警告。

Build Phases

在您的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&lt;HomeWidgetGlanceAppWidget&gt;() {
    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'); 如果没有这一步,您将无法在应用和小部件之间共享数据,并且对saveWidgetDatagetWidgetData的调用将返回错误。

保存数据

为了保存数据,调用HomeWidget.saveWidgetData&lt;String&gt;('id', data)

更新小部件

为了强制重新加载HomeScreenWidget,您需要调用:

HomeWidget.updateWidget(
    name: 'HomeWidgetExampleProvider',
    androidName: 'HomeWidgetExampleProvider',
    iOSName: 'HomeWidgetExample',
    qualifiedAndroidName: 'com.example.app.HomeWidgetExampleProvider',
);

Android名称将根据qualifiedAndroidName进行选择,如果未提供则回退到&lt;packageName&gt;.androidName,如果仍然未提供,则回退到&lt;packageName&gt;.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&lt;String&gt;('id', defaultValue: data)

交互式小部件

Android和iOS(从iOS 17开始)允许小部件具有按钮等交互元素。

Dart
  1. 写一个静态函数,该函数接受一个Uri作为参数。当用户点击视图时,这个函数会被调用。
@pragma("vm:entry-point")
FutureOr&lt;void&gt; backgroundCallback(Uri data) async {
  // 处理数据
  ...
}

@pragma('vm:entry-point')必须放在callback函数上方,以避免在发布模式下被树摇动。

  1. 通过调用以下代码注册回调函数:
HomeWidget.registerInteractivityCallback(backgroundCallback);
iOS
  1. 调整您的Podfile以将home_widget作为依赖项添加到您的小部件扩展
target 'YourWidgetExtension' do
   use_frameworks!
   use_modular_headers!

   pod 'home_widget', :path => '.symlinks/plugins/home_widget/ios'
end
  1. 为了让带有后台回调的插件能够使用,请在AppDelegate的application函数中添加以下代码
if #available(iOS 17, *) {
 HomeWidgetBackgroundWorker.setPluginRegistrantCallback { registry in
     GeneratedPluginRegistrant.register(with: registry)
 }
}
  1. 在您的App Target(Runner)中创建一个自定义AppIntent,确保在目标成员面板中选择您的应用和小部件扩展。

Target Membership

在此意图中,您应该导入home_widget并在perform方法中调用HomeWidgetBackgroundWorker.run(url: url, appGroup: appGroup!)urlappGroup可以硬编码或从小部件传递为参数。

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()
   }
}   
  1. 向您的小部件添加一个按钮。这个按钮可能被版本检查封装。传递一个之前创建的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)
  1. 使用当前设置,只要应用程序仍在后台,小部件现在就是交互式的。如果您希望小部件能够唤醒应用程序,需要向您的AppIntent文件添加以下代码
@available(iOS 16, *)
@available(iOSApplicationExtension, unavailable)
extension BackgroundIntent: ForegroundContinuableIntent {}

这段代码告诉系统始终在应用中执行意图,而不是在附加到小部件的进程中执行。请注意,这将使用正常的主要入口点启动您的Flutter应用,这意味着您的整个应用可能会在后台运行。为了避免这种情况,您应该在runApp中构建的第一个小部件内添加检查,以仅在应用在后台启动时执行必要的调用/设置。

Android Jetpack Glance
  1. 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"/>
  1. 创建一个自定义动作
class InteractiveAction : ActionCallback {
     override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
      val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("homeWidgetExample://titleClicked"))
      backgroundIntent.send()
    }
}
  1. 将操作作为修改器添加到视图
Text(
     title,
     style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold),
     modifier = GlanceModifier.clickable(onClick = actionRunCallback&lt;InteractiveAction&gt;()),
)
Android XML
  1. 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"/>
  1. 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,
      ),
    );
  }
}

Line Chart

重写代码以在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代码:

  1. 在您的TimelineEntry结构体中添加一个属性以检索路径:
struct MyEntry: TimelineEntry {
    …
    let lineChartPath: String
}
  1. getSnapshot中从UserDefaults获取路径:
func getSnapshot(
    ...
    let lineChartPath = userDefaults?.string(forKey: "lineChart") ?? "No screenshot available"
  1. 创建一个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())
    }
…
}
  1. 在小部件的View的主体中显示图表:
VStack {
        Text(entry.title)
        Text(entry.description)
        ChartImage
    }

Widget Entry View

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)
  1. 在您的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" />
  1. 更新您的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)&amp;homeWidget"))

为了只检测小部件链接,您需要在URL中添加查询参数homeWidget

Android Jetpack Glance

AndroidManifestActivity部分添加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&lt;MainActivity&gt;(
       context,
       Uri.parse("homeWidgetExample://message?message=$message")
     )
   )
)
Android XML

AndroidManifestActivity部分添加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

1 回复

更多关于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');
回到顶部