Skip to content

Commit c7c1c87

Browse files
committed
wip use "infinite" scrolling list view
1 parent 6ff89fa commit c7c1c87

2 files changed

Lines changed: 92 additions & 85 deletions

File tree

lib/pages/cakepay/cakepay_vendors_view.dart

Lines changed: 71 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import 'package:flutter_svg/flutter_svg.dart';
44

55
import '../../services/cakepay/cakepay_service.dart';
66
import '../../services/cakepay/src/models/card.dart';
7-
import '../../services/cakepay/src/models/vendor.dart';
87
import '../../themes/stack_colors.dart';
98
import '../../utilities/assets.dart';
109
import '../../utilities/constants.dart';
10+
import '../../utilities/logger.dart';
1111
import '../../utilities/text_styles.dart';
1212
import '../../utilities/util.dart';
1313
import '../../widgets/background.dart';
@@ -16,6 +16,7 @@ import '../../widgets/custom_buttons/app_bar_icon_button.dart';
1616
import '../../widgets/desktop/desktop_dialog.dart';
1717
import '../../widgets/desktop/desktop_dialog_close_button.dart';
1818
import '../../widgets/icon_widgets/credit_card_icon.dart';
19+
import '../../widgets/infinite_scroll_list_view.dart';
1920
import '../../widgets/loading_indicator.dart';
2021
import '../../widgets/rounded_container.dart';
2122
import '../../widgets/stack_text_field.dart';
@@ -31,20 +32,21 @@ class CakePayVendorsView extends StatefulWidget {
3132
}
3233

3334
class _CakePayVendorsViewState extends State<CakePayVendorsView> {
34-
List<CakePayVendor> _vendors = [];
3535
List<String> _countryNames = [];
3636
String? _selectedCountry;
37+
String? _searchQuery;
3738
bool _loading = true;
38-
String? _error;
3939

4040
final _searchController = TextEditingController();
4141
final _searchFocusNode = FocusNode();
4242
final _countrySearchController = TextEditingController();
4343

44+
final _listController = InfiniteScrollListController();
45+
4446
@override
4547
void initState() {
4648
super.initState();
47-
_loadVendors();
49+
_loadCountries();
4850
}
4951

5052
@override
@@ -55,59 +57,60 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
5557
super.dispose();
5658
}
5759

58-
List<CakePayCard> _availableCards() =>
59-
_vendors.expand((v) => v.cards.where((c) => c.available)).toList();
60-
61-
/// Derive a country list from the loaded vendors so we don't need the
62-
/// broken /marketplace/countries/ endpoint.
63-
Future<void> _deriveCountries() async {
64-
// naive caching
65-
if (_countryNames.isNotEmpty) return;
66-
67-
final response = await CakePayService.instance.client.getAllCountries();
60+
Future<({List<CakePayCard> cards, int? nextPage})> _fetchCards(
61+
int page,
62+
) async {
63+
final response = await CakePayService.instance.client.getVendors(
64+
page: page,
65+
pageSize: 50,
66+
country: _selectedCountry,
67+
search: _searchQuery,
68+
);
6869

6970
if (response.hasError || response.value == null) {
70-
if (mounted) {
71-
setState(() {
72-
_error = response.exception?.message ?? "Failed to load countries";
73-
});
74-
}
75-
} else {
76-
_countryNames =
77-
response.value!
78-
.where((e) => e.available)
79-
.map((e) => e.name)
80-
.toSet()
81-
.toList(growable: false)
82-
..sort();
71+
throw response.exception ??
72+
Exception("Unknown exception with value is null????");
8373
}
74+
75+
return (
76+
cards: response.value!.vendors
77+
.expand((e) => e.cards.where((e) => e.available))
78+
.toList(),
79+
nextPage: response.value!.nextPage,
80+
);
8481
}
8582

86-
Future<void> _loadVendors() async {
83+
Future<void> _loadCountries() async {
84+
// naive caching
85+
if (_countryNames.isNotEmpty) return;
86+
8787
setState(() {
8888
_loading = true;
89-
_error = null;
9089
});
9190

92-
final resp = await CakePayService.instance.client.getVendors(
93-
country: _selectedCountry,
94-
search: _searchController.text.trim().isNotEmpty
95-
? _searchController.text.trim()
96-
: null,
97-
);
91+
try {
92+
final response = await CakePayService.instance.client.getAllCountries();
9893

99-
if (!mounted) return;
100-
101-
if (resp.hasError || resp.value == null) {
102-
setState(() {
103-
_error = resp.exception?.message ?? "Failed to load gift cards";
104-
});
105-
} else {
106-
_vendors = resp.value!;
107-
await _deriveCountries();
94+
if (response.hasError || response.value == null) {
95+
Logging.instance.e(
96+
response.exception?.message ?? "Failed to load countries",
97+
error: response.exception,
98+
stackTrace: StackTrace.current,
99+
);
100+
} else {
101+
setState(() {
102+
_countryNames =
103+
response.value!
104+
.where((e) => e.available)
105+
.map((e) => e.name)
106+
.toSet()
107+
.toList(growable: false)
108+
..sort();
109+
});
110+
}
111+
} finally {
112+
if (mounted) setState(() => _loading = false);
108113
}
109-
110-
if (mounted) setState(() => _loading = false);
111114
}
112115

113116
Future<void> _onCardTapped(CakePayCard card) async {
@@ -119,7 +122,6 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
119122
@override
120123
Widget build(BuildContext context) {
121124
final isDesktop = Util.isDesktop;
122-
final cards = _availableCards();
123125

124126
return ConditionalParent(
125127
condition: isDesktop,
@@ -180,7 +182,10 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
180182
_SearchField(
181183
controller: _searchController,
182184
focusNode: _searchFocusNode,
183-
onSubmitted: (_) => _loadVendors(),
185+
onSubmitted: (value) {
186+
setState(() => _searchQuery = value);
187+
_listController.refresh();
188+
},
184189
),
185190
if (_countryNames.isNotEmpty) ...[
186191
SizedBox(height: isDesktop ? 12 : 12),
@@ -190,34 +195,33 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
190195
searchController: _countrySearchController,
191196
onChanged: (value) {
192197
setState(() => _selectedCountry = value);
193-
_loadVendors();
198+
_listController.refresh();
194199
},
195200
),
196201
],
197202
SizedBox(height: isDesktop ? 16 : 12),
198203
Expanded(
199204
child: _loading
200205
? const LoadingIndicator(width: 48, height: 48)
201-
: cards.isEmpty
202-
? Center(
203-
child: Text(
204-
_error ?? "No gift cards found",
205-
style: isDesktop
206-
? STextStyles.desktopTextSmall(context)
207-
: STextStyles.itemSubtitle(context),
208-
),
209-
)
210-
: ListView.separated(
211-
shrinkWrap: isDesktop,
212-
primary: isDesktop ? false : null,
213-
itemCount: cards.length,
206+
: InfiniteScrollListView<CakePayCard, int>(
207+
controller: _listController,
214208
padding: .only(bottom: isDesktop ? 32 : 16),
215-
separatorBuilder: (_, __) =>
209+
firstPageKey: 1,
210+
separatorBuilder: (_, _) =>
216211
SizedBox(height: isDesktop ? 16 : 12),
217-
itemBuilder: (_, index) => _CardTile(
218-
card: cards[index],
219-
onTap: () => _onCardTapped(cards[index]),
220-
),
212+
fetchPage: (pageKey) async {
213+
final result = await _fetchCards(pageKey);
214+
return InfiniteScrollPage(
215+
items: result.cards,
216+
nextPageKey: result.nextPage,
217+
);
218+
},
219+
itemBuilder: (context, item, index) {
220+
return _CardTile(
221+
card: item,
222+
onTap: () => _onCardTapped(item),
223+
);
224+
},
221225
),
222226
),
223227
],

lib/services/cakepay/src/client.dart

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ class CakePayClient {
4141

4242
// -- Marketplace --
4343

44-
Future<ApiResponse<List<CakePayVendor>>> getVendors({
44+
Future<ApiResponse<({List<CakePayVendor> vendors, int? nextPage})>>
45+
getVendors({
4546
String? country,
4647
String? countryCode,
4748
String? search,
@@ -70,23 +71,25 @@ class CakePayClient {
7071
'/marketplace/vendors/',
7172
query: query,
7273
parse: (body) {
73-
final decoded = jsonDecode(body);
74-
if (decoded is List) {
75-
return decoded
76-
.whereType<Map<String, dynamic>>()
77-
.map(CakePayVendor.fromJson)
78-
.toList();
79-
}
80-
if (decoded is Map<String, dynamic>) {
81-
final results = decoded['results'];
82-
if (results is List) {
83-
return results
84-
.whereType<Map<String, dynamic>>()
85-
.map(CakePayVendor.fromJson)
86-
.toList();
87-
}
88-
}
89-
return [];
74+
final dynamic decoded = jsonDecode(body);
75+
76+
final List<dynamic> rawList = switch (decoded) {
77+
final List<dynamic> list => list,
78+
{"results": final List<dynamic> results} => results,
79+
_ => const <dynamic>[],
80+
};
81+
82+
final List<CakePayVendor> vendors = rawList
83+
.whereType<Map<String, dynamic>>()
84+
.map(CakePayVendor.fromJson)
85+
.toList();
86+
87+
final int? nextPage =
88+
(page != null && pageSize != null && vendors.length >= pageSize)
89+
? page + 1
90+
: null;
91+
92+
return (vendors: vendors, nextPage: nextPage);
9093
},
9194
);
9295
}

0 commit comments

Comments
 (0)