ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity UI 强类型导航:为什么 Route 和 ViewHandle 缺一不可

Unity UI 强类型导航:为什么 Route 和 ViewHandle 缺一不可 在 Unity 项目中UIManager.Open(ItemDetailView, itemId)往往是导航系统失控的起点。本文从物品详情页出发比较字符串、enum、View 类型、资源引用和强类型 Route进一步解释为什么只有 Route 还不够每次打开都必须返回具有实例代际语义的 ViewHandle。“从背包打开物品详情”最短可以写成UIManager.Open(ItemDetailView,itemId);短不等于简单。这一行把资源地址、页面身份、实例规则、参数类型和返回值全部藏起来了。拼错字符串只是最浅的一层问题真正难处理的是调用方拿不到一个可以继续管理的“这一次打开”。先给结论可靠的导航入口需要同时回答两个问题RouteTViewModel要打开的是哪一类页面它如何创建有什么固定策略。ViewHandle这一次具体打开产生了哪个实例代际。Route 是定义Handle 是运行时事实。只做泛型 Open 而没有 Handle仍然无法可靠处理多开、缓存复用和迟到回调。1. 五种页面标识方案怎么选方案编译检查可携带策略支持多实例重构体验主要问题字符串无需额外查表弱差身份与地址混在一起enum名称可检查需中心 switch弱一般跨模块扩展困难参数仍不类型化typeof(TView)有可查元数据一般较好View 类型泄漏到业务层创建关系仍缺失Prefab/AssetReference资源可追踪不自然一般一般调用方耦合资源系统RouteTViewModel有直接编译进对象强好需要注册或生成阶段enum 比字符串好但它只是把拼写错误变成一个封闭列表。随着 Package、热更新模块或业务程序集增加一个中央 enum 会成为合并冲突中心。直接引用 Prefab 对小项目很直观却让业务流程知道资源后端。更换 Resources/Addressables、加入 Presenter 或测试替身时调用方都要变化。2. Route 应该包含什么不应该包含什么教学化简版publicabstractclassRoute{publicstringAssetKey{get;}publicRoutePolicyPolicy{get;}}publicsealedclassRouteTViewModel:Route{publicFuncTViewModelCreateViewModel{get;}publicFuncUIView,TViewModel,IBindingContextCreateBinding{get;}publicFuncIPresenterCreatePresenter{get;}}Route 适合保存编译时稳定的装配关系资源键ViewModel 工厂BindingContext 工厂Presenter 工厂不可变页面策略静态依赖路由。Route 不应该保存当前 GameObject当前动画进度当前请求参数某个玩家的状态是否正在加载。这些属于一次打开产生的 NavigationEntry而不是页面定义。3. 为什么用 ViewModel 类型而不是 View 类型做路由泛型业务调用方通常关心“我要查看哪件物品并接收选择或取消结果”而不是“我要实例化哪个 MonoBehaviour”。ViewHandlehandlenavigator.Open(Routes.ItemDetail,newItemDetailOpenArgs(itemId));以 ViewModel 作为泛型参数有几个好处调用方不依赖 uGUI、UI Toolkit 或自定义 View 实现测试中可替换 View Provider获取页面状态时可类型化TryGetViewModelItemDetailViewModel同一个展示模型可以在不同渲染适配层复用。代价是框架需要维护 ViewContract将 ViewModel 路由映射到资产和 View 类型。这个关系适合由生成器在编译期固化。4. ViewHandle 解决了泛型 API 仍解决不了的问题假设奖励详情页允许同时打开两个varfirstnavigator.Open(Routes.Reward,newRewardParam(1001));varsecondnavigator.Open(Routes.Reward,newRewardParam(1002));这时CloseRewardViewModel()没有唯一答案。Handle 才能精确指向一次打开navigator.Close(first);一个最小实现publicreadonlystructViewHandle:IEquatableViewHandle{publicstaticViewHandleInvaliddefault;publiclongValue{get;}publicboolIsValidValue!0;internalViewHandle(longvalue)Valuevalue;}Navigator 内部使用 Handle 查找 NavigationEntrysealedclassNavigationEntry{publicViewHandleHandle;publicRouteRoute;publicobjectViewModel;publicIViewView;publicViewStateState;publicintOperationVersion;}5. Handle 为什么必须有“代际”语义最隐蔽的 Bug 发生在缓存复用第一次打开物品详情得到 Handle 10页面关闭并进入缓存第二次复用同一个 GameObject旧异步任务拿着 Handle 10 调用 Close如果复用原 Handle新打开的页面被错误关闭。正确做法是缓存对象可以复用打开身份不能复用GameObject A Handle 10 → Closed/Cached GameObject A Handle 11 → VisibleHandle 10 永远保持终态第二次打开分配 Handle 11。这与网络连接、实体句柄中的 generation counter 是同一个思想防止陈旧引用重新获得权力。6. 参数应该怎样类型化最弱的写法是object param灵活但错误延后navigator.Open(route,newWrongParam());可以让 Route 同时携带参数类型publicsealedclassRouteTViewModel,TParam{publicFuncTParam,TViewModelCreateViewModel{get;}}但 API 和生成代码会更复杂。实际设计可以分层高频核心页面使用强类型参数无参数页面保留简洁重载框架内部兼容非泛型 Route 以便统一存储动态模块可使用受控的序列化参数并在边界处验证。不要为了 API 表面统一把所有参数都降级为 Dictionary。7. 返回结果也要有明确类型物品详情可能让玩家选择“装备”或“取消”。如果只传一个Actionobject调用方和页面仍要约定装箱类型。可以在业务导航门面中定义结果publicabstractrecordItemDetailResult{publicsealedrecordEquipped(stringItemId):ItemDetailResult;publicsealedrecordCancelled:ItemDetailResult;}FUI 当前 Navigator 返回的是ViewHandle没有内置RouteTParam, TResult。项目若需要等待页面结果可以在业务门面中用TaskCompletionSourceItemDetailResult协调并让 Close、Back、取消令牌都只完成一次结果。这个包装是可继续扩展的业务能力不是 FUI 当前公开 API。最容易漏掉的不是成功选择而是页面被 Back、依赖失败或场景切换关闭时等待者永远不完成。结果协调器必须把页面 Handle 与完成源绑定并在任意终态返回Cancelled或明确异常。8. 创建过程应该是事务而不是一串副作用打开页面的实际步骤通常是解析 Route → 检查单实例/缓存 → 打开依赖 → 获取资源 Lease → 创建 ViewModel → 创建 View → 创建 BindingContext/Presenter → Bind → 执行 Enter Transition → 提交到活动表与历史任一步骤失败都要逆序撤销已经完成的动作try{leaseawaitprovider.CreateAsync(route.Descriptor,null,token);entryCreateEntry(route,lease);entry.Binding.Bind();awaitentry.Transition.EnterAsync(token);Commit(entry);returnentry.Handle;}catch{entry?.Binding?.Dispose();entry?.Presenter?.Dispose();lease?.Dispose();RollbackDependencies();throw;}真正实现时不应只依赖散落的 null 判断。可以用局部事务对象记录已获得资源确保只提交一次、只回滚一次。9. 单实例和多实例如何共存AllowMultiple false不等于 Open 时静默返回旧 Handle 就结束了。需要先定义语义已 Visible返回现有 Handle还是 BringToFront正在 Loading复用进行中的任务还是拒绝第二次请求已 Covered是否 Reveal在 Cache复用对象但生成新 Handle。正在 Exiting等待关闭完成还是取消退出推荐把默认行为固定在 RoutePolicy 中并允许少量显式OpenOptions表达这一次操作的意图。Options 不应重新定义页面固有策略。10. FUI 中这条链怎样落地FUI 的公开调用路径是[ViewContract] 声明 ↓ Source Generator typed Route / RouteRegistry / BindingRegistry ↓ Navigator.Open / OpenAsync ↓ ViewHandle NavigationEntry生成的 Route 携带资源键、ViewModel/BindingContext/Presenter 工厂和 RoutePolicy。Navigator 维护活动实例、终态、视觉顺序、历史、缓存、预加载和依赖所有权。这里应注意生成器消除了稳定关系的运行时发现但 Navigator 的状态管理仍然是手写、可测试的运行时代码。代码生成不能替代正确的导航语义。11. 实现中的坑点坑一Route 既是定义又保存运行时状态静态 Route 被多个页面实例共享写入 CurrentView 会制造交叉污染。坑二Handle 暴露内部索引如果 Handle 只是 List 下标删除和复用槽位会让旧 Handle 指向新对象。使用单调 ID 或 index generation。坑三终态一关闭就删除调用方随后查询状态只能得到“不存在”无法区分 Invalid、Closed、Failed。可以保留有界的终态记录并设置清理策略。坑四资源键成为新的全局字符串字符串仍可存在于 Route 内部但要由构建验证检查资产是否存在调用方不应自行拼接路径。坑五直接把 ViewModel 实例当句柄缓存或重新绑定时ViewModel 的复用策略可能变化对象引用也无法表达加载中和失败终态。12. 可执行验证路由资产不存在时Open 失败且没有残留活动 Entry。同一单实例路由连续 Open行为符合明确定义。多实例路由返回不同 Handle可分别关闭。缓存复用后 GameObject 相同但 Handle 不同。旧 Handle 的 Close 不影响新代际。错误参数在编译期或进入 Navigator 边界时失败。创建 BindingContext 失败时资源 Lease 和依赖全部释放。业务程序集不需要引用具体 View 类型或资源后端。下一篇会继续处理 Route 进入运行时后的问题打开和关闭不是两个方法而是一组需要防止竞态的状态迁移。资料与源码索引FUINavigation 文档FUIViewHandleFUIRoute继续阅读下一篇Unity UI 生命周期设计为什么弹窗遮挡不等于页面关闭
返回列表