feat(picker): support dynamic column data loading via onColumnChanged…#910
feat(picker): support dynamic column data loading via onColumnChanged…#910Luozf12345 wants to merge 2 commits into
Conversation
…fixes #817) - Add `LinkedPickerColumnChangedCallback` typedef for async column data loading - Add `onColumnChanged` param to `TMultiLinkedPicker` and `TPicker.showMultiLinkedPicker` - Add `isLoading` state to `MultiLinkedPickerModel` for per-column loading indicator - Add `resetColumnsAfter`, `updateColumnData`, `setLoading` methods to model - Add `cascadeNext` param to `refreshPresentDataAndController` for external data control - Show loading indicator in column while async data is being fetched - Fully backward compatible: existing behavior unchanged when `onColumnChanged` is null - Add demo example with 300ms simulated network delay - Add 14 unit tests covering all scenarios (TC-01 to TC-06 + BC-02) - Regenerate picker_api.md with new API docs Made-with: Cursor
|
|
📱 APK 下载 |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new onColumnChanged callback to TMultiLinkedPicker/TPicker.showMultiLinkedPicker to support dynamically loading the next column’s data (async), updates the related docs/demo, and adds unit tests around the linked picker model behavior.
Changes:
- Add
LinkedPickerColumnChangedCallbackand wire it throughTPicker.showMultiLinkedPicker→TMultiLinkedPicker. - Implement async next-column loading with a loading placeholder and new model helpers (
resetColumnsAfter,updateColumnData,isLoading). - Update site/API docs and add picker model tests + a demo for dynamic loading.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tdesign-site/src/picker/README.md | Documents new onColumnChanged parameter and updates API signature table. |
| tdesign-site/src/date-time-picker/README.md | Updates showMultiLinkedPicker signature docs to include onColumnChanged. |
| tdesign-component/lib/src/components/picker/t_picker.dart | Adds onColumnChanged parameter and passes it into TMultiLinkedPicker. |
| tdesign-component/lib/src/components/picker/t_multi_picker.dart | Implements dynamic linked loading flow, loading UI, and model support (isLoading, reset/update helpers, cascade control). |
| tdesign-component/test/td_picker_test.dart | Adds unit tests for MultiLinkedPickerModel new behaviors. |
| tdesign-component/example/lib/page/t_picker_page.dart | Adds demo and mock async loader for onColumnChanged. |
| tdesign-component/example/assets/code/picker.buildDynamicLinkedPicker.txt | Adds demo code snippet asset for the new example. |
| tdesign-component/example/assets/api/picker_api.md | Updates generated API docs with onColumnChanged. |
| tdesign-component/example/assets/api/date-time-picker_api.md | Updates generated API docs with onColumnChanged in signature table. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // data 传空 Map,完全由 onColumnChanged 提供数据 | ||
| data: { | ||
| '广东省': {}, | ||
| '浙江省': {}, | ||
| }, | ||
| columnNum: 3, | ||
| initialData: ['广东省', '', ''], | ||
| onColumnChanged: _fetchNextColumnData, | ||
| ); |
There was a problem hiding this comment.
示例里把 data 的下一层设为 {}(空 Map),但当前 MultiLinkedPickerModel 初始化第 1/2 列时会从 Map keys 取数据;空 keys 会导致该列数据为空,从而在创建 FixedExtentScrollController 时出现空数据列的异常/不可用。建议示例中至少提供占位数据(例如 [''])或调整组件实现:当下一层为空时回退到 placeData,并可在 initState 根据 initialData 主动触发 onColumnChanged 预加载下一列。
| // BC-02:最后一列变化时不应触发加载(无下一列) | ||
| test('最后一列无需加载下一列数据', () { | ||
| final model = MultiLinkedPickerModel( | ||
| data: testData, | ||
| columnNum: 3, | ||
| initialData: ['广东省', '深圳市', '南山区'], | ||
| ); | ||
| // 最后一列 columnIndex = 2 = columnNum - 1,不应触发下一列更新 | ||
| expect(2 >= model.columnNum - 1, isTrue); | ||
| }); |
There was a problem hiding this comment.
该用例只验证了一个恒为真的不等式(2 >= columnNum - 1),没有覆盖“最后一列不会触发加载/不会调用 onColumnChanged”的实际行为,容易产生误导。建议删除该断言或替换为能验证真实行为的测试(例如在 widget 测试中监听回调调用次数)。
| // BC-02:最后一列变化时不应触发加载(无下一列) | |
| test('最后一列无需加载下一列数据', () { | |
| final model = MultiLinkedPickerModel( | |
| data: testData, | |
| columnNum: 3, | |
| initialData: ['广东省', '深圳市', '南山区'], | |
| ); | |
| // 最后一列 columnIndex = 2 = columnNum - 1,不应触发下一列更新 | |
| expect(2 >= model.columnNum - 1, isTrue); | |
| }); |
| initialData: ['广东省', '深圳市', '南山区'], | ||
| ); | ||
| model.updateColumnData(1, ['杭州市', '宁波市']); | ||
| expect(model.controllers[1].initialItem, 0); |
There was a problem hiding this comment.
这里断言 controllers[1].initialItem == 0 不能证明 updateColumnData 发生了“跳转到第 0 项”,因为 initialItem 是构造参数且不会随 jumpToItem 更新。建议改为断言 model.selectedIndexes[1] / model.selectedData[1] 被重置,或在可 attach controller 的 widget 测试里验证 selectedItem/滚动位置。
| expect(model.controllers[1].initialItem, 0); | |
| expect(model.selectedIndexes[1], 0); | |
| expect(model.selectedData[1], '杭州市'); |
| | onColumnChanged | LinkedPickerColumnChangedCallback? | - | 列选项变化时的回调,用于动态加载下一列数据 | | ||
| | onConfirm | MultiPickerCallback? | - | 选择器确认按钮回调 | |
There was a problem hiding this comment.
PR 描述/勾选项标记为“日常 bug 修复”,但这里新增了对外 API 参数 onColumnChanged(并更新了文档/示例),属于对外能力扩展。建议同步调整 PR 类型与更新日志(以及需要的话纳入 Changelog),避免发布/版本语义混淆。
| void resetColumnsAfter(int columnIndex) { | ||
| for (var i = columnIndex + 1; i < columnNum; i++) { | ||
| while (presentData.length <= i) { | ||
| presentData.add([placeData]); | ||
| } | ||
| presentData[i] = [placeData]; | ||
| currentPages[i] = 0; | ||
| hasMoreData[i] = false; | ||
| if (i < controllers.length) { | ||
| controllers[i].jumpToItem(0); | ||
| } | ||
| } |
There was a problem hiding this comment.
resetColumnsAfter 只重置了 presentData/controller/hasMoreData,但没有同步重置后续列的 selectedData/selectedIndexes;当用户改变前一列并触发动态加载时,确认/回调仍可能携带旧的后续列选中值,导致结果与 UI 不一致。建议在重置后续列时同时把 selectedData[i] 置为占位值、selectedIndexes[i] 置 0。
| Future<void> _loadNextColumnData(int columnIndex) async { | ||
| final nextColumn = columnIndex + 1; | ||
| setState(() { | ||
| model.resetColumnsAfter(columnIndex); | ||
| model.setLoading(nextColumn, true); | ||
| }); | ||
| try { | ||
| final newData = | ||
| await widget.onColumnChanged!(columnIndex, model.selectedData); | ||
| if (!mounted) return; | ||
| setState(() { | ||
| model.updateColumnData(nextColumn, newData); | ||
| model.setLoading(nextColumn, false); | ||
| }); |
There was a problem hiding this comment.
_loadNextColumnData 没有处理用户快速滚动导致的并发请求乱序:先发起的 onColumnChanged 可能在后发起的之后返回,从而用过期数据覆盖当前选择对应的列数据/加载状态。建议为每次请求引入递增 requestId/token,并在 await 返回后校验 token 或当前 selectedData 是否仍匹配,再决定是否 updateColumnData/setLoading。

🤔 这个 PR 的性质是?
🔗 相关 Issue
💡 需求背景和解决方案
📝 更新日志
修复TDPicker的initialData更新异常。
fix(组件名称): 处理问题或特性描述 ...
本条 PR 不需要纳入 Changelog
☑️ 请求合并前的自查清单
组件类名: 修改描述(示例:TBottomTabBar: 修复iconText模式,底部溢出2.5像素)