スキル一覧に戻る
hunmer

create-common-widget-from-renderer

by hunmer

Memento is a cross-platform application developed with Flutter, serving as a personal assistant app that integrates features such as chatting, journaling, and activity tracking.

61🍴 10📅 2026年1月17日
GitHubで見るManusで実行

SKILL.md


name: create-common-widget-from-renderer description: 将插件中内置的dataRenderer(自定义UI)封装成标准的公共小组件,使其可被其他插件或功能复用。核心特性:(1)提取dataRenderer的UI逻辑创建独立组件文件,(2)在common_widgets.dart中注册新组件,(3)在_provideCommonWidgets中添加数据提供函数,(4)移除旧的内置渲染器代码

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/widgets_gallery/common_widgets/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/widgets_gallery/common_widgets/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

使用 StatefulBuilderEventListenerContainer 包裹渲染逻辑:

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 渲染最新数据

常见插件事件列表

插件事件名触发时机
checkincheckin_completed打卡完成时
checkincheckin_deleted删除打卡项目时
todotodo_added添加任务时
todotodo_updated更新任务时
todotodo_deleted删除任务时
diarydiary_entry_added添加日记时
diarydiary_entry_updated更新日记时
diarydiary_entry_deleted删除日记时
habithabit_completed完成习惯时
trackertracker_record_added添加追踪记录时

关键要点

  1. 绕过静态 props 缓存:不使用 GenericSelectorWidget 的静态 commonWidgetProps,而是每次渲染时动态调用 commonWidgetsProvider

  2. 事件驱动更新:通过监听插件广播的事件,在数据变化时自动触发 UI 刷新

  3. 保持数据源一致性:从 selectorConfig.selectedData 提取原始数据(如 id),然后通过 commonWidgetsProvider 获取完整的最新数据

  4. 错误处理:提供友好的错误状态显示,避免组件崩溃

与原有方案的区别

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

Notes

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

スコア

総合スコア

70/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

+5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

レビュー

💬

レビュー機能は近日公開予定です