smithery/hunmer

create-common-widget-from-renderer

将插件中?

Installation

$ npx skills add smithery/hunmer --skill create-common-widget-from-renderer

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/hunmer.

npx skills add smithery/hunmer

Browse all from smithery/hunmer

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 28,052 B
  • docs SUMMARY.md 383 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Create Common Widget from dataRenderer

将插件中内置的 dataRenderer(自定义 UI)封装成标准的公共小组件,使其可被其他插件复用。

Usage

# 将插件的内置渲染器封装为公共组件
/create-common-widget-from-renderer <plugin-path> --widget-id <widget-id> --component-id <component-id>

# 完整参数
/create-common-widget-from-renderer lib/plugins/checkin \
  --widget-id checkin_item_selector \
  --component-id checkinItemCard

Examples:

# 将签到项目的内置卡片封装为公共组件
/create-common-widget-from-renderer lib/plugins/checkin \
  --widget-id checkin_item_selector \
  --component-id checkinItemCard

# 将日记条目的内置卡片封装为公共组件
/create-common-widget-from-renderer lib/plugins/diary \
  --widget-id diary_entry_selector \
  --component-id diaryEntryCard

Arguments

  • <plugin-path>: 插件根目录路径(包含 home_widgets.dart)
  • --widget-id <id>: 源小组件 ID(包含内置 dataRenderer 的选择器小组件)
  • --component-id <id>: 新公共组件的 ID(驼峰命名,如 checkinItemCard)

Workflow

1. Analyze Existing dataRenderer

读取并分析现有的 dataRenderer 实现:

// 示例:lib/plugins/checkin/home_widgets.dart

static Widget _renderCheckinItemData(
  BuildContext context,
  SelectorResult result,
  Map<String, dynamic> config,
) {
  // 从 result.data 获取数据
  final itemData = result.data as Map<String, dynamic>;
  final itemId = itemData['id'] as String?;

  // 获取最新数据
  final plugin = PluginManager.instance.getPlugin('checkin') as CheckinPlugin?;
  final checkinItem = plugin?.checkinItems.firstWhere(...);

  // 构建 UI
  return Container(
    child: Column(
      children: [
        // 图标和标题
        // 打卡状态
        // 热力图
      ],
    ),
  );
}

识别关键元素:

  • 输入数据结构(从 result.data 获取)
  • UI 组件结构(布局、样式)
  • 动态数据获取(如通过 PluginManager)
  • 尺寸适配逻辑(medium/large 的差异)

2. Create Standalone Widget Component

在 lib/screens/widgetsgallery/commonwidgets/widgets/ 创建新组件文件:

// lib/screens/widgets_gallery/common_widgets/widgets/checkin_item_card.dart

import 'package:flutter/material.dart';
import 'package:Memento/screens/home_screen/models/home_widget_size.dart';

/// 签到项目卡片小组件
///
/// 显示签到项目的名称、图标、今日打卡状态和热力图
class CheckinItemCardWidget extends StatelessWidget {
  final Map<String, dynamic> props;
  final HomeWidgetSize size;

  const CheckinItemCardWidget({
    super.key,
    required this.props,
    required this.size,
  });

  /// 从 props 创建实例(用于公共小组件系统)
  factory CheckinItemCardWidget.fromProps(
    Map<String, dynamic> props,
    HomeWidgetSize size,
  ) {
    return CheckinItemCardWidget(
      props: props,
      size: size,
    );
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    // 从 props 获取数据(避免直接访问插件)
    final name = props['title'] as String? ?? '签到项目';
    final group = props['subtitle'] as String?;
    final colorValue = props['color'] as int? ?? 0xFF007AFF;
    final iconCode = props['iconCodePoint'] as int? ?? Icons.checklist.codePoint;
    final isCheckedToday = props['isCheckedToday'] as bool? ?? false;

    final itemColor = Color(colorValue);

    return Container(
      padding: const EdgeInsets.all(12),
      child: Column(
        children: [
          // UI 构建逻辑(从原 dataRenderer 复制并调整)
          _buildHeader(theme, itemColor, name, group, iconCode, isCheckedToday),
          if (size == HomeWidgetSize.medium || size == HomeWidgetSize.large)
            _buildHeatmap(itemColor),
        ],
      ),
    );
  }

  Widget _buildHeader(...) { /* ... */ }
  Widget _buildHeatmap(...) { /* ... */ }
}

关键点:

  • 使用 props 参数而不是直接访问 PluginManager
  • 实现 fromProps 工厂方法
  • 根据 size 参数调整显示内容
  • 纯展示组件,不处理事件

3. Register in common_widgets.dart

在公共组件注册表中添加新组件:

// lib/screens/widgets_gallery/common_widgets/common_widgets.dart

// 1. 添加 import
import 'widgets/checkin_item_card.dart';

// 2. 添加枚举值
enum CommonWidgetId {
  // ... 现有值 ...
  checkinItemCard,
}

// 3. 添加元数据
const Map<CommonWidgetId, CommonWidgetMetadata> metadata = {
  // ... 现有元数据 ...

  CommonWidgetId.checkinItemCard: CommonWidgetMetadata(
    id: CommonWidgetId.checkinItemCard,
    name: '签到项目卡片',
    description: '显示签到项目的图标、名称、今日打卡状态和热力图',
    icon: Icons.checklist,
    defaultSize: HomeWidgetSize.medium,
    supportedSizes: [HomeWidgetSize.medium, HomeWidgetSize.large],
  ),
};

// 4. 添加构建器分支
class CommonWidgetBuilder {
  static Widget build(...) {
    switch (widgetId) {
      // ... 现有 case ...

      case CommonWidgetId.checkinItemCard:
        return CheckinItemCardWidget.fromProps(props, size);
    }
  }
}

4. Add Data Provider in home_widgets.dart

在插件的 _provideCommonWidgets 函数中添加新组件的数据提供:

// lib/plugins/checkin/home_widgets.dart

static Map<String, Map<String, dynamic>> _provideCommonWidgets(
  Map<String, dynamic> data,
) {
  // data 包含:id, name, group, icon, color(由 dataSelector 提供)

  final name = (data['name'] as String?) ?? '签到项目';
  final group = (data['group'] as String?) ?? '';
  final colorValue = (data['color'] as int?) ?? 0xFF007AFF;
  final iconCode = (data['icon'] as int?) ?? Icons.checklist.codePoint;

  // 获取实时数据(如需要)
  final plugin = PluginManager.instance.getPlugin('checkin') as CheckinPlugin?;
  CheckinItem? item;
  bool isCheckedToday = false;

  if (plugin != null) {
    final itemId = data['id'] as String?;
    if (itemId != null) {
      item = plugin.checkinItems.firstWhere((i) => i.id == itemId, orElse: () => null);
      isCheckedToday = item?.isCheckedToday() ?? false;
    }
  }

  return {
    // 新封装的签到项目卡片
    'checkinItemCard': {
      'id': data['id'],
      'title': name,
      'subtitle': group.isNotEmpty ? group : '签到',
      'iconCodePoint': iconCode,
      'color': colorValue,
      'isCheckedToday': isCheckedToday,
      // 周数据(用于 medium 尺寸)
      'weekData': _generateWeekData(item),
      // 月度数据(用于 large 尺寸)
      'daysData': _generateMonthData(item),
    },

    // ... 其他组件 ...
  };
}

// 辅助方法:生成周数据
static List<Map<String, dynamic>> _generateWeekData(CheckinItem? item) {
  // ... 生成 7 天签到状态 ...
}

// 辅助方法:生成月度数据
static List<Map<String, dynamic>> _generateMonthData(CheckinItem? item) {
  // ... 生成当月签到状态 ...
}

5. Remove Old dataRenderer

移除不再需要的内置渲染器:

// lib/plugins/checkin/home_widgets.dart

// 移除 dataRenderer 引用
registry.register(
  HomeWidget(
    id: 'checkin_item_selector',
    // dataRenderer: _renderCheckinItemData,  // ❌ 移除
    navigationHandler: _navigateToCheckinItem,
    dataSelector: _extractCheckinItemData,
    commonWidgetsProvider: _provideCommonWidgets,
    // ...
  ),
);

// 移除旧的渲染方法
// static Widget _renderCheckinItemData(...) { ... }  // ❌ 删除
// static Widget _buildCheckinItemWidget(...) { ... }  // ❌ 删除
// static Widget _buildHeatmapGrid(...) { ... }  // ❌ 删除

保留的内容:

  • navigationHandler:点击导航功能仍需要
  • dataSelector:数据转换逻辑仍需要
  • _provideCommonWidgets:公共组件数据提供

Key Concepts

1. Props vs Data Access

错误方式(组件直接访问插件):

final plugin = PluginManager.instance.getPlugin('checkin');
final item = plugin.checkinItems.firstWhere(...);

正确方式(通过 props 传递数据):

// 在 _provideCommonWidgets 中获取数据并传递
final item = plugin.checkinItems.firstWhere(...);
'checkinItemCard': {
  'id': data['id'],
  'isCheckedToday': item.isCheckedToday(),
  'weekData': _generateWeekData(item),
}

// 在组件中使用 props
final isCheckedToday = props['isCheckedToday'] as bool? ?? false;

2. Size-Based Rendering

根据 HomeWidgetSize 调整显示内容:

@override
Widget build(BuildContext context) {
  // 显示热力图的条件
  final showHeatmap = size == HomeWidgetSize.medium ||
                     size == HomeWidgetSize.large;

  return Column(
    children: [
      _buildHeader(),
      if (showHeatmap) _buildHeatmap(),
    ],
  );
}

Widget _buildHeatmap(Color itemColor) {
  // 根据尺寸选择数据源
  if (size == HomeWidgetSize.medium) {
    return _buildWeekHeatmap(props['weekData'], itemColor);
  } else if (size == HomeWidgetSize.large) {
    return _buildMonthHeatmap(props['daysData'], itemColor);
  }
  return const SizedBox.shrink();
}

3. Factory Method Pattern

公共组件必须实现 fromProps 工厂方法:

class CheckinItemCardWidget extends StatelessWidget {
  factory CheckinItemCardWidget.fromProps(
    Map<String, dynamic> props,
    HomeWidgetSize size,
  ) {
    return CheckinItemCardWidget(
      props: props,
      size: size,
    );
  }
}

这使得 CommonWidgetBuilder 可以统一构建所有组件。

Complete Example: Checkin Item Card Migration

Before (内置渲染器)

// lib/plugins/checkin/home_widgets.dart

registry.register(
  HomeWidget(
    id: 'checkin_item_selector',
    dataRenderer: _renderCheckinItemData,  // 内置渲染器
    navigationHandler: _navigateToCheckinItem,
    dataSelector: _extractCheckinItemData,
    // ...
  ),
);

static Widget _renderCheckinItemData(
  BuildContext context,
  SelectorResult result,
  Map<String, dynamic> config,
) {
  // 从 PluginManager 获取最新数据
  final plugin = PluginManager.instance.getPlugin('checkin') as CheckinPlugin?;
  final itemId = result.data['id'] as String;
  final item = plugin?.checkinItems.firstWhere((i) => i.id == itemId);

  // 构建 UI
  return Container(
    child: Column([
      _buildHeader(item),
      _buildHeatmap(item),
    ]),
  );
}

After (公共组件)

1. 新建组件文件:

// lib/screens/widgets_gallery/common_widgets/widgets/checkin_item_card.dart

class CheckinItemCardWidget extends StatelessWidget {
  final Map<String, dynamic> props;
  final HomeWidgetSize size;

  factory CheckinItemCardWidget.fromProps(props, size) => /* ... */;

  @override
  Widget build(BuildContext context) {
    // 从 props 读取数据
    final name = props['title'] as String? ?? '签到项目';
    final isCheckedToday = props['isCheckedToday'] as bool? ?? false;

    return Container(
      child: Column([
        _buildHeader(name, isCheckedToday),
        if (size == HomeWidgetSize.medium || size == HomeWidgetSize.large)
          _buildHeatmap(props['weekData'] ?? props['daysData']),
      ]),
    );
  }
}

2. 注册公共组件:

// lib/screens/widgets_gallery/common_widgets/common_widgets.dart

import 'widgets/checkin_item_card.dart';

enum CommonWidgetId {
  checkinItemCard,
  // ...
}

CommonWidgetId.checkinItemCard: CommonWidgetMetadata(
  id: CommonWidgetId.checkinItemCard,
  name: '签到项目卡片',
  description: '显示签到项目的图标、名称、今日打卡状态和热力图',
  icon: Icons.checklist,
  defaultSize: HomeWidgetSize.medium,
  supportedSizes: [HomeWidgetSize.medium, HomeWidgetSize.large],
),

case CommonWidgetId.checkinItemCard:
  return CheckinItemCardWidget.fromProps(props, size);

3. 添加数据提供:

// lib/plugins/checkin/home_widgets.dart

static Map<String, Map<String, dynamic>> _provideCommonWidgets(
  Map<String, dynamic> data,
) {
  // 获取实时数据
  final plugin = PluginManager.instance.getPlugin('checkin') as CheckinPlugin?;
  final itemId = data['id'] as String?;
  final item = plugin?.checkinItems.firstWhere((i) => i.id == itemId);

  return {
    'checkinItemCard': {
      'id': data['id'],
      'title': data['name'],
      'isCheckedToday': item?.isCheckedToday() ?? false,
      'weekData': _generateWeekData(item),
      'daysData': _generateMonthData(item),
    },
  };
}

4. 移除旧代码:

// lib/plugins/checkin/home_widgets.dart

registry.register(
  HomeWidget(
    id: 'checkin_item_selector',
    // dataRenderer: _renderCheckinItemData,  // ❌ 移除
    navigationHandler: _navigateToCheckinItem,  // ✅ 保留
    dataSelector: _extractCheckinItemData,       // ✅ 保留
    commonWidgetsProvider: _provideCommonWidgets, // ✅ 保留
    // ...
  ),
);

// 删除旧方法
// static Widget _renderCheckinItemData(...) { }  // ❌ 删除
// static Widget _buildCheckinItemWidget(...) { }  // ❌ 删除
// static Widget _buildHeatmapGrid(...) { }        // ❌ 删除

Best Practices

1. Props 字段命名

使用语义化、自描述的字段名:

// ✅ 好的命名
'checkinItemCard': {
  'title': '早起打卡',
  'subtitle': '健康习惯',
  'iconCodePoint': 0xe157,
  'color': 0xFF4CAF50,
  'isCheckedToday': true,
}

// ❌ 避免的命名
'checkinItemCard': {
  't': '早起打卡',
  'sub': '健康习惯',
  'icon': 0xe157,
  'c': 0xFF4CAF50,
  'done': true,
}

2. 数据类型安全

始终使用类型安全的转换和默认值:

// ✅ 安全的类型转换
final name = props['title'] as String? ?? '默认名称';
final count = props['count'] as int? ?? 0;
final isChecked = props['isChecked'] as bool? ?? false;

// ❌ 不安全的直接转换
final name = props['title'] as String;  // 可能抛出异常
final count = props['count'] as int;    // 可能抛出异常

3. 尺寸适配

为不同尺寸提供不同数据:

return {
  'checkinItemCard': {
    // 通用数据
    'title': name,
    'isCheckedToday': isCheckedToday,

    // medium 尺寸使用
    'weekData': List.generate(7, (i) => {...}),

    // large 尺寸使用
    'daysData': List.generate(daysInMonth, (i) => {...}),
  },
};

4. 实时数据获取

在 _provideCommonWidgets 中获取实时数据,而不是在组件中:

// ✅ 在数据提供者中获取
static Map<String, Map<String, dynamic>> _provideCommonWidgets(
  Map<String, dynamic> data,
) {
  final item = _getItem(data['id']);
  return {
    'checkinItemCard': {
      'isCheckedToday': item?.isCheckedToday() ?? false,
      'consecutiveDays': item?.getConsecutiveDays() ?? 0,
    },
  };
}

// ❌ 避免在组件中访问插件
class CheckinItemCardWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final plugin = PluginManager.instance.getPlugin('checkin');  // 不推荐
  }
}

Testing Checklist

完成后验证:

  • flutter analyze 无错误
  • 公共组件能正确渲染(medium 和 large 尺寸)
  • 组件显示正确的内容(标题、状态、热力图等)
  • 点击组件能正常导航到详情页
  • 其他插件也能使用这个公共组件
  • 数据更新后组件能正确刷新

Troubleshooting

问题 1: 公共组件显示为空白

原因: props 缺少必需字段

解决:

// 检查组件接收到的 props
debugPrint('[CheckinItemCard] props: $props');

// 确保所有必需字段都有默认值
final name = props['title'] as String? ?? '默认标题';
final iconCode = props['iconCodePoint'] as int? ?? Icons.checklist.codePoint;

问题 2: 热力图显示异常

原因: 尺寸判断逻辑错误

解决:

// 确保根据 size 选择正确的数据源
Widget _buildHeatmap(Color itemColor) {
  if (size == HomeWidgetSize.medium && weekData != null) {
    return _buildWeekHeatmap(weekData!, itemColor);
  } else if (size == HomeWidgetSize.large && daysData != null) {
    return _buildMonthHeatmap(daysData!, itemColor);
  }
  return const SizedBox.shrink();
}

问题 3: 点击组件无反应

原因: navigationHandler 或 data 配置问题

解决:

// 确保在 _provideCommonWidgets 中传递了 id
'checkinItemCard': {
  'id': data['id'],  // ⚠️ 必需!用于导航
  // ...
}

// 确保注册时保留了 navigationHandler
registry.register(
  HomeWidget(
    navigationHandler: _navigateToCheckinItem,  // ✅ 必需
    // ...
  ),
);

问题 4: 数据不更新

原因: 公共组件是 StatelessWidget,数据没有更新机制

解决: 公共组件的数据更新由 GenericSelectorWidget 处理。确保:

  1. _provideCommonWidgets 中正确获取实时数据
  2. 数据选择器配置正确保存了原始 SelectorResult

Migration Checklist

准备阶段

  • 确认 dataRenderer 的 UI 逻辑
  • 识别需要传递给组件的数据字段
  • 确定组件支持的尺寸

实现阶段

  • 创建组件文件(lib/screens/widgetsgallery/commonwidgets/widgets/<name>.dart)
  • 在 common_widgets.dart 中注册(import、枚举、元数据、构建器)
  • 在 _provideCommonWidgets 中添加数据提供
  • 移除旧的 dataRenderer 和相关方法
  • 移除不再使用的 import

测试阶段

  • 测试 medium 尺寸显示
  • 测试 large 尺寸显示
  • 测试点击导航
  • 测试数据更新
  • 运行 flutter analyze

Event-Driven Data Updates

问题背景

公共小组件使用静态 props 渲染,当插件数据更新时(如用户完成打卡、删除项目等),小组件不会自动刷新显示最新数据。

解决方案

使用 StatefulBuilder + EventListenerContainer 监听插件事件,在事件触发时动态调用 commonWidgetsProvider 获取最新数据。

完整实现步骤

1. 添加必要的导入

// lib/plugins/checkin/home_widgets.dart

import 'package:Memento/widgets/event_listener_container.dart';
import 'package:Memento/screens/home_screen/widgets/selector_widget_types.dart';
import 'package:Memento/screens/widgets_gallery/common_widgets/common_widgets.dart';

2. 修改小组件 builder

使用 StatefulBuilder 和 EventListenerContainer 包裹渲染逻辑:

registry.register(
  HomeWidget(
    id: 'checkin_item_selector',
    pluginId: 'checkin',
    name: 'checkin_quickAccess'.tr,
    // ...
    commonWidgetsProvider: _provideCommonWidgets,
    builder: (context, config) {
      // 使用 StatefulBuilder 和 EventListenerContainer 实现动态更新
      return StatefulBuilder(
        builder: (context, setState) {
          return EventListenerContainer(
            events: const [
              'checkin_completed',  // 打卡完成
              'checkin_deleted',    // 删除项目
              // 添加更多需要监听的事件
            ],
            onEvent: () => setState(() {}),
            child: _buildDynamicSelectorWidget(
              context,
              config,
              registry.getWidget('checkin_item_selector')!,
            ),
          );
        },
      );
    },
  ),
);

3. 创建动态渲染方法

/// 构建动态选择器小组件(支持事件触发时重新获取数据)
static Widget _buildDynamicSelectorWidget(
  BuildContext context,
  Map<String, dynamic> config,
  HomeWidget widgetDefinition,
) {
  // 解析选择器配置
  SelectorWidgetConfig? selectorConfig;
  try {
    if (config.containsKey('selectorWidgetConfig')) {
      selectorConfig = SelectorWidgetConfig.fromJson(
        config['selectorWidgetConfig'] as Map<String, dynamic>,
      );
    }
  } catch (e) {
    debugPrint('[CheckinHomeWidgets] 解析配置失败: $e');
  }

  // 判断是否已配置
  if (selectorConfig == null || !selectorConfig.isConfigured) {
    return _buildUnconfiguredWidget(context);
  }

  // 检查是否使用了公共小组件
  if (selectorConfig.usesCommonWidget) {
    return _buildDynamicCommonWidget(
      context,
      selectorConfig,
      widgetDefinition,
      config,
    );
  }

  // 默认视图
  final originalResult = selectorConfig.toSelectorResult();
  if (originalResult == null) {
    return _buildErrorWidget(context, '无法解析选择的数据');
  }

  return _buildDefaultConfiguredWidget(
    context,
    originalResult,
    widgetDefinition,
  );
}

/// 构建动态公共小组件(每次渲染都重新获取最新数据)
static Widget _buildDynamicCommonWidget(
  BuildContext context,
  SelectorWidgetConfig selectorConfig,
  HomeWidget widgetDefinition,
  Map<String, dynamic> config,
) {
  try {
    final widgetId = selectorConfig.commonWidgetId!;
    final size = config['widgetSize'] as HomeWidgetSize? ??
        widgetDefinition.defaultSize;

    // 将字符串 ID 转换为枚举值
    final commonWidgetId = CommonWidgetsRegistry.fromString(widgetId);
    if (commonWidgetId == null) {
      return _buildErrorWidget(context, '未知的公共组件: $widgetId');
    }

    // 获取原始数据(从 selectorConfig.selectedData)
    final selectedData = selectorConfig.selectedData;
    if (selectedData == null) {
      return _buildErrorWidget(context, '无法获取选择的数据');
    }

    // 从 selectedData 中提取实际的数据数组
    Map<String, dynamic> data = {};
    if (selectedData.containsKey('data')) {
      final dataArray = selectedData['data'];
      if (dataArray is List && dataArray.isNotEmpty) {
        final rawData = dataArray[0];
        if (rawData is Map<String, dynamic>) {
          data = rawData;
        } else if (rawData != null && rawData is Map) {
          data = Map<String, dynamic>.from(rawData);
        }
      }
    }

    // 动态调用 commonWidgetsProvider 获取最新数据
    if (widgetDefinition.commonWidgetsProvider != null) {
      final availableWidgets = widgetDefinition.commonWidgetsProvider!(data);
      final latestProps = availableWidgets[widgetId];

      if (latestProps != null) {
        return CommonWidgetBuilder.build(
          context,
          commonWidgetId,
          latestProps,
          size,
        );
      }
    }

    return _buildErrorWidget(context, '无法获取最新数据');
  } catch (e) {
    debugPrint('[CheckinHomeWidgets] 构建公共组件失败: $e');
    return _buildErrorWidget(context, '渲染公共组件失败');
  }
}

/// 辅助方法:未配置状态
static Widget _buildUnconfiguredWidget(BuildContext context) {
  final theme = Theme.of(context);
  return SizedBox.expand(
    child: Container(
      padding: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        color: theme.colorScheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            '点击配置',
            style: theme.textTheme.bodyMedium?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
        ],
      ),
    ),
  );
}

/// 辅助方法:错误状态
static Widget _buildErrorWidget(BuildContext context, String message) {
  final theme = Theme.of(context);
  return SizedBox.expand(
    child: Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: theme.colorScheme.errorContainer,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.error_outline, size: 32, color: theme.colorScheme.error),
          const SizedBox(height: 8),
          Text(
            message,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onErrorContainer,
            ),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    ),
  );
}

工作原理

用户操作(如完成打卡)
  ↓
插件广播事件(EventManager.broadcast('checkin_completed'))
  ↓
EventListenerContainer 捕获事件
  ↓
onEvent: () => setState(() {}) 触发重建
  ↓
_buildDynamicSelectorWidget 重新执行
  ↓
_buildDynamicCommonWidget 重新执行
  ↓
commonWidgetsProvider(data) 被调用,从插件获取最新数据
  ↓
CommonWidgetBuilder.build 渲染最新数据

常见插件事件列表

插件 事件名 触发时机
checkin checkin_completed 打卡完成时
checkin checkin_deleted 删除打卡项目时
todo todo_added 添加任务时
todo todo_updated 更新任务时
todo todo_deleted 删除任务时
diary diaryentryadded 添加日记时
diary diaryentryupdated 更新日记时
diary diaryentrydeleted 删除日记时
habit habit_completed 完成习惯时
tracker trackerrecordadded 添加追踪记录时

关键要点

  1. 绕过静态 props 缓存:不使用 GenericSelectorWidget 的静态 commonWidgetProps,而是每次渲染时动态调用 commonWidgetsProvider
  1. 事件驱动更新:通过监听插件广播的事件,在数据变化时自动触发 UI 刷新
  1. 保持数据源一致性:从 selectorConfig.selectedData 提取原始数据(如 id),然后通过 commonWidgetsProvider 获取完整的最新数据
  1. 错误处理:提供友好的错误状态显示,避免组件崩溃

与原有方案的区别

方面 原有方案(静态 props) 新方案(动态更新)
数据来源 selectorConfig.commonWidgetProps(静态) commonWidgetsProvider(data)(动态)
更新机制 无自动更新 监听事件自动刷新
使用的 Widget GenericSelectorWidget 自定义 _buildDynamicSelectorWidget
数据新鲜度 配置时的快照 每次渲染都是最新数据

Notes

  • 公共组件应该是纯展示组件,不处理业务逻辑
  • 所有数据通过 props 传递,不在组件内访问插件
  • 实时数据在 _provideCommonWidgets 中获取
  • 保留 navigationHandler 用于点击导航
  • 保留 dataSelector 用于数据转换
  • 公共组件可被其他插件复用
  • 需要动态更新时使用 EventListenerContainer 监听插件事件