TF-4425 Fix load more not triggered on iOS 18 when fast scrolling to bottom (#4428)
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
# 0075. Fix load more not triggered on iOS when fast scrolling to bottom
|
||||||
|
|
||||||
|
Date: 2026-03-31
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- Issues:
|
||||||
|
- [TF-4425 Bug: load more is not working](https://github.com/linagora/tmail-flutter/issues/TF-4425)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
On iOS, `handleLoadMoreEmailsRequest()` was never triggered when users scrolled to the bottom of the email list. The original condition used exact equality:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root cause:** iOS uses `BouncingScrollPhysics` (spring-based simulation), which causes `pixels` to either overshoot or settle slightly short of `maxScrollExtent` depending on scroll speed and device performance. In both cases, `ScrollEndNotification` fires when `pixels != maxScrollExtent`, so the condition is never satisfied.
|
||||||
|
|
||||||
|
This behavior is device-dependent — newer/faster devices (iPhone 13 Pro Max, iOS 18) are less affected, while older/slower devices (iPhone 7, iOS 15) exhibit larger deviations. Android is unaffected because `ClampingScrollPhysics` clamps `pixels` exactly within `[0, maxScrollExtent]`.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Apply `ClampingScrollPhysics` on iOS only, and replace `==` with `>=`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// physics
|
||||||
|
physics: PlatformInfo.isIOS
|
||||||
|
? const ClampingScrollPhysics()
|
||||||
|
: const AlwaysScrollableScrollPhysics(),
|
||||||
|
|
||||||
|
// condition
|
||||||
|
scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent
|
||||||
|
```
|
||||||
|
|
||||||
|
`ClampingScrollPhysics` ensures `pixels` is clamped exactly to `maxScrollExtent` at the boundary, making the condition deterministic. The `>=` adds a defensive guard for any residual edge cases.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Load more works correctly across all iOS versions and device generations.
|
||||||
|
- No behavior change on Android or Web.
|
||||||
|
- iOS loses the native bounce feel at list boundaries. Acceptable for an email list UI.
|
||||||
|
- No risk of duplicate requests due to the existing `isRunning` guard.
|
||||||
@@ -693,7 +693,7 @@ class SearchEmailView extends GetWidget<SearchEmailController>
|
|||||||
onNotification: (ScrollNotification scrollInfo) {
|
onNotification: (ScrollNotification scrollInfo) {
|
||||||
if (scrollInfo is ScrollEndNotification
|
if (scrollInfo is ScrollEndNotification
|
||||||
&& controller.searchMoreState != SearchMoreState.waiting
|
&& controller.searchMoreState != SearchMoreState.waiting
|
||||||
&& scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent
|
&& scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent
|
||||||
&& scrollInfo.metrics.axisDirection == AxisDirection.down) {
|
&& scrollInfo.metrics.axisDirection == AxisDirection.down) {
|
||||||
controller.searchMoreEmailsAction();
|
controller.searchMoreEmailsAction();
|
||||||
}
|
}
|
||||||
@@ -701,7 +701,9 @@ class SearchEmailView extends GetWidget<SearchEmailController>
|
|||||||
},
|
},
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
controller: controller.resultSearchScrollController,
|
controller: controller.resultSearchScrollController,
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: PlatformInfo.isIOS
|
||||||
|
? const ClampingScrollPhysics()
|
||||||
|
: const AlwaysScrollableScrollPhysics(),
|
||||||
key: const PageStorageKey('list_presentation_email_in_search_view'),
|
key: const PageStorageKey('list_presentation_email_in_search_view'),
|
||||||
itemCount: listPresentationEmail.length,
|
itemCount: listPresentationEmail.length,
|
||||||
itemBuilder: (context, index) => _buildEmailItem(
|
itemBuilder: (context, index) => _buildEmailItem(
|
||||||
|
|||||||
@@ -366,7 +366,9 @@ class ThreadView extends GetWidget<ThreadController>
|
|||||||
Widget listView = ListView.separated(
|
Widget listView = ListView.separated(
|
||||||
key: const PageStorageKey('list_presentation_email_in_threads'),
|
key: const PageStorageKey('list_presentation_email_in_threads'),
|
||||||
controller: controller.listEmailController,
|
controller: controller.listEmailController,
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: PlatformInfo.isIOS
|
||||||
|
? const ClampingScrollPhysics()
|
||||||
|
: const AlwaysScrollableScrollPhysics(),
|
||||||
itemCount: listPresentationEmail.length + 2,
|
itemCount: listPresentationEmail.length + 2,
|
||||||
itemBuilder: (context, index) => Obx(() {
|
itemBuilder: (context, index) => Obx(() {
|
||||||
if (index == listPresentationEmail.length) {
|
if (index == listPresentationEmail.length) {
|
||||||
@@ -440,7 +442,7 @@ class ThreadView extends GetWidget<ThreadController>
|
|||||||
|
|
||||||
bool _handleScrollNotificationListener(ScrollNotification scrollInfo) {
|
bool _handleScrollNotificationListener(ScrollNotification scrollInfo) {
|
||||||
if (scrollInfo is ScrollEndNotification &&
|
if (scrollInfo is ScrollEndNotification &&
|
||||||
scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent &&
|
scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent &&
|
||||||
!controller.loadingMoreStatus.value.isRunning &&
|
!controller.loadingMoreStatus.value.isRunning &&
|
||||||
scrollInfo.metrics.axisDirection == AxisDirection.down
|
scrollInfo.metrics.axisDirection == AxisDirection.down
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user