📚 短故事聚合页
短故事聚合页提供聚合浏览、阅读、收藏和商业化能力。插件同时支持 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(),
),
],
),
);
}
}
生命周期与限制
- 自动管理:
StoryHomeNativeView
在dispose
阶段会自动调用原生层销毁逻辑,无需手动管理。 - 平台视图限制: PlatformView 由原生渲染,避免在其上方叠加
Positioned
/Stack
等复杂布局,必要时可在外层使用安全区域、背景容器。 - 事件监听: 通过
listener
参数注册事件监听器,在组件销毁时会自动取消注册。
🧭 方式二:使用 API 控制聚合页
API 方式用于历史项目兼容或需要原生容器承载的场景。如无特殊需求,仍建议优先选择 NativeView 方案。
调用流程
createStoryHome(config: ...)
创建聚合页,返回包含widgetId
的结果;showStoryHome(widgetId)
打开原生聚合页页面(Android 启动 Activity,iOS 推入控制器);- 页面结束时调用
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> 包含 widgetId 和 success |
showStoryHome(widgetId) | 显示聚合页(全屏原生页面) | Map<String, dynamic> |
destroyStoryHome(widgetId) | 销毁聚合页 | bool |
🧩 公共配置项(StoryHomeConfig
)
以下参数两种接入方式均可使用,未显式赋值时会使用 SDK 默认值。
基础配置
参数 | 类型 | 平台支持 | 说明 | 默认值 |
---|---|---|---|---|
canPullRefresh | bool? | 🤖 Android | 是否支持下拉刷新 | false |
topOffset | int? | 🤖 Android | Y轴偏移量,单位dp,适配自定义标题栏 | -1 (无偏移) |
recPageSize | int? | 🤖 Android | 推荐榜分页大小 | 9 |
readerConfig | Map<String, dynamic>? | ✅ 双端 | 阅读器配置,透传到阅读器页面 | null |
extra | Map<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 方式:
回调方法 | 参数 | 说明 |
---|---|---|
onStoryHomeReady | String homeId | 聚合页创建完成并准备就绪 |
onStoryHomeError | String error | 聚合页发生错误 |
onItemClick | StoryInfo 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
渲染黑屏或空白
可能原因:
- SDK 未正确初始化(
PangrowthContent.initialize
) - SDK 未启动(
PangrowthContent.start
且startStory: true
) - 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,
);
}
}
🔗 相关文档
- 短故事信息接口 - 信息查询 API
- 短故事阅读器 - 阅读器组件
- 事件处理 - 完整的事件体系说明
- Widget组件总览 - 所有组件导航
💡 提示: 短故事聚合页需要有效的短故事内容源。建议优先使用 NativeView 方式集成,这样可以更好地融入 Flutter 的页面管理体系,并自动处理生命周期。