Skip to content

feat: 为 Android 侧 Widget 添加交互,为日程添加 deep link 支持#148

Open
LyCecilion wants to merge 1 commit into
BenderBlog:mainfrom
LyCecilion:android-widget-interaction
Open

feat: 为 Android 侧 Widget 添加交互,为日程添加 deep link 支持#148
LyCecilion wants to merge 1 commit into
BenderBlog:mainfrom
LyCecilion:android-widget-interaction

Conversation

@LyCecilion

Copy link
Copy Markdown

摘要

该 Pull Request 为 Android 侧日程 Widget 添加点击交互功能,同时为日程引入 deep link 支持。应用现在可以处理来自 Widget(或外部启动)的链接,直接跳转到指定的课程、考试或实验详情页。 具体地,点击该 Widget 的头部(XDYou 日程信息M月D日 周X 第 W 周)会跳转到日程页面;点击该 Widget 的具体日程可以跳转到其详情页面,效果类似于在 app 日程页面点击某一日程后查看详情。

Android 侧 Widget

主要变更

在 Android Widget 侧:零音为 Glance Widget 的头部和日程行添加了点击动作。点击头部后,头部通过内部 URI 直接打开日程页面。点击日程行时,日程行通过携带日程元数据的内部 URI 打开日程页面,日程页面匹配对应日程后打开对应日程。这里,日程元数据包括来源类型、索引、周索引、天索引、开始/结束时间等。为达成这一点,零音为 TimeLineItem 扩展了点击定位所需的元数据,同时保留了现有的 今天/明天 切换功能。

在 Flutter 侧:零音添加了 ClassTableLaunchTarget 用于解析小部件的启动 URI;添加了一个小部件启动处理器,将有效的小部件 URI 转换为日程页面的导航;将 HomeWidget.widgetClickedHomeWidget.initiallyLaunchedFromHomeWidget() 连接到主页;通过现有的路由解析器将启动目标数据传递到日程页面。在日程页面,应用首先会进行匹配:如果匹配,则打开详情;否则进入日程页面而非直接失败。

在 Android 清单侧,零音为 MainActivity 添加了 home_widget 启动意图动作。这里有一个潜在问题,零音不确定自己是否 properly handle 了该问题,将在后文说明。

设计说明

Android 小部件是使用 Jetpack Glance 实现的原生 Android 应用小部件。考虑到该项目已经在使用 home_widget,故该 PR 力求桥接层精简。具体地:

Glance Widget 点击
  -> home_widget 通过 URI 启动 MainActivity
  -> Flutter 从 home_widget 接收 URI
  -> Flutter 将其解析为 ClassTableLaunchTarget
  -> 打开现有的课表路由
  -> 复用现有的安排详情 UI

零音不愿引入自定义的平台通道或一套并行的 Android <-> Flutter 导航系统。对于日程详情导航,Widget 不会将 serialized 的完整日程对象传回 Flutter,而是传递相对稳定的标识元数据,让 Flutter 根据自己的当前课表状态匹配。零音预期 Android widget 数据模型的轻量化和交流的简洁,避免跨平台重复完整的详情渲染契约。

考虑到课程安排、考试和实验的数据标识方式不同,因此,所有课程通过来源、索引、周/天 和 节次 范围匹配;考试通过索引和开始时间匹配;实验通过索引和时间范围匹配,因此 Widget 行 URI 同时包含了结构字段和时间字段。

潜在问题

该 feature 的引入可能会导致一个潜在问题。具体地,在应用已经打开时回到桌面再点击小部件,Flutter 内置的 deep link 机制会尝试将 Widget 的 URI 解释为普通路由,从而会引发错误

Could not find a generator for route RouteSettings("/?source=...", null)
潜在问题导致的报错

为了解决这一问题,零音为 MainActivity 禁用了 Flutter 的自动 deep link 处理(通过 meta-data 设置 flutter_deeplinking_enabledfalse),并添加了防御性的 onGenerateRoute 回滚,以确保在极为特殊的情况下,若 Widget 路由仍然到达了 Flutter 路由解析器,也不会导致应用崩溃。

但正如前文所说,零音并不确定是否已 properly handle 了此问题。

测试

零音在自己的设备上完成了测试,表现符合该 PR 的预期。

说明

本 PR 有意将 xdyou://classtable 作为内部 Widget 的启动 URI。未在 Android 中注册公开的外部应用链接 scheme,因此该变更不会有意暴露新的公开 deep link URI。即,本 PR 中的 deep link 支持仅用于内部 Widget 交互,并不作为公共 API 对外公开。

Copilot AI review requested due to automatic review settings May 30, 2026 03:55
@BenderBlog

BenderBlog commented May 30, 2026

Copy link
Copy Markdown
Owner

Bender2995-max-reasoning 模型运行中

Q 您让我捋捋,我加命名路由了应该?
A 正确,我在 lib/routing/routes.dart 加了命名路由,也就是说,深链的一个必要条件应该满足了。

Q 那该功能涉及何功能?
A 涉及 home_widget 相关功能,代码待核实。

Q 根据描述,有何问题?
A 未阅读代码,简单浏览描述,暂时问题如下:

  1. 目前的链路对未登录状况是否有反应?
  2. 目前登录状况对未获取课表是否有合适状况?
  3. 需要确认其提出的问题解决方式。
  4. 是否有必要,或者核实,在课程表页面中实现一个内部路由?

Q 对贡献有啥进一步安排?
A 根据目前得到的非常不完整,不全面信息,准备如此处理:

  1. 该更改将会合并,但下个版本不行,下个版本要赶紧发了。
  2. 阅读其实现方式,这将很有价值。
  3. 请求贡献者不要用第三人称指代自己。

Bender2995-max-reasoning 结束推理,推理结果公示如上,具体输出如下。

您真厉害,解决了我多年前想实现的一个功能。当时我搞这个没搞出来卡在深链和状态管理混乱上,现在应该都理清了。

请您查看上面的推理过程,然后慢慢写,我今晚或者明天审下代码。这个功能估计会折腾我们一段时间才会发布了。请您保持耐心。

追加:QQ 私信我功能演示视频,我得去复习相关知识。

@LyCecilion

Copy link
Copy Markdown
Author

LyRinAI-6.5 模型运行中

好的,现在我是用户了。现在我需要回答这些问题。我将逐条展开解释。

LyRinAI-6.5 结束推理,推理结果公示如上,具体输出如下。


关于 lib/routing/routes.dart

对,lib/routing/routes.dart 确实存在,而其中 Routes.classTable 亦已经存在。routes.dart 形如

class Routes {
  static const classTable = '/class-table';

  static Route<T> resolveRoute<T extends Object?>(
    String name, {
    Object? arguments,
  }) {
    return MaterialPageRoute<T>(
      settings: RouteSettings(name: name, arguments: arguments),
      builder: (_) => _resolve(name, arguments),
    );
  }
}

不过虽然这使得项目内部导航入口条件得以满足,但它并没有注册到 MaterialApp.routesonGenerateRoute 之类,所以 Flutter 的自动 deep link 不会自动走这条路径。因此:

解释前文的潜在问题

在 XDYou 位于后台时,如果回到桌面再次点击 Widget,home_widget 向应用发起

xdyou://classtable?source=school&...

但 Flutter 将它转换为一个普通 route

/?source=school&index=...

从而 Flutter 去询问 MaterialApp 是否注册该界面。显然并没有,于是 Flutter 报告错误

Could not find a generator for route RouteSettings("/?source=...", null)

在这里,本 PR 的修改尝试绕过 Flutter 对 deep link 的接管,即:在 Widget 被点击时,home_widget 插件收到该 click,经由

 -> HomePage 的 HomeWidget.widgetClicked.listen(...)
 -> handleWidgetLaunchUri(...)
 -> context.pushReplacementNamed(Routes.classTable, arguments: target)
 -> XDYou 的 Routes.resolveRoute(...)

它实际上最后转交给了 XDYou 独立实现的 route resolver。

也就是说,项目内目前已有的 Routes.classTable 是这个链路的必要内部入口,但它本身不能让 Android/Flutter 自动 deep link 成功。该 PR 并没有对整个 route 的逻辑进行变更,以确保修改是最小化的。

未登录和未获取课表时的行为

未登录时,Widget 会显示位于未登录状态,因为 ClassTableDataHolder 会读取 WidgetState.jsonloggedIn。这个时候点击 Widget 的头部,会启动 App;而 App 会判断 isFirst == true,所以首页仍然是 LoginWindow,不会进入课表页。未登录状态不存在日程行,所以不会跳转到日程页面。

登录但没有获取课表时……分两种情况讨论吧。

第一种情况是程序第一次运行,用户刚登录,但课表还没有获取完全。经过测试,在第一次登录后和完整获取所有数据(课表、实验、考试)之前打断程序,Widget 仍然会显示「未登录」状态。但这并非本次 PR 引起的问题。

注意到 Widget 侧在加载时会先读 WidgetState.json,如果该文件不存在或 loggedIn != true 则直接当成未登录;否则再去读课程表、考试、实验等的 JSON 文件。但 Flutter 侧是在首页所有内容加载完成后才去写 WidgetState.json 等内容,而在写入之前,Flutter 侧需要跑完

Semester
Classtable
Exam
PhysicsExperiment
OtherExperiment
Library
SchoolCard
Electricity
Notification

所以就算获取成功了大部分内容,因为 WidgetState.json 的加载过晚,Widget 仍然会以为未登录。

第二种情况是用户日常打开程序的时候,课表有刷新;程序可能已经同步了 Classtable,但在同步 Exam 之类的时候被打断。这个时候理论上课表应该是新的,因为 classtable_session.dart 似乎在获取完课表之后会立刻写 JSON。

叠甲

本质上该 PR 只追加了点击后跳转的功能,对 Widget 的原操作逻辑,即获取 Flutter 侧课表之类的逻辑没有进行任何改动。

是否实现了课程表页面内的内部路由

没有。没有新增一个课程表内部的 route,也没有把详情页做成独立的 route。目前的做法是,打开 Routes.classTable 后传入一个 ClassTableLaunchTarget,随后 ContentClassTablePage 根据 target 匹配被点击的日程,如果匹配到了就调用原本的 BothSideSheet.show(),也就是说具体日程详情仍然是课表页面内部的一个 sheet 状态,而非一个独立的 Flutter route。

因为现有的 XDYou,在课表页面点击一个日程,本就是 ClassCard 里打开 BothSideSheet 而非单独页面。正如前文所说的减少改动面,该 PR 并没有将详情页抽成新路由。因为我本身对 Flutter 不太熟悉,不敢大刀阔斧地动,也不太确定开发者是否有这方面的预期。

附加

QQ 的视频今晚发吧,待会有事。贡献的话…到期末周前应该都有时间,不必着急.png

@BenderBlog

Copy link
Copy Markdown
Owner

代码看完了,没啥大问题。下个版本我要做 iOS 方面的修改,还有路由方面的修改。
人家 Flutter 官方要用 go_router 了,哎……
https://docs.flutter.dev/ui/navigation/deep-linking
建议先忙期末复习吧。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants