Android Remote Mediator Class 的 Flutter 等价物

Flutter equivalent for Android Remote Mediator Class

我是本地 Android 开发人员,正在考虑迁移到 Flutter,我已经对 Flutter 中的核心库替代方案进行了所有研究。我特别在意的一件事是,当处理大量数据列表时。在 Android 中,我们可以使用 Paging 3 Android 库中可用的 ROOM library. What's even better is that with the RemoteMediator class 将此数据保存在 SQLite 本地数据库中,我们可以在查询数据时创建无限滚动回收器来自本地数据库,而网络调用查询新数据并将其存储在数据库中。所以回收器从数据库而不是网络调用中查询数据。所以这些数据可以在没有互联网访问的情况下访问。

我知道 sqflite 包是 Flutter 在 Android 中 ROOM 的替代品,但是我们可以使用这个数据库来查询分页的项目列表以显示在 ListViewBuilder 中并且用户滚动吗?

我一直在做这个,一开始想不通。但是现在有一个可行的解决方案。 我使用数据库优先方法,因此我的数据库是我的真实来源,填充它的信息来自我们的 api 门户。

我用的是infinite scrolling package for my pagination and every time it fetches a new page I sync my database hive的结果,我再return一个数据库的子列表。

同步是一个更新插入,因此它更新已经存在的记录并添加不存在的记录。

无限滚动包允许我们实现一些东西,第一个是页面请求侦听器,它告诉我们我们在哪个页面上,我们需要在我们的 fetchPage 方法中增加它。

这是一篇很棒的文章here

第二个是分页控制器,它允许我们追加一页或追加最后一页,所以我们需要弄清楚最后一页是什么时候。

另一个小问题是管理数据库中的内容与门户中的内容之间的小差异。例如,如果我的数据库从门户中尚不存在的 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,
        );
      }
    }

此代码的大部分已被混淆。

如果有人发现任何明显的逻辑缺陷或其他问题,请告诉我。