适合谁看

  • 已经有 Flutter 页面,想加鸿蒙系统直达的人

  • 正在做 Intents Kit 或冷启动导航的人

  • 想避免"能跳到页面但体验很怪"的开发者

问题背景

鸿蒙系统直达和应用内点击进入,最大的不同是:

维度

应用内点击

系统直达

页面上下文

已经准备好

未必准备好

参数来源

前一个页面传值

系统入口传参

壳路由状态

正确

未必正确

页面栈

有前序页面

可能没有

所以"原来能打开的页面",不一定天然适合被系统直接打开。

项目中的真实场景

食界探味当前已经支持系统直达的页面:

页面

pageId

额外参数

跳转方式

搜索页

search

直接 go

AI 助手页

ai_assistant

直接 go

心愿单页

wish_box

直接 go

探索页

explore

直接 go

菜品详情页

dish_detail

dishId

go + push

核心实现

第一步:给页面定义稳定的"系统语义入口"

不要直接让系统入口认识 Flutter route path。更稳的做法是先定义 pageId

为什么要用 pageId 而不是路由路径:

方式

示例

问题

路由路径

/search/dish/:id

和 Flutter 实现耦合

pageId

searchdish_detail

和实现解耦

食界探味的 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)

go(route)

直接切换 Tab

非壳路由页(search)

go('/explore') + push(route)

先回主页,再 push,返回栈正确

详情页(dish/:id)

go('/explore') + push('/dish/$dishId')

先回主页,再 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 页面支持鸿蒙系统直达

关键代码位置

文件

作用

app/ohos/entry/src/main/resources/base/profile/insight_intent.json

pageId 定义

app/ohos/entry/src/main/ets/entryability/InsightIntentExecutorImpl.ets

参数校验

app/ohos/entry/src/main/ets/plugins/IntentNavigationPlugin.ets

pending 缓存

app/lib/core/platform/intent_navigation_channel.dart

路由映射

app/lib/app.dart

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 页面支持鸿蒙系统直达,关键不是"能不能打开",而是"能不能自然承接":

  1. 定义 pageId — 用业务语义而非路由路径

  2. 判断参数需求 — 功能页无参数,详情页需要 dishId

  3. 区分跳转策略 — 壳路由 go,详情页 go+push

  4. 接受无前序上下文 — 页面用参数独立加载

  5. 补 pending 机制 — 冷启动时缓存并消费

页面参数、壳路由关系和冷启动上下文都要一起考虑。这一步做好了,鸿蒙系统入口能力才算真正接进产品里。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐