android - Android Remote Mediator 类的 Flutter 等效项
问题描述
我是一名考虑迁移到 Flutter 的原生 Android 开发人员,我已经完成了 Flutter 中核心库替代方案的所有研究。我特别关注的一件事是,在处理大数据列表时。在 Android 中,我们可以使用ROOM库将这些数据保存在 SQLite 本地数据库中。更棒的是,借助 Paging 3 Android 库中提供的RemoteMediator类,我们可以创建一个无限滚动回收器,同时从本地 DB 查询数据,同时网络调用查询新数据并将其存储在 DB 中。所以回收器从数据库中查询数据,而不是从网络调用中查询数据。所以这些数据可以在没有互联网访问的情况下访问。
我知道sqflite 包是 Flutter 在 Android 中对 ROOM 的替代品,但是我们可以使用这个数据库来查询要在 ListViewBuilder 中显示的分页项目列表并让用户滚动吗?
解决方案
我一直在这样做,一开始无法弄清楚。但现在有一个可行的解决方案。我使用数据库优先方法,因此我的数据库是我的事实来源,而填充它的信息来自我们的 api 门户。
我使用无限滚动包进行分页,每次获取新页面时,我都会将数据库配置单元与结果同步,然后返回数据库的子列表。
同步是一个 upsert,因此它会更新已存在的记录并添加不存在的记录。
无限滚动包允许我们实现一些东西,首先是一个页面请求监听器,它告诉我们我们在哪个页面上,我们需要在我们的 fetchPage 方法中增加它。
这里有一篇很棒的文章
第二个是分页控制器,它允许我们追加页面或追加最后一页,因此我们需要确定最后一页的时间。
另一个小问题是管理数据库中的内容与门户中的内容之间的微小差异。例如,如果我的数据库从门户中尚不存在的 17 个条目开始,然后我开始请求页面,当我请求最后一页时,它只给了我例如 5 个结果,我需要知道将其添加到数据库中剩余的 17 个额外条目总共有 22 个,这意味着我的最后一页会有所不同,我们需要计算它以不显示重复记录并且不会错过任何项目。
说够了,给我看看代码。
这是我的适配器
import 'dart:math' as math;
import 'package:built_collection/built_collection.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import '../api/models/error_response.dart';
import '../api/models/events/event.dart';
import '../api/models/events/event_paging_response.dart';
import '../api/models/util/pagination.dart';
import '../api/models/util/portal_request.dart';
import '../api/utils/api_response.dart';
import '../api/utils/app_exceptions.dart';
import '../extensions/iterable_extension.dart';
import '../repositories/hive_event_repository.dart';
import '../repositories/event_repository.dart';
import '../repositories/event_sync_repository.dart';
class EventPagingAdapter {
EventPagingAdapter(
this.eventSyncRepository,
this.databaseEventRepository,
this.eventRepository,
);
final EventSyncRepository eventSyncRepository;
final EventRepository eventRepository;
final DatabaseEventRepository databaseEventRepository;
PagingController<int, Event>? pagingController;
Pagination? _lastPageInPortal;
Pagination? _lastPortalResponse;
bool _hasFetchedOnce = false;
bool _hasBeenDisposed = false;
int get limit => 20;
PortalRequest? _eventRequest;
PortalRequest? get eventRequest => _eventRequest;
void initialize() {
pagingController?.addPageRequestListener((pageKey) {
_requestPage(_lastPortalResponse, pageKey);
});
}
void setEventRequest(
PortalRequest eventRequest,
) {
if (_eventRequest == null) {
_eventRequest = eventRequest;
} else {
_eventRequest = _eventRequest?.rebuild(
(b) => b..search = eventRequest.search,
);
}
}
void refresh() {
if (!_hasFetchedOnce) {
_hasFetchedOnce = true;
_firstRequest();
} else {
pagingController?.refresh();
}
}
void retryLastRequest() {
pagingController?.retryLastFailedRequest();
}
void dispose() {
_hasBeenDisposed = true;
pagingController?.dispose();
}
Future<void> _requestPage(
Pagination? _lastPortalResponse,
int pageKey,
) async {
final _notNullEventRequest = _eventRequest;
if (_notNullEventRequest != null) {
EventPagingResponse? _portalPage;
try {
if (_lastPageInPortal == null && !_isLastPortalPage(_lastPortalResponse)) {
_portalPage = await _fetchPortalPage(
_notNullEventRequest,
pageKey,
);
await _syncEvents(
_portalPage?.data ?? _buildEmptyBuiltList(),
);
} else if (_isLastPortalPage(_lastPortalResponse)) {
_lastPageInPortal = _lastPortalResponse;
}
await _fetchAndAppendDBPage(
_portalPage,
pageKey,
);
} catch (error) {
if (!_hasBeenDisposed) {
_handleError(
error,
_portalPage,
pageKey,
);
}
}
}
}
Future<void> _fetchAndAppendDBPage(
EventPagingResponse? _portalPage,
int pageKey,
) async {
if (!_hasBeenDisposed) {
final page = await _fetchDBPage(
pageKey,
_portalPage?.pagination ?? _lastPageInPortal,
);
_appendPage(
page,
_portalPage?.pagination ?? _lastPageInPortal,
pageKey,
);
}
}
Future<EventPagingResponse?> _fetchPortalPage(
PortalRequest _eventRequest,
int pageKey,
) async {
final _skip = pageKey * limit;
return eventRepository.getEventsResponse(
eventRequest: _eventRequest.rebuild(
(p0) => p0
..limit = limit
..skip = _skip
..sort = 'booking_start_date'
..pagination = true,
),
);
}
Future<EventPagingResponse?> _fetchDBPage(
int pageKey,
Pagination? _portalPage,
) async {
final _lastPageInPortalCount = _portalPage?.count ?? 0;
final _skip = _portalPage != null ? pageKey * limit - (limit - _lastPageInPortalCount) : pageKey * limit;
return _getPagedDatabaseEventList(
_portalPage != null
? _eventRequest?.rebuild(
(p0) => p0..skip = _portalPage.skip,
)
: _eventRequest?.rebuild(
(p0) => p0..skip = _skip,
),
pageKey,
);
}
Future<void> _appendPage(
EventPagingResponse? hivePage,
Pagination? _portalPagination,
int pageKey,
) async {
final _isLastDBPageBackingField = await _isLastDBPage(
hivePage?.pagination,
);
//_portalPagination will be null if the last request failed, for instance we may be offline
if (_portalPagination == null && _isLastDBPageBackingField ||
_isLastPortalPage(_portalPagination) && _isLastDBPageBackingField) {
pagingController?.appendLastPage(
hivePage?.data.toList() ?? [],
);
} else {
final nextPageKey = pageKey + 1;
pagingController?.appendPage(
hivePage?.data.toList() ?? [],
nextPageKey,
);
}
}
Future<void> _syncEvents(
BuiltList<Event> eventList,
) async {
if (eventList.isNotEmpty) {
await databaseEventRepository.syncEvents(
eventList,
);
}
await eventSyncRepository.removeEvents();
}
Future<EventPagingResponse> _getPagedDatabaseEventList(
PortalRequest? _eventRequest,
int pageKey,
) async {
final _skip = _eventRequest?.skip ?? 0;
final _eventList = await _getDatabaseEventList(
_eventRequest?.search,
);
final _hiveTotal = _eventList.length;
final _count = math.min(_hiveTotal - _skip, limit).clamp(0, _hiveTotal);
final _end = _count + _skip;
return _buildEventPagingResponse(
_eventList.sublist(_skip, _end).toBuiltList(),
_buildPagination(
_eventRequest,
_count,
_hiveTotal,
),
);
}
Pagination _buildPagination(
PortalRequest? _eventRequest,
int _count,
int _hiveTotal,
) {
return Pagination(
(b) => b
..skip = _eventRequest?.skip
..count = _count
..limit = limit
..total = _hiveTotal,
);
}
EventPagingResponse _buildEventPagingResponse(
BuiltList<Event> data,
Pagination pagination,
) {
return EventPagingResponse(
(b) => b
..fats = null
..pagination = pagination.toBuilder()
..data = data.toBuilder(),
);
}
void _firstRequest() {
_requestPage(null, 0);
}
Future<int> _getEventCount(
String? search,
) async {
final eventCount = await databaseEventRepository.getEventCount(
search: search?.replaceAll(' ', '') ?? '',
);
return eventCount;
}
Future<List<Event>> _getDatabaseEventList(
String? search,
) async {
final eventList = await databaseEventRepository.getEvents(
search: search?.replaceAll(' ', '') ?? '',
);
return eventList.uniqueBy((e) => e.booking_ref).toList();
}
bool _isLastPortalPage(Pagination? newPage) {
if (newPage == null) {
return false;
}
final _skip = newPage.skip ?? 0;
final _count = newPage.count ?? 0;
final _total = newPage.total ?? 0;
return _skip + _count >= _total;
}
Future<bool> _isLastDBPage(
Pagination? newPage,
) async {
final _skip = newPage?.skip ?? 0;
final _count = newPage?.count ?? 0;
final _hiveTotal = await _getEventCount(
_eventRequest?.search,
);
return _skip + _count >= _hiveTotal;
}
Future<void> _handleError(
Object error,
EventPagingResponse? _portalPage,
int pageKey,
) async {
try {
final appException = error as AppException;
if (appException.error?.statusCode == 503) {
await _fetchAndAppendDBPage(
_portalPage,
pageKey,
);
} else {
pagingController?.error = ApiResponse.error(
appException.error?.message,
error: ErrorResponse(
(b) => b
..message = appException.error?.message
..error = error.toString()
..url = appException.error?.url,
),
);
}
} catch (e) {
pagingController?.error = ApiResponse.error(
'Application Error',
error: ErrorResponse(
(b) => b
..statusCode = -1
..message = '$error',
),
);
}
}
BuiltList<Event> _buildEmptyBuiltList() {
return BuiltList.of(
[],
);
}
}
我们从视图模型控制所有这一切
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:rxdart/rxdart.dart';
import '../../adapters/timesheet_paging_adapter.dart';
import '../../api/models/events/event.dart';
import '../../api/models/util/portal_request.dart';
class EventListViewModel {
EventListViewModel(this._eventPagingAdapter);
final EventPagingAdapter _eventPagingAdapter;
final _pagingController = PagingController<int, Timesheet>(firstPageKey: 0);
var _isPagingAdapterInitialized = false;
final searchText = BehaviorSubject<String?>.seeded('');
void _initializePagingAdapter() {
_eventPagingAdapter.pagingController = _pagingController;
_eventPagingAdapter.initialize();
}
void refresh() {
_eventPagingAdapter.refresh();
}
void updateQuery(PortalRequest portalRequest) {
_eventPagingAdapter.setTimesheetRequest(portalRequest);
refresh();
}
void retryLastRequest() {
_eventPagingAdapter.pagingController?.retryLastFailedRequest();
}
PagingController<int, Timesheet> getPagingController() {
if (_isPagingAdapterInitialized == false) {
_isPagingAdapterInitialized = true;
_initializePagingAdapter();
}
return _eventPagingAdapter.pagingController ?? _pagingController;
}
void dispose() {
searchText.close();
_eventPagingAdapter.dispose();
}
}
这会从无限滚动包中填充我们的事件视图 PagedSliverList。
import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:sliver_tools/sliver_tools.dart';
import '../../api/models/events/event.dart';
import '../../api/utils/api_response.dart';
import '../../view_models/events/event_list_view_model.dart';
import '../shared_widgets/connectivity_widget.dart';
import '../shared_widgets/error_widget.dart' as ew;
import '../shared_widgets/no_results.dart';
import '../shared_widgets/rounded_button.dart';
import '../shared_widgets/my_loading_widget.dart';
import '../shared_widgets/my_sliver_refresh_indicator.dart';
import 'event_tile.dart';
class EventListView extends StatelessWidget {
const EventListView({
Key? key,
required this.eventListViewModel,
}) : super(key: key);
final EventListViewModel eventListViewModel;
@override
Widget build(BuildContext context) {
return MySliverRefreshIndicator(
onRefresh: eventListViewModel.refresh,
padding: EdgeInsets.zero,
sliver: MultiSliver(
children: [
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: PagedSliverList.separated(
pagingController: eventListViewModel.getPagingController(),
builderDelegate: PagedChildBuilderDelegate<Event>(
itemBuilder: (context, timesheet, index) => _eventItem(
timesheet: timesheet,
),
firstPageErrorIndicatorBuilder: (context) => _buildErrorWidget(),
noItemsFoundIndicatorBuilder: (context) => _emptyListIndicator(),
newPageErrorIndicatorBuilder: (context) =>
_errorListItemWidget(onTryAgain: eventListViewModel.retryLastRequest),
firstPageProgressIndicatorBuilder: (context) => const Center(
child: MyLoadingWidget(),
),
newPageProgressIndicatorBuilder: (context) => _loadingListItemWidget(),
),
separatorBuilder: (context, index) => const SizedBox(
height: 4,
),
),
),
],
),
);
}
ew.ErrorWidget _buildErrorWidget() {
final error = eventListViewModel.getPagingController().error as ApiResponse;
return ew.ErrorWidget(
showImage: true,
error: error,
onTryAgain: () => eventListViewModel.getPagingController().refresh(),
);
}
Widget _errorListItemWidget({
required VoidCallback onTryAgain,
}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Error getting new page...'),
RoundedButton(
label: 'Retry',
onPressed: onTryAgain,
),
],
);
}
Widget _loadingListItemWidget() {
return const SizedBox(
height: 36,
child: Center(
child: MyLoadingWidget(),
),
);
}
Widget _emptyListIndicator() {
return const NoResults();
}
Widget _eventItem({required Event event}) {
return EventTile(
event: event,
refreshBookings: eventListViewModel.getPagingController().refresh,
);
}
}
大部分代码都被混淆了。
如果有人看到任何明显的逻辑缺陷或其他问题,请告诉我。
推荐阅读
- python - Plotly 静态图像导出获取 OSError:[WinError 193] %1 不是有效的 Win32 应用程序
- excel - 包含变量的复杂公式中的引号
- sql - 如何计算查询时间的累积总和?
- excel - 更改 Power Query 数据源
- unit-testing - 为什么 Angular 8 单元测试中的 viewChild 参考未定义
- java - 如果由 ProcessBuilder() 执行,脚本看不到 ROS
- three.js - 无法加载从 three.js 导出的 Collada
- regex - 数字指令的正则表达式
- scala - 如何仅在 Scala 中跟踪某些类的类字段访问?
- python - tkinter 窗口仅在第一次执行时运行良好