mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add pull-to-refresh to the notifications list
The devices list already supported pull-to-refresh; mirror that on the notifications list by wrapping its CustomScrollView in a RefreshIndicator with AlwaysScrollableScrollPhysics. Keep the existing list visible during a refresh (_isLoading = _items.isEmpty) instead of flashing the skeleton, matching the devices list behaviour. Add widget tests covering pull-to-refresh for both lists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7b8a33606c
commit
8b8fa5ce89
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
|
|
||||||
- [ ] Mobile - Implement pull to refresh on the notifications and devices list
|
- [x] Mobile - Implement pull to refresh on the notifications and devices list
|
||||||
- [ ] Mobile - reduce lists length (# of items) so they fit on a phone in one screen (use iPhone latest gen and Google phone latest gen)
|
- [ ] Mobile - reduce lists length (# of items) so they fit on a phone in one screen (use iPhone latest gen and Google phone latest gen)
|
||||||
- [ ] Mobile - In the devices list the filters and sort buttons overlap (dont fit in the screen)
|
- [ ] Mobile - In the devices list the filters and sort buttons overlap (dont fit in the screen)
|
||||||
- [ ] When changing pages (either list) the items should change to placeholders while loading
|
- [ ] When changing pages (either list) the items should change to placeholders while loading
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class _NotificationsListState extends State<NotificationsList>
|
|||||||
final token = CancelToken();
|
final token = CancelToken();
|
||||||
_fetchToken = token;
|
_fetchToken = token;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = _items.isEmpty;
|
||||||
_error = null;
|
_error = null;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@@ -217,14 +217,18 @@ class _NotificationsListState extends State<NotificationsList>
|
|||||||
children: [
|
children: [
|
||||||
_buildNotificationsHeader(context),
|
_buildNotificationsHeader(context),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () => _fetchPage(_currentPage),
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
slivers: [
|
slivers: [
|
||||||
..._buildNotificationSlivers(context),
|
..._buildNotificationSlivers(context),
|
||||||
...widget.trailingSlivers,
|
...widget.trailingSlivers,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,4 +100,30 @@ void main() {
|
|||||||
|
|
||||||
await tearDownTree(tester);
|
await tearDownTree(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('pulling the list down refetches devices', (tester) async {
|
||||||
|
// A single reply list whose contents are swapped before the pull, so the
|
||||||
|
// same route re-serializes fresh data on the refresh request.
|
||||||
|
final devices = <Map<String, dynamic>>[
|
||||||
|
deviceJson(macAddress: '00:00:00:00:00:01'),
|
||||||
|
];
|
||||||
|
adapter.onGet('/devices', (server) => server.reply(200, devices));
|
||||||
|
|
||||||
|
await pumpScreen(tester, const DeviceList());
|
||||||
|
await pumpUntilFound(tester, find.byType(DeviceRowWide));
|
||||||
|
expect(find.byType(DeviceRowWide), findsOneWidget);
|
||||||
|
|
||||||
|
// The next fetch returns an extra device; a pull-down should pick it up.
|
||||||
|
devices.add(deviceJson(macAddress: '00:00:00:00:00:02'));
|
||||||
|
await tester.fling(
|
||||||
|
find.byType(CustomScrollView),
|
||||||
|
const Offset(0, 300),
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(DeviceRowWide), findsNWidgets(2));
|
||||||
|
|
||||||
|
await tearDownTree(tester);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,4 +105,46 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(const SizedBox());
|
await tester.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('pulling the list down refetches notifications', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(400, 600);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
|
||||||
|
// A single reply list whose contents are swapped before the pull, so the
|
||||||
|
// same route re-serializes fresh data on the refresh request.
|
||||||
|
final items = <Map<String, dynamic>>[
|
||||||
|
notificationJson(id: 1, title: 'Before refresh'),
|
||||||
|
];
|
||||||
|
adapter.onGet(
|
||||||
|
'/notifications',
|
||||||
|
(server) => server.reply(200, items),
|
||||||
|
queryParameters: {'is_new': true, 'page_offset': 0, 'page_limit': 6},
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: gruvboxDarkTheme,
|
||||||
|
home: const Scaffold(body: NotificationsList()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.textContaining('Before refresh'), findsOneWidget);
|
||||||
|
|
||||||
|
// The next fetch returns fresh data; a pull-down should pick it up.
|
||||||
|
items
|
||||||
|
..clear()
|
||||||
|
..add(notificationJson(id: 2, title: 'After refresh'));
|
||||||
|
await tester.fling(
|
||||||
|
find.byType(CustomScrollView),
|
||||||
|
const Offset(0, 300),
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.textContaining('After refresh'), findsOneWidget);
|
||||||
|
expect(find.textContaining('Before refresh'), findsNothing);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user