鸿蒙如何把一个已有 Flutter 页面改造成支持系统直达
适合谁看
-
已经有 Flutter 页面,想加鸿蒙系统直达的人
-
正在做 Intents Kit 或冷启动导航的人
-
想避免"能跳到页面但体验很怪"的开发者
问题背景
鸿蒙系统直达和应用内点击进入,最大的不同是:
|
维度 |
应用内点击 |
系统直达 |
|---|---|---|
|
页面上下文 |
已经准备好 |
未必准备好 |
|
参数来源 |
前一个页面传值 |
系统入口传参 |
|
壳路由状态 |
正确 |
未必正确 |
|
页面栈 |
有前序页面 |
可能没有 |
所以"原来能打开的页面",不一定天然适合被系统直接打开。
项目中的真实场景
食界探味当前已经支持系统直达的页面:
|
页面 |
pageId |
额外参数 |
跳转方式 |
|---|---|---|---|
|
搜索页 |
|
无 |
直接 go |
|
AI 助手页 |
|
无 |
直接 go |
|
心愿单页 |
|
无 |
直接 go |
|
探索页 |
|
无 |
直接 go |
|
菜品详情页 |
|
dishId |
go + push |
核心实现
第一步:给页面定义稳定的"系统语义入口"
不要直接让系统入口认识 Flutter route path。更稳的做法是先定义 pageId。
为什么要用 pageId 而不是路由路径:
|
方式 |
示例 |
问题 |
|---|---|---|
|
路由路径 |
|
和 Flutter 实现耦合 |
|
pageId |
|
和实现解耦 |
食界探味的 pageId 定义:
{
"pageId": {
"type": "string",
"enum": [
{ "value": "search", "displayName": "搜索美食" },
{ "value": "wish_box", "displayName": "心愿单" },
{ "value": "ingredients", "displayName": "食材探索" },
{ "value": "explore", "displayName": "探索美食" },
{ "value": "dish_detail", "displayName": "查看菜品详情" }
]
}
}
这一步的价值: 把"鸿蒙系统怎么叫这个页面"和"Flutter 内部怎么实现这个页面"拆开。以后改路由命名,系统入口整条链不用跟着改。
第二步:判断页面是否需要额外参数
并不是所有页面都能用同一种直达模型:
|
页面类型 |
是否需要参数 |
示例 |
|---|---|---|
|
功能页直达 |
不需要 |
search、wish_box、explore |
|
详情页直达 |
需要 dishId |
dish_detail |
|
带上下文的直达 |
需要 query |
ai_assistant(可带 initialQuery) |
食界探味的参数校验:
// InsightIntentExecutorImpl.ets
// 功能页:只需要 pageId
if (!VALID_PAGE_IDS.includes(pageId)) {
resolve(makeResult(-1, `unknown pageId: ${pageId}`));
return;
}
// 详情页:还需要 dishId
if (pageId === 'dish_detail' && (!dishId || dishId.length === 0)) {
resolve(makeResult(-1, 'dishId type error'));
return;
}
第三步:壳路由页面和独立详情页的承接方式不同
这是"已有页面适配系统直达"时最容易忽略的。
食界探味的两种跳转策略:
// intent_navigation_channel.dart
static const _shellRoutes = <String>{
'/explore', '/inspiration', '/collection', '/profile',
};
static void _navigate(_NavigationPayload payload) {
// 特殊处理:详情页
if (payload.pageId == 'dish_detail') {
final dishId = payload.dishId;
if (dishId == null || dishId.isEmpty) return;
_router?.go('/explore'); // 先回到壳路由
scheduleMicrotask(() {
_router?.push('/dish/$dishId'); // 再 push 详情页
});
return;
}
// 通用处理:普通页面
final route = _pageIdToRoute[payload.pageId];
if (route == null) return;
if (_shellRoutes.contains(route)) {
_router?.go(route); // 壳路由直接 go
} else {
_router?.go('/explore'); // 非壳路由先回主页
scheduleMicrotask(() {
_router?.push(route); // 再 push
});
}
}
为什么壳路由和详情页要分开处理:
|
页面类型 |
跳转方式 |
原因 |
|---|---|---|
|
壳路由页(explore) |
|
直接切换 Tab |
|
非壳路由页(search) |
|
先回主页,再 push,返回栈正确 |
|
详情页(dish/:id) |
|
先回主页,再 push,带参数 |
如果用同一种方式跳转:
❌ 所有页面都用 go(route):
详情页直接 go('/dish/xxx')
→ 返回时回到探索页(正确)
→ 但从搜索页进详情页时,返回栈会丢失搜索页
✅ 壳路由 go + 非壳路由 push:
详情页 go('/explore') + push('/dish/xxx')
→ 返回时先回到探索页
→ 再返回时回到上一个页面
→ 返回栈正确
第四步:页面本身要接受"没有前序页面上下文"
系统直达进来的页面,不能默认认为:
-
一定是从前一个页面点进来
-
一定已经有完整内存态
这意味着页面要更多依赖:
-
路由参数
-
首次数据加载
菜品详情页的例子:
// dish_detail_screen.dart
class DishDetailScreen extends ConsumerStatefulWidget {
final String dishId; // 从路由参数获取,不依赖前序页面
const DishDetailScreen({super.key, required this.dishId});
@override
ConsumerState<DishDetailScreen> createState() => _DishDetailScreenState();
}
class _DishDetailScreenState extends ConsumerState<DishDetailScreen> {
@override
Widget build(BuildContext context) {
final dishFuture = ref.watch(_dishProvider(widget.dishId)); // 用 dishId 独立加载
// ...
}
}
关键点: 详情页必须把"靠参数独立完成首次加载"当成硬要求。系统直达进来时,没有前序页面帮你准备数据。
第五步:补一层"Flutter 未 ready 时如何补消费"
系统直达不是总发生在 Flutter 完全初始化之后。所以需要 pending 机制:
// IntentNavigationPlugin.ets
navigateToPage(pageId: string, dishId?: string): void {
if (this.channel) {
// Flutter 已 ready,直接推送
this.channel.invokeMethod('onIntentNavigation', args);
} else {
// Flutter 未 ready,先存到 pendingNavigation
IntentNavigationPlugin.pendingNavigation = { pageId, dishId };
}
}
// intent_navigation_channel.dart
static void init(GoRouter router) {
_router = router;
_channel.setMethodCallHandler((call) async {
// 监听实时推送
});
_consumePending(); // 主动消费 pending
}
static Future<void> _consumePending() async {
try {
final payload = await _channel.invokeMethod<Object?>('consumePendingNavigation');
final navigation = _parseArguments(payload);
if (navigation != null) _navigate(navigation);
} on MissingPluginException {
// 非鸿蒙平台,忽略
}
}
这段代码说明:一个已有 Flutter 页面要真正支持系统直达,不只是路由写对,还要把冷启动时机问题也处理掉。
完整的改造流程图
已有 Flutter 页面(只支持应用内点击)
│
▼
第 1 步:定义 pageId
→ insight_intent.json 添加 enum
│
▼
第 2 步:判断是否需要额外参数
→ dish_detail 需要 dishId
│
▼
第 3 步:在 Flutter 边界层加路由映射
→ intent_navigation_channel.dart 加 _pageIdToRoute
→ 区分壳路由 go 和详情页 go+push
│
▼
第 4 步:页面接受无前序上下文
→ 详情页用 dishId 独立加载数据
→ 不依赖前序页面传值
│
▼
第 5 步:补 pending 机制
→ IntentNavigationPlugin 缓存 pending
→ Flutter 初始化后 _consumePending()
│
▼
第 6 步:在 EntryAbility 注册
→ configureFlutterEngine 添加插件
→ onCreate/onNewWant 处理参数
│
▼
已有 Flutter 页面支持鸿蒙系统直达
关键代码位置
|
文件 |
作用 |
|---|---|
|
|
pageId 定义 |
|
|
参数校验 |
|
|
pending 缓存 |
|
|
路由映射 |
|
|
GoRouter 页面定义 |
常见坑
-
直接把系统入口绑到 Flutter route path — 应该用 pageId 解耦
-
页面需要参数,却没有在入口层校验 — dish_detail 必须校验 dishId
-
壳路由页和详情页用同一套跳转方式 — 详情页需要 go+push
-
页面默认前序状态一定存在 — 系统直达进来时可能没有前序页面
-
只在热启动下测试通过 — 冷启动时 pending navigation 的消费链也要验证
-
路由能跳到页面,但页面仍然依赖前序页面传值 — 系统直达进去后是空态
可复用模板
系统直达改造模板
已有 Flutter 页面
│
├─ 1. 定义 pageId(insight_intent.json)
├─ 2. 判断是否需要额外参数(dish_detail 需要 dishId)
├─ 3. 在 Flutter 边界层加路由映射(_pageIdToRoute)
├─ 4. 区分壳路由 go 和详情页 go+push
├─ 5. 页面接受无前序上下文(用参数独立加载)
├─ 6. 补 pending 机制(冷启动缓存 + 消费)
└─ 7. 在 EntryAbility 注册插件
路由跳转策略模板
static void _navigate(NavigationPayload payload) {
// 壳路由:直接 go
if (_shellRoutes.contains(route)) {
_router?.go(route);
return;
}
// 非壳路由/详情页:先回主页,再 push
_router?.go('/home');
scheduleMicrotask(() {
_router?.push(route);
});
}
页面独立加载模板
class DetailScreen extends ConsumerStatefulWidget {
final String id; // 从路由参数获取
@override
Widget build(BuildContext context) {
final data = ref.watch(dataProvider(id)); // 用 id 独立加载
// 不依赖前序页面传值
}
}
本篇总结
让一个已有 Flutter 页面支持鸿蒙系统直达,关键不是"能不能打开",而是"能不能自然承接":
-
定义 pageId — 用业务语义而非路由路径
-
判断参数需求 — 功能页无参数,详情页需要 dishId
-
区分跳转策略 — 壳路由 go,详情页 go+push
-
接受无前序上下文 — 页面用参数独立加载
-
补 pending 机制 — 冷启动时缓存并消费
页面参数、壳路由关系和冷启动上下文都要一起考虑。这一步做好了,鸿蒙系统入口能力才算真正接进产品里。
更多推荐



所有评论(0)