create-common-widget-from-renderer

star 67

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

hunmer By hunmer schedule Updated 1/14/2026

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

常见插件事件列表

| 插件 | 事件名 | 触发时机 |

|-----|-------|---------|

| checkin | checkin_completed | 打卡完成时 |

| checkin | checkin_deleted | 删除打卡项目时 |

| todo | todo_added | 添加任务时 |

| todo | todo_updated | 更新任务时 |

| todo | todo_deleted | 删除任务时 |

| diary | diary_entry_added | 添加日记时 |

| diary | diary_entry_updated | 更新日记时 |

| diary | diary_entry_deleted | 删除日记时 |

| habit | habit_completed | 完成习惯时 |

| tracker | tracker_record_added | 添加追踪记录时 |

关键要点

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

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

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

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

与原有方案的区别

| 方面 | 原有方案(静态 props) | 新方案(动态更新) |

|-----|---------------------|------------------|

| 数据来源 | selectorConfig.commonWidgetProps(静态) | commonWidgetsProvider(data)(动态) |

| 更新机制 | 无自动更新 | 监听事件自动刷新 |

| 使用的 Widget | GenericSelectorWidget | 自定义 _buildDynamicSelectorWidget |

| 数据新鲜度 | 配置时的快照 | 每次渲染都是最新数据 |

Notes

  • 公共组件应该是纯展示组件,不处理业务逻辑

  • 所有数据通过 props 传递,不在组件内访问插件

  • 实时数据在 _provideCommonWidgets 中获取

  • 保留 navigationHandler 用于点击导航

  • 保留 dataSelector 用于数据转换

  • 公共组件可被其他插件复用

  • 需要动态更新时使用 EventListenerContainer 监听插件事件

Install via CLI
npx skills add https://github.com/hunmer/Memento --skill create-common-widget-from-renderer
Repository Details
star Stars 67
call_split Forks 11
navigation Branch main
article Path SKILL.md
More from Creator