📖 短故事模块
版本 PanGrowth

📚 短故事聚合页

短故事聚合页提供聚合浏览、阅读、收藏和商业化能力。插件同时支持 NativeView 组件API 控制 两种接入方式,可根据业务形态灵活选择。

🚪 接入方式概览

方式推荐场景实现特点
NativeView 组件(推荐)Flutter 页面内嵌短故事内容,追求最快集成StoryHomeNativeView 自动管理创建/销毁,Flutter 布局友好
API 调用需要在原生 Activity / ViewController 中展示,或兼容历史流程createStoryHome / showStoryHome / destroyStoryHome 主动控制生命周期

两种方式共用 StoryHomeConfig 配置对象,事件回调通过 StoryHomeListener 体系管理。

✅ 方式一:使用 StoryHomeNativeView(推荐)

StoryHomeNativeView 通过 PlatformView 内嵌原生聚合页,Flutter 端仅需像普通 Widget 一样使用。

基础用法

import 'package:flutter/material.dart';
import 'package:pangrowth_content/pangrowth_content.dart';

class StoryHomePage extends StatelessWidget {
  const StoryHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('短故事')),
      body: StoryHomeNativeView(
        canPullRefresh: true,
        recPageSize: 9,
        listener: StoryHomeListener(
          onStoryHomeReady: (viewId) {
            debugPrint('短故事聚合页就绪: $viewId');
          },
          onItemClick: (story) {
            // 点击故事卡片,跳转到阅读器页面
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (_) => StoryReaderPage(
                  storyId: story.storyId,
                  storyName: story.title,
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

监听聚合页生命周期

可通过 StoryHomeListener 获取组件创建、销毁及错误信息,便于埋点或自定义 UI。

class StoryHomeWithListenerPage extends StatefulWidget {
  const StoryHomeWithListenerPage({super.key});

  @override
  State<StoryHomeWithListenerPage> createState() => _StoryHomeWithListenerPageState();
}

class _StoryHomeWithListenerPageState extends State<StoryHomeWithListenerPage> {
  String? _viewId;
  bool _isLoading = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('短故事聚合页')),
      body: Stack(
        children: [
          StoryHomeNativeView(
            canPullRefresh: true,
            recPageSize: 9,
            listener: StoryHomeListener(
              onStoryHomeReady: (viewId) {
                debugPrint('聚合页就绪: $viewId');
                setState(() {
                  _viewId = viewId;
                  _isLoading = false;
                });
              },
              onStoryHomeDisposed: () {
                debugPrint('聚合页已释放');
                setState(() {
                  _viewId = null;
                });
              },
              onItemClick: (story) {
                debugPrint('用户点击故事: ${story.storyId}, 标题: ${story.title}');
                // 导航到阅读器页面
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (_) => StoryReaderPage(
                      storyId: story.storyId,
                      storyName: story.title,
                    ),
                  ),
                );
              },
              onStoryHomeError: (error) {
                debugPrint('聚合页加载失败: $error');
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('聚合页加载失败,请稍后重试'),
                    action: SnackBarAction(
                      label: '确定',
                      onPressed: () {},
                    ),
                  ),
                );
              },
            ),
          ),
          if (_isLoading)
            const Center(
              child: CircularProgressIndicator(),
            ),
        ],
      ),
    );
  }
}

生命周期与限制

  • 自动管理: StoryHomeNativeViewdispose 阶段会自动调用原生层销毁逻辑,无需手动管理。
  • 平台视图限制: PlatformView 由原生渲染,避免在其上方叠加 Positioned/Stack 等复杂布局,必要时可在外层使用安全区域、背景容器。
  • 事件监听: 通过 listener 参数注册事件监听器,在组件销毁时会自动取消注册。

🧭 方式二:使用 API 控制聚合页

API 方式用于历史项目兼容或需要原生容器承载的场景。如无特殊需求,仍建议优先选择 NativeView 方案。

调用流程

  1. createStoryHome(config: ...) 创建聚合页,返回包含 widgetId 的结果;
  2. showStoryHome(widgetId) 打开原生聚合页页面(Android 启动 Activity,iOS 推入控制器);
  3. 页面结束时调用 destroyStoryHome(widgetId) 释放资源。

参考实现

class StoryHomeLauncher {
  String? _widgetId;

  Future<void> openStoryHome() async {
    final result = await PangrowthContent.createStoryHome(
      config: StoryHomeConfig(
        canPullRefresh: true,
        recPageSize: 9,
      ),
    );

    if (result['success'] != true) {
      debugPrint('创建聚合页失败: $result');
      return;
    }

    _widgetId = result['widgetId'] as String?;
    if (_widgetId == null) {
      debugPrint('未返回 widgetId');
      return;
    }

    await PangrowthContent.showStoryHome(_widgetId!);
  }

  Future<void> dispose() async {
    if (_widgetId != null) {
      await PangrowthContent.destroyStoryHome(_widgetId!);
      _widgetId = null;
    }
  }
}

destroyStoryHome 为幂等调用,即使聚合页已关闭再次调用也不会抛出异常。

条件化显示(高级)

对于需要频繁打开/关闭聚合页的场景,可以缓存 widgetId 并重复使用:

class StoryHomeManager {
  String? _widgetId;

  Future<void> toggleStoryHome(bool show) async {
    if (show) {
      // 创建或显示
      if (_widgetId == null) {
        final result = await PangrowthContent.createStoryHome(
          config: StoryHomeConfig(canPullRefresh: true),
        );
        if (result['success'] == true) {
          _widgetId = result['widgetId'] as String?;
        }
      }

      if (_widgetId != null) {
        await PangrowthContent.showStoryHome(_widgetId!);
      }
    } else {
      // 隐藏(不销毁)
      if (_widgetId != null) {
        // 注意: 当前 API 不支持 hide,只能通过 destroy 来关闭
        await PangrowthContent.destroyStoryHome(_widgetId!);
        _widgetId = null;
      }
    }
  }

  Future<void> dispose() async {
    if (_widgetId != null) {
      await PangrowthContent.destroyStoryHome(_widgetId!);
      _widgetId = null;
    }
  }
}

API 方法列表

方法说明返回值
createStoryHome(config)创建聚合页Map<String, dynamic> 包含 widgetIdsuccess
showStoryHome(widgetId)显示聚合页(全屏原生页面)Map<String, dynamic>
destroyStoryHome(widgetId)销毁聚合页bool

🧩 公共配置项(StoryHomeConfig)

以下参数两种接入方式均可使用,未显式赋值时会使用 SDK 默认值。

基础配置

参数类型平台支持说明默认值
canPullRefreshbool?🤖 Android是否支持下拉刷新false
topOffsetint?🤖 AndroidY轴偏移量,单位dp,适配自定义标题栏-1(无偏移)
recPageSizeint?🤖 Android推荐榜分页大小9
readerConfigMap<String, dynamic>?✅ 双端阅读器配置,透传到阅读器页面null
extraMap<String, dynamic>?✅ 双端透传给原生侧的额外配置项null

阅读器配置示例

通过 readerConfig 可以预配置阅读器行为,避免在打开阅读器时重复设置:

StoryHomeNativeView(
  canPullRefresh: true,
  recPageSize: 9,
  readerConfig: {
    // 通用配置项(双端支持)
    'defaultTextSize': 16,       // 默认字体大小
    'hideBack': false,           // 是否隐藏返回按钮
    'endPageRecSize': 6,         // 文末推荐数量

    // iOS 专用配置项
    'customRewardAD': true,               // 是否自定义激励广告
    'addCustomRewardPoint': true,         // 是否增加自定义解锁点
    'showCustomBottomBannerAD': false,    // 是否显示banner广告
    'showCustomMiddleAD': true,           // 是否显示章间广告
    'pageIntervalForMiddleAD': 5,         // 章间广告显示间隔(章数)
    'pageTurnType': 1,                    // 翻页模式: 0仿真/1滑页/2平移/3上下滚动
    'fontSizeLevel': 2,                   // 字体大小档位
  },
)

配置说明:

  • defaultTextSize: 阅读器默认字体大小,双端支持
  • hideBack: 是否隐藏返回按钮,双端支持
  • endPageRecSize: 章节结束时的推荐短故事数量
  • iOS专用配置项用于控制广告展示和翻页效果

📡 事件监听

组件级事件(StoryHomeListener)

组件级事件通过 StoryHomeListener 管理,适用于 NativeView 方式:

回调方法参数说明
onStoryHomeReadyString homeId聚合页创建完成并准备就绪
onStoryHomeErrorString error聚合页发生错误
onItemClickStoryInfo story用户点击短故事卡片
onStoryHomeDisposed-聚合页销毁

🆚 API vs NativeView 对比

对比维度NativeView 方式API 方式
使用方式像 Flutter Widget手动调用 create/show/destroy
生命周期Flutter 自动管理需要手动管理
代码量少,简洁多,需要状态管理
布局灵活性可内嵌任意位置全屏展示
页面切换使用 Navigator原生页面栈
推荐场景内嵌式展示(推荐)需要精细控制或原生容器

🌍 平台差异与适配建议

Android 专属参数

  • canPullRefresh: 控制下拉刷新功能,iOS 不支持此参数
  • topOffset: Y轴偏移量,用于适配自定义标题栏
  • recPageSize: 推荐榜分页大小,控制每次加载的短故事数量

iOS 适配建议

  • iOS 默认使用系统导航栏,如需自定义标题栏,可配合 Flutter 自定义 AppBar 使用
  • iOS 不支持下拉刷新参数,但 SDK 内部可能有其他刷新机制

阅读器联动

通过 readerConfig 可以统一配置阅读器的展示行为,避免在每次打开阅读器时重复设置:

// 聚合页配置
StoryHomeNativeView(
  readerConfig: {
    'defaultTextSize': 18,
    'hideBack': false,
    'pageTurnType': 1,  // 滑页模式
  },
)

当用户点击短故事卡片进入阅读器时,会自动应用这些配置。

💡 使用场景示例

场景1: TabBar 中的短故事聚合页

class ContentHomePage extends StatefulWidget {
  const ContentHomePage({super.key});

  @override
  State<ContentHomePage> createState() => _ContentHomePageState();
}

class _ContentHomePageState extends State<ContentHomePage>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('内容中心'),
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: '推荐'),
            Tab(text: '短故事'),
            Tab(text: '视频'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          // 推荐页
          const Center(child: Text('推荐内容')),

          // 短故事聚合页
          StoryHomeNativeView(
            canPullRefresh: true,
            recPageSize: 9,
            listener: StoryHomeListener(
              onStoryHomeReady: (viewId) {
                debugPrint('[短故事Tab] 聚合页就绪: $viewId');
              },
              onItemClick: (story) {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (_) => StoryReaderPage(
                      storyId: story.storyId,
                      storyName: story.title,
                    ),
                  ),
                );
              },
            ),
          ),

          // 视频页
          const Center(child: Text('视频内容')),
        ],
      ),
    );
  }
}

场景2: 带自定义标题栏的聚合页(Android)

class StoryHomeWithCustomHeaderPage extends StatelessWidget {
  const StoryHomeWithCustomHeaderPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // 自定义标题栏(高度56dp)
          Container(
            height: 56 + MediaQuery.of(context).padding.top,
            padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
            color: Colors.white,
            child: Row(
              children: [
                IconButton(
                  icon: const Icon(Icons.arrow_back),
                  onPressed: () => Navigator.pop(context),
                ),
                const Text(
                  '短故事',
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const Spacer(),
                IconButton(
                  icon: const Icon(Icons.search),
                  onPressed: () {
                    // 跳转到搜索页
                  },
                ),
              ],
            ),
          ),

          // 聚合页内容(设置topOffset避免被标题栏遮挡)
          Expanded(
            child: StoryHomeNativeView(
              canPullRefresh: true,
              topOffset: 56,  // 与自定义标题栏高度一致
              recPageSize: 9,
            ),
          ),
        ],
      ),
    );
  }
}

场景3: API 方式动态展示聚合页

class StoryApiDemoPage extends StatefulWidget {
  const StoryApiDemoPage({super.key});

  @override
  State<StoryApiDemoPage> createState() => _StoryApiDemoPageState();
}

class _StoryApiDemoPageState extends State<StoryApiDemoPage> {
  String? _widgetId;
  bool _isCreating = false;

  Future<void> _createAndShowStoryHome() async {
    setState(() => _isCreating = true);

    try {
      // 1. 创建聚合页
      final result = await PangrowthContent.createStoryHome(
        config: StoryHomeConfig(
          canPullRefresh: true,
          recPageSize: 9,
        ),
      );

      if (result['success'] == true) {
        _widgetId = result['widgetId'] as String?;

        // 2. 显示聚合页(全屏原生页面)
        if (_widgetId != null) {
          await PangrowthContent.showStoryHome(_widgetId!);
        }
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('创建聚合页失败')),
        );
      }
    } catch (e) {
      debugPrint('创建聚合页异常: $e');
    } finally {
      setState(() => _isCreating = false);
    }
  }

  @override
  void dispose() {
    if (_widgetId != null) {
      PangrowthContent.destroyStoryHome(_widgetId!);
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('API 方式演示')),
      body: Center(
        child: ElevatedButton(
          onPressed: _isCreating ? null : _createAndShowStoryHome,
          child: _isCreating
              ? const SizedBox(
                  width: 16,
                  height: 16,
                  child: CircularProgressIndicator(strokeWidth: 2),
                )
              : const Text('打开短故事聚合页'),
        ),
      ),
    );
  }
}

场景4: 状态管理 + 聚合页

// 使用 Provider 管理聚合页状态
class StoryHomeProvider extends ChangeNotifier {
  String? _viewId;
  bool _isReady = false;
  String? _error;

  String? get viewId => _viewId;
  bool get isReady => _isReady;
  String? get error => _error;

  void onReady(String viewId) {
    _viewId = viewId;
    _isReady = true;
    _error = null;
    notifyListeners();
  }

  void onError(String error) {
    _error = error;
    _isReady = false;
    notifyListeners();
  }

  void onDisposed() {
    _viewId = null;
    _isReady = false;
    _error = null;
    notifyListeners();
  }
}

// 页面中使用
class StoryHomeWithProviderPage extends StatelessWidget {
  const StoryHomeWithProviderPage({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => StoryHomeProvider(),
      child: Scaffold(
        appBar: AppBar(
          title: const Text('短故事'),
          actions: [
            Consumer<StoryHomeProvider>(
              builder: (context, provider, _) {
                if (!provider.isReady) {
                  return const Padding(
                    padding: EdgeInsets.symmetric(horizontal: 16),
                    child: Center(
                      child: SizedBox(
                        width: 16,
                        height: 16,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      ),
                    ),
                  );
                }
                return IconButton(
                  icon: const Icon(Icons.info_outline),
                  onPressed: () {
                    showDialog(
                      context: context,
                      builder: (_) => AlertDialog(
                        title: const Text('聚合页状态'),
                        content: Text('ViewID: ${provider.viewId}'),
                        actions: [
                          TextButton(
                            onPressed: () => Navigator.pop(context),
                            child: const Text('确定'),
                          ),
                        ],
                      ),
                    );
                  },
                );
              },
            ),
          ],
        ),
        body: Consumer<StoryHomeProvider>(
          builder: (context, provider, _) {
            return Stack(
              children: [
                StoryHomeNativeView(
                  canPullRefresh: true,
                  recPageSize: 9,
                  listener: StoryHomeListener(
                    onStoryHomeReady: provider.onReady,
                    onStoryHomeError: provider.onError,
                    onStoryHomeDisposed: provider.onDisposed,
                    onItemClick: (story) {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (_) => StoryReaderPage(
                            storyId: story.storyId,
                            storyName: story.title,
                          ),
                        ),
                      );
                    },
                  ),
                ),
                if (provider.error != null)
                  Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Icon(
                          Icons.error_outline,
                          color: Colors.red,
                          size: 48,
                        ),
                        const SizedBox(height: 16),
                        Text(
                          provider.error!,
                          style: const TextStyle(color: Colors.red),
                        ),
                      ],
                    ),
                  ),
              ],
            );
          },
        ),
      ),
    );
  }
}

⚠️ 常见问题排查

问题1: 聚合页无法显示

症状: StoryHomeNativeView 渲染黑屏或空白

可能原因:

  1. SDK 未正确初始化(PangrowthContent.initialize)
  2. SDK 未启动(PangrowthContent.startstartStory: true)
  3. GroMore 广告SDK 未初始化

解决方法:

// 确保初始化顺序正确
// 1. 初始化 GroMore SDK
await GroMore.initialize(...);

// 2. 初始化 PangrowthContent
await PangrowthContent.initialize(
  configPath: 'assets/config.json',
);

// 3. 启动短故事模块
await PangrowthContent.start(startStory: true);

// 4. 使用 StoryHomeNativeView

问题2: API 方式创建失败

症状: createStoryHome 返回 success: false

排查步骤:

final result = await PangrowthContent.createStoryHome(
  config: StoryHomeConfig(canPullRefresh: true),
);

print('创建结果: $result');
// 检查返回的错误信息
if (result['success'] != true) {
  print('错误代码: ${result['code']}');
  print('错误信息: ${result['message']}');
}

问题3: 点击故事无反应

症状: 点击短故事卡片没有跳转到阅读器

可能原因:

  • onItemClick 回调未正确处理
  • Navigator 路由配置错误

解决方法:

StoryHomeNativeView(
  listener: StoryHomeListener(
    onItemClick: (story) {
      // 确保打印调试信息
      debugPrint('点击故事: ${story.storyId}, 标题: ${story.title}');

      // 使用正确的路由跳转
      Navigator.of(context).push(
        MaterialPageRoute(
          builder: (context) => StoryReaderPage(
            storyId: story.storyId,
            storyName: story.title,
          ),
        ),
      );
    },
  ),
)

问题4: 自定义标题栏被内容遮挡(Android)

症状: 自定义标题栏与聚合页内容重叠

解决方法:

StoryHomeNativeView(
  topOffset: 56,  // 设置Y轴偏移量(单位dp),值应等于标题栏高度
  canPullRefresh: true,
)

问题5: TabBar 切换后聚合页状态丢失

症状: 在 TabBarView 中切换 Tab 后,聚合页重新加载

解决方法: 使用 AutomaticKeepAliveClientMixin 保持状态

class StoryHomeTab extends StatefulWidget {
  const StoryHomeTab({super.key});

  @override
  State<StoryHomeTab> createState() => _StoryHomeTabState();
}

class _StoryHomeTabState extends State<StoryHomeTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);  // 必须调用
    return StoryHomeNativeView(
      canPullRefresh: true,
      recPageSize: 9,
    );
  }
}

🔗 相关文档


💡 提示: 短故事聚合页需要有效的短故事内容源。建议优先使用 NativeView 方式集成,这样可以更好地融入 Flutter 的页面管理体系,并自动处理生命周期。

需要进一步协助?

与 LightCore 技术顾问沟通,获取商业化策略与集成支持。