В прошлой статье я поделился своим опытом работы со списками. В ней было рассмотрено, как осуществлять загрузку, фильтрацию, актуализацию данных и другие действия над списком. Как можно увидеть из статьи, внедрение таких функций требует от разработчика написания довольно большого количества кода и реализации не всегда очевидных сценариев.


В этой статье я расскажу, как упростить создание контроллеров для управления списками при помощи моей библиотеки. В ней я постарался объединить свои идеи и наработки по реализации контроллера списка. В качестве примера, мы создадим простое приложение со списком, данные в который загружаются по мере его прокрутки.


Основные функции библиотеки:


  • получение, сортировка и фильтрация данных списка;
  • актуализация данных списка без необходимости повторной его загрузки;
  • бесконечная прокрутка c поддержкой любой стратегии пагинации;
  • поддержка нескольких направлений загрузки записей списка (например, в случае необходимости создания списков с бесконечной прокруткой вперёд и назад);
  • прерывание загрузки данных (например, в случае изменения критериев фильтра);
  • повторение загрузки данных (например, в случае ошибки во время предыдущей загрузки);
  • отображение промежуточных результатов загрузки данных списка;
  • отслеживание изменений записей списка для выполнения произвольных действий.

Немаловажным моментом является то, что библиотека архитектуронезависима: может использоваться с любыми виджетами и совместима с любым подходом управления состоянием (будь то ValueNotifier, Bloc или любой другой подход).


Работать с библиотекой очень просто. Сначала необходимо создать заготовку класса, который будет служить контроллером списка. Затем подключить к этому классу предоставляемые библиотекой mixin-ы, которые отвечают за необходимую вам функциональность. И, наконец, реализовать в классе функции, необходимые для работы используемых mixin-ов.


Сейчас мы рассмотрим реализацию списка с бесконечной прокруткой на базе StatefulWidget.


Готовое приложение


Создадим новое Flutter-приложение.


> flutter create list_controller_example

Добавим к проекту библиотеки list_controller и equatable.


> cd list_controller_example
> flutter pub add list_controller equatable

Далее будем работать только с файлом main.dart. Заменим всё его содержимое на следующее:


import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:list_controller/list_controller.dart';

void main() {
  runApp(const MaterialApp(
    title: 'List Controller Example',
    home: MyHomePage(),
  ));
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key}) : super();

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('List Controller Example'),
      ),
      body: Container()
    );
  }
}

Для работы контроллера необходимо реализовать некоторые структуры данных.


Создадим класс для описания запроса на получение данных из источника.


class ListQuery extends Equatable {
  const ListQuery({this.weightGt = 0});

  final int weightGt;

  @override
  List<Object?> get props => [weightGt];
}

В этом классе параметр weightGt нужен для указания того, что из источника данных нужно загрузить записи, с весом большим, чем сохранённый в этом параметре.


Далее необходимо создать класс для хранения состояния списка. Библиотека не накладывает на этот класс никаких ограничений, однако, предоставляет класс, который подходит для большинства сценариев. Его мы и будем использовать в нашем приложении.


Сконфигурируем этот класс следующим образом.


typedef ListStateExample = ListState<int, ListQuery>;

Здесь int — это тип данных, которым представлены записи списка.


Теперь определим класс, которым будет представлен результат, загруженный из источника данных.


class LoadResult {
  const LoadResult({required this.records, required this.isFinalPage});

  final List<int> records;
  final bool isFinalPage;
}

В нём records — это список загруженных записей. При этом isFinalPage сообщает, достигнуто ли окончание списка.


Для реализации необходимого нам функционала потребуются следующие mixin-ы: ListCore, RecordsLoader, KeysetPagination. Добавим их к классу _MyHomePageState.


class _MyHomePageState extends State<MyHomePage>
    with
        ListCore<int>,
        RecordsLoader<int, ListQuery, LoadResult>,
        KeysetPagination<int, ListQuery, LoadResult> {
    ...

Далее в этот класс добавим:


  1. переменную для хранения состояния списка;
  2. код для загрузки первой страницы данных;
  3. вызов функции закрытия контроллера.

  ListStateExample state = ListStateExample(query: const ListQuery()); // 1

  @override
  void initState() {
    loadRecords(state.query); // 2
    super.initState();
  }

  @override
  void dispose() {
    closeList(); // 3
    super.dispose();
  }

Теперь мы можем реализовать функции, которые нужны для работы подключенных mixin-ов.


Начнём с mixin-а RecordsLoader. Добавим код:


  @override
  void onRecordsLoadStart({required ListQuery query, required LoadingKey loadingKey}) => setState(() {
        state = state.copyWith(stage: ListStage.loading());
      });

  @override
  Future<LoadResult> performLoadQuery({required ListQuery query, required LoadingKey loadingKey}) async {
    await Future.delayed(const Duration(milliseconds: 1500));
    return LoadResult(
      records: List<int>.generate(20, (i) => query.weightGt + i + 1),
      isFinalPage: query.weightGt >= 80,
    );
  }

  @override
  void putLoadResultToState({required ListQuery query, required LoadResult loadResult, required LoadingKey loadingKey}) => setState(() {
        state = state.copyWith(
          records: [
            ...state.records,
            ...loadResult.records,
          ],
          stage: loadResult.isFinalPage ? ListStage.complete() : ListStage.idle(),
        );
      });

Функция onRecordsLoadStart будет вызываться перед началом загрузки данных. Обычно в ней необходимо поменять состояние списка таким образом, чтобы отобразить происходящую загрузку.


Как видно, во многих функциях присутствует параметр loadingKey. В нашем случае он не используется, однако при необходимости он служит для идентификации и синхронизации различных загрузок при реализации более сложных сценариев.


В функции performLoadQuery осуществляется загрузка данных. Для упрощения примера, в нашем случае просто генерируется список с последовательностью чисел.


В функции putLoadResultToState происходит добавление загруженных записей к состоянию списка и сброс переменной, отвечающей за отображение индикатора загрузки.


Теперь реализуем функции, необходимые для mixin-а KeysetPagination. Добавим код:


  @override
  ListStage getListStage(LoadingKey loadingKey) => state.stage;

  @override
  ListQuery buildNextPageQuery(LoadingKey loadingKey) => ListQuery(weightGt: state.records.last);

Функция getListStage возвращает состояние, в котором находится список.


Функция buildNextPageQuery формирует запрос для получение следующей страницы данных. Позже этот запрос будет передан в функцию performLoadQuery.


На этом этапе мы закончили написание кода контроллера списка. Переходим к отображению этого списка.


Заменим всю функцию build на следующую:


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('List Controller Example'),
      ),
      body: Stack(
        children: [
          ListView.builder(
            itemCount: state.records.length,
            itemBuilder: (context, index) {
              const recordsLoadThreshold = 1;
              if (index >= state.records.length - recordsLoadThreshold) {
                WidgetsBinding.instance.addPostFrameCallback((_) {
                  loadNextPage();
                });
              }
              return ListTile(title: Text(state.records[index].toString()));
            },
          ),
          if (state.isLoading) const Center(child: CircularProgressIndicator()),
        ],
      ),
    );
  }

Если список находится в состоянии загрузки, то поверх списка отображается индикатор загрузки. Когда загрузка данных завершена, отображаются элементы списка. Загрузка следующей страницы списка происходит вызовом loadNextPage после того, как пользователь проскролил список до позиции recordsLoadThreshold с конца.


Заключение


В статье мы рассмотрели создание списка с бесконечной прокруткой. Приведённый пример демонстрирует только базовые возможности библиотеки. О таких функциях, как прерывание загрузки, повтор неудачной загрузки, актуализация данных списка, можно узнать на странице.


Код приложения из статьи доступен по ссылке.

Комментарии (0)