fix(ui): restore scrolling on UserInfoScaffold#192
Conversation
A SingleChildScrollView with AlwaysScrollableScrollPhysics nested inside SliverToBoxAdapter shrink-wraps to its content (maxScrollExtent 0) yet still wins the vertical gesture arena, so drags on the page content moved nothing and the barcode card could sit unreachable below the fold. Regression from 933cd63. Remove the inner scroller and wrap the whole CustomScrollView in the RefreshIndicator instead; AlwaysScrollableScrollPhysics on the scroll view keeps pull-to-refresh working when the content fits the viewport.
There was a problem hiding this comment.
Code Review
This pull request refactors UserInfoScaffold by wrapping the top-level CustomScrollView in a RefreshIndicator and removing the nested RefreshIndicator from SliverToBoxAdapter, which improves the pull-to-refresh behavior. It also adds comprehensive widget tests for scrolling and refreshing. The review feedback suggests disabling the RefreshIndicator and hiding the refresh button when widget.onRefresh is null, as well as simplifying the actions list expansion using the null-aware spread operator (...?).
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| body: RefreshIndicator( | ||
| onRefresh: () async { | ||
| await _handleRefresh(); | ||
| AnalyticsUtil.instance.logEvent('user_info_refresh'); | ||
| }, |
There was a problem hiding this comment.
當 widget.onRefresh 為 null 時,使用者仍然可以觸發下拉更新(Pull-to-Refresh)手勢。這不僅會顯示載入動畫但無任何實際更新,還會錯誤地發送 user_info_refresh 的 Analytics 事件,導致數據不準確。
建議在 widget.onRefresh 為 null 時停用 RefreshIndicator 的手勢偵測,並避免發送不必要的事件。
| body: RefreshIndicator( | |
| onRefresh: () async { | |
| await _handleRefresh(); | |
| AnalyticsUtil.instance.logEvent('user_info_refresh'); | |
| }, | |
| body: RefreshIndicator( | |
| notificationPredicate: widget.onRefresh == null | |
| ? (_) => false | |
| : defaultScrollNotificationPredicate, | |
| onRefresh: () async { | |
| if (widget.onRefresh == null) return; | |
| await _handleRefresh(); | |
| AnalyticsUtil.instance.logEvent('user_info_refresh'); | |
| }, |
| height: 24.0, | ||
| width: 24.0, | ||
| actions: <Widget>[ | ||
| ...widget.actions ?? <Widget>[], |
| IconButton( | ||
| icon: _isRefreshing | ||
| ? SizedBox( | ||
| width: 20, | ||
| height: 20, | ||
| child: CircularProgressIndicator( | ||
| strokeWidth: 2, | ||
| color: isDark | ||
| ? colorScheme.onSurface | ||
| : colorScheme.onPrimary, | ||
| ), | ||
| ) | ||
| : const Icon(Icons.refresh_rounded), | ||
| onPressed: _isRefreshing ? null : _handleRefresh, | ||
| ), |
There was a problem hiding this comment.
當 widget.onRefresh 為 null 時,AppBar 右側仍會顯示重新整理按鈕,且點擊後無任何反應。
建議僅在 widget.onRefresh 不為 null 時才顯示此按鈕,以提供更直覺的 UI 體驗。
| IconButton( | |
| icon: _isRefreshing | |
| ? SizedBox( | |
| width: 20, | |
| height: 20, | |
| child: CircularProgressIndicator( | |
| strokeWidth: 2, | |
| color: isDark | |
| ? colorScheme.onSurface | |
| : colorScheme.onPrimary, | |
| ), | |
| ) | |
| : const Icon(Icons.refresh_rounded), | |
| onPressed: _isRefreshing ? null : _handleRefresh, | |
| ), | |
| if (widget.onRefresh != null) | |
| IconButton( | |
| icon: _isRefreshing | |
| ? SizedBox( | |
| width: 20, | |
| height: 20, | |
| child: CircularProgressIndicator( | |
| strokeWidth: 2, | |
| color: isDark | |
| ? colorScheme.onSurface | |
| : colorScheme.onPrimary, | |
| ), | |
| ) | |
| : const Icon(Icons.refresh_rounded), | |
| onPressed: _isRefreshing ? null : _handleRefresh, | |
| ), |
問題
NKUST-ITC/NKUST-AP-Flutter#434 回報:個人資料頁的學生條碼/QRcode
顯示不完整,而且頁面無法上下捲動。
原因
933cd63 重構後,頁面內容包在
SliverToBoxAdapter > RefreshIndicator > SingleChildScrollView(AlwaysScrollableScrollPhysics)裡。
SliverToBoxAdapter給子元件無界高度,內層SingleChildScrollView因此撐成內容全高,自身 maxScrollExtent 為 0,實際捲不動;但
AlwaysScrollableScrollPhysics讓它照樣接收垂直拖曳,最內層 scrollable在手勢競爭(gesture arena)中勝出,外層
CustomScrollView收不到手勢。結果整頁鎖死:內容超過一屏時(小螢幕或大字體),條碼卡片卡在畫面外,
捲不到也看不完整。
修正
SingleChildScrollView與RefreshIndicator,SliverToBoxAdapter直接放內容RefreshIndicator改包整個CustomScrollViewCustomScrollView設AlwaysScrollableScrollPhysics,內容不滿一屏時仍能下拉更新(與
score_scaffold.dart既有寫法一致)驗證
test/scaffold/user_info_scaffold_test.dart兩條 widget test:修正前失敗(兩次拖曳後條碼底部停在 817 未動),修正後通過。
onRefresh(修正前後皆通過,作為回歸防護)。flutter test:ap_common_flutter_ui 全部 34 條通過(Flutter 3.38.10)dart analyze --fatal-infos無問題,dart format無變更Ref: NKUST-ITC/NKUST-AP-Flutter#434