flutter-home-screen-widget

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

flutter-home-screen-widgets

Flutter主屏幕小组件

Goal

目标

Implements native home screen widgets (iOS and Android) for a Flutter application using the
home_widget
package. It establishes data sharing between the Dart environment and native platforms via App Groups (iOS) and SharedPreferences (Android), enabling text updates and rendering Flutter UI components as images for native display. Assumes a pre-existing Flutter project environment with native build tools (Xcode and Android Studio) configured.
使用
home_widget
包为Flutter应用实现原生主屏幕小组件(支持iOS和Android)。通过iOS的App Groups和Android的SharedPreferences在Dart环境与原生平台之间建立数据共享,实现文本更新并将Flutter UI组件渲染为图片以供原生展示。前提是已配置好原生构建工具(Xcode和Android Studio)的现有Flutter项目环境。

Instructions

操作步骤

  1. Initialize Dependencies Add the
    home_widget
    package to the Flutter project.
    bash
    flutter pub add home_widget
    flutter pub get
  2. Decision Logic: Platform & Feature Selection Determine the target platforms and required widget capabilities. [BLOCKING] User Consultation BEFORE performing any implementation, you MUST ask:
    • "Which platforms are you targeting?"
    • "Do you need simple text or complex UI?"
    Flowchart:
    • If iOS -> Proceed to Step 4 (iOS Native Setup).
    • If Android -> Proceed to Step 5 (Android Native Setup).
    • If rendering Flutter UI as images -> Proceed to Step 6 after basic setup.
  3. Implement Dart Data Sharing Logic Create the Dart logic to save data to the native key/value store and trigger widget updates.
    dart
    import 'package:home_widget/home_widget.dart';
    
    // Replace with actual App Group ID for iOS
    const String appGroupId = 'group.com.yourcompany.app'; 
    const String iOSWidgetName = 'NewsWidgets';
    const String androidWidgetName = 'NewsWidget';
    
    Future<void> updateWidgetData(String title, String description) async {
      await HomeWidget.setAppGroupId(appGroupId);
      await HomeWidget.saveWidgetData<String>('headline_title', title);
      await HomeWidget.saveWidgetData<String>('headline_description', description);
      await HomeWidget.updateWidget(
        iOSName: iOSWidgetName,
        androidName: androidWidgetName,
      );
    }
  4. iOS Native Setup (If applicable)
    • Configure an App Group in Xcode for both the Runner target and the Widget Extension target.
    • Create a Widget Extension target in Xcode (e.g.,
      NewsWidgets
      ). Uncheck "Include Live Activity" and "Include Configuration Intent".
    • Implement the
      TimelineProvider
      and
      View
      in Swift:
    swift
    import WidgetKit
    import SwiftUI
    
    struct NewsArticleEntry: TimelineEntry {
        let date: Date
        let title: String
        let description: String
    }
    
    struct Provider: TimelineProvider {
        func placeholder(in context: Context) -> NewsArticleEntry {
            NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description")
        }
    
        func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
            let entry: NewsArticleEntry
            if context.isPreview {
                entry = placeholder(in: context)
            } else {
                // Replace with actual App Group ID
                let userDefaults = UserDefaults(suiteName: "group.com.yourcompany.app")
                let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
                let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
                entry = NewsArticleEntry(date: Date(), title: title, description: description)
            }
            completion(entry)
        }
    
        func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
            getSnapshot(in: context) { (entry) in
                let timeline = Timeline(entries: [entry], policy: .atEnd)
                completion(timeline)
            }
        }
    }
    
    struct NewsWidgetsEntryView : View {
        var entry: Provider.Entry
    
        var body: some View {
            VStack(alignment: .leading) {
                Text(entry.title).font(.headline)
                Text(entry.description).font(.subheadline)
            }
        }
    }
    Validate-and-Fix: Run
    flutter build ios --config-only
    to ensure the Flutter configuration syncs with the new Xcode targets. If build fails, verify the App Group ID matches exactly between Dart and Swift.
  5. Android Native Setup (If applicable)
    • Create an
      AppWidgetProvider
      in Android Studio (
      New -> Widget -> App Widget
      ).
    • Define the XML layout (
      res/layout/news_widget.xml
      ):
    xml
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/widget_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white">
        <TextView
            android:id="@+id/headline_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Title"
            android:textStyle="bold"
            android:textSize="20sp" />
        <TextView
            android:id="@+id/headline_description"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/headline_title"
            android:text="Description"
            android:textSize="16sp" />
    </RelativeLayout>
    • Implement the Kotlin Provider (
      NewsWidget.kt
      ):
    kotlin
    package com.yourdomain.yourapp
    
    import android.appwidget.AppWidgetManager
    import android.appwidget.AppWidgetProvider
    import android.content.Context
    import android.widget.RemoteViews
    import es.antonborri.home_widget.HomeWidgetPlugin
    
    class NewsWidget : AppWidgetProvider() {
        override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
            for (appWidgetId in appWidgetIds) {
                val widgetData = HomeWidgetPlugin.getData(context)
                val views = RemoteViews(context.packageName, R.layout.news_widget).apply {
                    val title = widgetData.getString("headline_title", null)
                    setTextViewText(R.id.headline_title, title ?: "No title set")
    
                    val description = widgetData.getString("headline_description", null)
                    setTextViewText(R.id.headline_description, description ?: "No description set")
                }
                appWidgetManager.updateAppWidget(appWidgetId, views)
            }
        }
    }
  6. Render Flutter Widgets as Images (Optional) If the user requires complex UI (like charts) on the widget, render the Flutter widget to a PNG and pass the file path. Dart Implementation:
    dart
    final _globalKey = GlobalKey();
    
    // Wrap your target widget with a RepaintBoundary/Key
    // Center(key: _globalKey, child: const LineChart())
    
    Future<void> renderAndSaveWidget() async {
      if (_globalKey.currentContext != null) {
        var path = await HomeWidget.renderFlutterWidget(
          const LineChart(),
          fileName: 'screenshot',
          key: 'filename',
          logicalSize: _globalKey.currentContext!.size,
          pixelRatio: MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
        );
        await HomeWidget.updateWidget(iOSName: iOSWidgetName, androidName: androidWidgetName);
      }
    }
    Native Image Loading (Android Example):
    kotlin
    // Inside RemoteViews apply block:
    val imageName = widgetData.getString("filename", null)
    val imageFile = java.io.File(imageName)
    if (imageFile.exists()) {
        val myBitmap = android.graphics.BitmapFactory.decodeFile(imageFile.absolutePath)
        setImageViewBitmap(R.id.widget_image, myBitmap)
    }
  1. 初始化依赖
    home_widget
    包添加到Flutter项目中。
bash
flutter pub add home_widget
flutter pub get
  1. 决策逻辑:平台与功能选择 确定目标平台和所需的小组件功能。 [阻塞步骤] 用户确认 在执行任何实现操作之前,你必须询问:
  • "你目标的平台是哪些?"
  • "你需要简单文本还是复杂UI?"
流程图:
  • 如果是iOS -> 进入步骤4(iOS原生配置)。
  • 如果是Android -> 进入步骤5(Android原生配置)。
  • 如果需要将Flutter UI渲染为图片 -> 完成基础配置后进入步骤6。
  1. 实现Dart数据共享逻辑 创建Dart逻辑以将数据保存到原生键值存储并触发小组件更新。
dart
import 'package:home_widget/home_widget.dart';

// 替换为iOS的实际App Group ID
const String appGroupId = 'group.com.yourcompany.app'; 
const String iOSWidgetName = 'NewsWidgets';
const String androidWidgetName = 'NewsWidget';

Future<void> updateWidgetData(String title, String description) async {
  await HomeWidget.setAppGroupId(appGroupId);
  await HomeWidget.saveWidgetData<String>('headline_title', title);
  await HomeWidget.saveWidgetData<String>('headline_description', description);
  await HomeWidget.updateWidget(
    iOSName: iOSWidgetName,
    androidName: androidWidgetName,
  );
}
  1. iOS原生配置(如适用)
  • 在Xcode中为Runner目标和Widget Extension目标配置App Group。
  • 在Xcode中创建Widget Extension目标(例如
    NewsWidgets
    )。取消勾选"Include Live Activity"和"Include Configuration Intent"。
  • 在Swift中实现
    TimelineProvider
    View
swift
import WidgetKit
import SwiftUI

struct NewsArticleEntry: TimelineEntry {
    let date: Date
    let title: String
    let description: String
}

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> NewsArticleEntry {
        NewsArticleEntry(date: Date(), title: "Placeholder Title", description: "Placeholder description")
    }

    func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
        let entry: NewsArticleEntry
        if context.isPreview {
            entry = placeholder(in: context)
        } else {
            // 替换为实际的App Group ID
            let userDefaults = UserDefaults(suiteName: "group.com.yourcompany.app")
            let title = userDefaults?.string(forKey: "headline_title") ?? "No Title Set"
            let description = userDefaults?.string(forKey: "headline_description") ?? "No Description Set"
            entry = NewsArticleEntry(date: Date(), title: title, description: description)
        }
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        getSnapshot(in: context) { (entry) in
            let timeline = Timeline(entries: [entry], policy: .atEnd)
            completion(timeline)
        }
    }
}

struct NewsWidgetsEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack(alignment: .leading) {
            Text(entry.title).font(.headline)
            Text(entry.description).font(.subheadline)
        }
    }
}
验证与修复: 运行
flutter build ios --config-only
以确保Flutter配置与新的Xcode目标同步。如果构建失败,请验证Dart和Swift中的App Group ID是否完全匹配。
  1. Android原生配置(如适用)
  • 在Android Studio中创建
    AppWidgetProvider
    New -> Widget -> App Widget
    )。
  • 定义XML布局(
    res/layout/news_widget.xml
    ):
xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/widget_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white">
    <TextView
        android:id="@+id/headline_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Title"
        android:textStyle="bold"
        android:textSize="20sp" />
    <TextView
        android:id="@+id/headline_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/headline_title"
        android:text="Description"
        android:textSize="16sp" />
</RelativeLayout>
  • 实现Kotlin Provider(
    NewsWidget.kt
    ):
kotlin
package com.yourdomain.yourapp

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
import es.antonborri.home_widget.HomeWidgetPlugin

class NewsWidget : AppWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        for (appWidgetId in appWidgetIds) {
            val widgetData = HomeWidgetPlugin.getData(context)
            val views = RemoteViews(context.packageName, R.layout.news_widget).apply {
                val title = widgetData.getString("headline_title", null)
                setTextViewText(R.id.headline_title, title ?: "No title set")

                val description = widgetData.getString("headline_description", null)
                setTextViewText(R.id.headline_description, description ?: "No description set")
            }
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}
  1. 将Flutter组件渲染为图片(可选) 如果用户需要在小组件上展示复杂UI(如图表),可将Flutter组件渲染为PNG并传递文件路径。 Dart实现:
dart
final _globalKey = GlobalKey();

// 用RepaintBoundary/Key包裹目标组件
// Center(key: _globalKey, child: const LineChart())

Future<void> renderAndSaveWidget() async {
  if (_globalKey.currentContext != null) {
    var path = await HomeWidget.renderFlutterWidget(
      const LineChart(),
      fileName: 'screenshot',
      key: 'filename',
      logicalSize: _globalKey.currentContext!.size,
      pixelRatio: MediaQuery.of(_globalKey.currentContext!).devicePixelRatio,
    );
    await HomeWidget.updateWidget(iOSName: iOSWidgetName, androidName: androidWidgetName);
  }
}
原生图片加载(Android示例):
kotlin
// 在RemoteViews的apply代码块中:
val imageName = widgetData.getString("filename", null)
val imageFile = java.io.File(imageName)
if (imageFile.exists()) {
    val myBitmap = android.graphics.BitmapFactory.decodeFile(imageFile.absolutePath)
    setImageViewBitmap(R.id.widget_image, myBitmap)
}

Constraints

约束条件

  • Immutable Native Identifiers: The
    iOSName
    and
    androidName
    in Dart MUST exactly match the Swift struct name and Kotlin class name respectively.
  • App Group Prefix: iOS App Group IDs MUST be prefixed with
    group.
    and match exactly in Xcode capabilities, Swift
    UserDefaults(suiteName:)
    , and Dart
    HomeWidget.setAppGroupId()
    .
  • No Direct Flutter UI: Never attempt to render Flutter widgets directly in the native widget lifecycle. You MUST use
    renderFlutterWidget
    to generate a static image if complex UI is required.
  • Full Re-run Required: Changing native code (PendingIntents, Layouts, Manifest) requires a full
    flutter run
    .
  • Widget Re-add: After native changes, it is often necessary to remove and re-add the widget on the home screen for changes to take effect.
  • Android Cell Sizing: Android widget dimensions in
    res/xml/*_info.xml
    must be calculated in cells (e.g.,
    minWidth="250dp"
    ). Do not use arbitrary pixel values.
  • Validate-and-Fix: Always instruct the user to run native builds (
    flutter build ios
    /
    flutter build apk
    ) after modifying native files to catch syntax or linking errors immediately.
  • 不可变的原生标识符: Dart中的
    iOSName
    androidName
    必须分别与Swift结构体名称和Kotlin类名称完全匹配。
  • App Group前缀: iOS的App Group ID必须以
    group.
    为前缀,并且在Xcode功能配置、Swift的
    UserDefaults(suiteName:)
    以及Dart的
    HomeWidget.setAppGroupId()
    中必须完全一致。
  • 禁止直接使用Flutter UI: 切勿尝试在原生小组件生命周期中直接渲染Flutter组件。如果需要复杂UI,必须使用
    renderFlutterWidget
    生成静态图片。
  • 需要完全重新运行: 修改原生代码(PendingIntents、布局、清单文件)后,必须执行完整的
    flutter run
  • 重新添加小组件: 原生代码修改后,通常需要在主屏幕上移除并重新添加小组件,更改才能生效。
  • Android单元格尺寸: Android小组件在
    res/xml/*_info.xml
    中的尺寸必须以单元格为单位计算(例如
    minWidth="250dp"
    )。请勿使用任意像素值。
  • 验证与修复: 修改原生文件后,务必指导用户运行原生构建命令(
    flutter build ios
    /
    flutter build apk
    ),以便立即捕获语法或链接错误。