I’m participating in nuggets Creators Camp # 4, click here to learn more and learn together!

In App, list data loading is a very common function, and list data display exists in almost most apps. However, for list data display with a large amount of data, in order to improve user experience and reduce server pressure, paginated list data loading is generally adopted, and only one page of data is loaded for the first time. The next page of data is triggered when the user slides down to the bottom of the list.

In order to realize the function of list paging quickly in the development process, the unified encapsulation of list paging loading is essential, so that in the development process, we only need to pay attention to the actual business logic without spending too much time on the processing of paging data loading, so as to save the development workload and improve the development efficiency.

0 x00 effect

Let’s take a look at the effect of paginated loading of a wrapped list:

Example code for use after encapsulation:

The State:

class ArticleListsState  extends PagingState<Article>{}Copy the code

Controller:

class ArticleListsController extends PagingController<Article.ArticleListsState> {
  final ArticleListsState state = ArticleListsState();
  /// For interface requests
  final ApiService apiService = Get.find();
  

  @override
  ArticleListsState getState() => ArticleListsState();

  @overrideFuture<PagingData<Article>? > loadData(PagingParams pagingParams)async{
    /// Request interface data
    PagingData<Article>? articleList = await apiService.getArticleList(pagingParams);
    returnarticleList; }}Copy the code

View:

class ArticleListsPage extends StatelessWidget {
  final controller = Get.find<ArticleListsController>();
  final state = Get.find<ArticleListsController>().state;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Article List")),
      body: buildRefreshListWidget<Article,ArticleListsController>(itemBuilder: (item, index){
        return_buildItem(item); })); }/// The item layout
  Widget _buildItem(Article item) {
    return Card(...);
  }
}
Copy the code

0 x01 implementation

Above shows by encapsulating the list after paging loading list implementation effect and attach a key example code, through the example code, you can see that when using list page loading function after packaging need to focus only on the interface layout and display data requests itself, without having to pay attention to the details of the paging, make a list of the realization of the paging loading easier. The following code shows how to implement the encapsulation of paginated list loading.

Overall introduction

Before looking at the concrete implementation, first take you from the overall structure, the final implementation of the function, the use of the tripartite library to do an overall introduction.

The overall structure

The entire list encapsulation is divided into three layers: State, Controller, and View.

  • State: used to store interface State data. A complex interface may have a lot of State data. In order to facilitate the maintenance of State data, the State data will be uniformly placed in State.
  • Controller: handles the page service logic.
  • View: UI elements, i.e. widgets.

Realize the function

The main functions of the encapsulated list paging load are as follows:

  • Tabular data display
  • The drop-down refresh
  • Pull on loading
  • Automatically determine if there is more data
  • Handles paging logic automatically
  • List item Click event encapsulation

Third party libraries used

  • Pull_to_refresh: pull down to refresh and load more
  • GetX: dependency management and status management

GetX uses dependency management and state management as well as dependency management. This article mainly introduces the dependency management of GetX. For more information about GetX, see the following article:

  • GetX integration and Usage of Flutter application framework

  • Flutter dissects the implementation of Getx dependency management step by step through the source code

  • Use of Flutter GetX dependency injection in detail

  • The use of Flutter GetX dependency injection tag is explained

  • Detailed explanation of Flutter GetX dependency injection Bindings usage

  • The GetBuilder for Flutter state management uses detailed explanation and source code analysis

The specific implementation

As described earlier, there are three levels of encapsulation for paginated list loading: State, Controller, View, and the main work of encapsulation is the encapsulation of these three layers, the implementation of PagingState, PagingController base class and buildRefreshListWidget function encapsulation.

PagingState

PagingState is used to encapsulate and save PagingState data and list data, does not involve actual business logic processing, source code is as follows:

class PagingState<T>{
  
  /// Number of paging pages
  int pageIndex = 1;
  
  ///If there's more data
  bool hasMore = true;

  /// Id used to refresh the list
  Object refreshId = Object(a);/// The list of data
  List<T> data = <T>[];
}
Copy the code

PagingState has a generic T that is the item type of list data, that is, the data entity type of list data item. RefreshId Specifies the ID of the refresh list interface, which is used by the Controller to refresh specified widgets later. RefreshId is a function of GetX state management. The effects of other variables are described in more detail in the comments and will not be described here.

PagingController

PagingController encapsulates the logical processing of paging, source code is as follows:

abstract class PagingController<M.S extends PagingState<M>> extends GetxController{

  /// PagingState
  late S pagingState;
  /// Refreshes the Controller of the control
  RefreshController refreshController =  RefreshController();

  @override
  void onInit() {
    super.onInit();
    /// Save the State
    pagingState = getState();
  }

  @override
  void onReady() {
    super.onReady();
    /// The page is displayed to refresh data
    refreshData();
  }


  /// The refresh data
  void refreshData() async{
    initPaging();
    await _loadData();
    /// Refresh to complete
    refreshController.refreshCompleted();
  }

  ///Initialize paging data
  void initPaging() {
    pagingState.pageIndex = 1;
    pagingState.hasMore = true;
    pagingState.data.clear();
  }

  /// The data load
  Future<List<M>? > _loadData()async {
    PagingParams pagingParams = PagingParams.create(pageIndex: pagingState.pageIndex);
    PagingData<M>? pagingData = await loadData(pagingParams);
    List<M>? list = pagingData? .data;/// If the data is not empty, add the data to data
    /// And the number of pages pageIndex + 1
    if(list ! =null && list.isNotEmpty) {
      pagingState.data.addAll(list);
      pagingState.pageIndex += 1;
    }

    /// Determine if there is more datapagingState.hasMore = pagingState.data.length < (pagingData? .total ??0);

    /// Update the interface
    update([pagingState.refreshId]);
    return list;
  }


  /// To load more
  void loadMoreData() async{
    await _loadData();
    /// loaded
    refreshController.loadComplete();
  }

  /// The final method for loading dataFuture<PagingData<M>? > loadData(PagingParams pagingParams);/// Access to the State
  S getState();

}
Copy the code

PagingController inherits from GetxController and has two generic types M and S, which are the data entity type of the list item and the type of PageState respectively.

The member variable pagingState is of type generic S or pagingState, which is obtained in onInit through the abstract method getState, which is implemented in a subclass and returns an object of type pagingState.

RefreshController is the SmartRefresher Controller of the pull_to_refresh library, which controls the refresh/load completion.

The “refreshData” and “loadMoreData” methods are called in the corresponding event. The internal implementation of the “loadData” method calls _loadData to load the data. After the loading is complete, the “refreshController” method is called. The initPaging method, which initializes paging data, is also called before loading data in refreshData to reset paging parameters and data.

PagingParams = PagingParams; PagingParams = PagingParams; PagingParams = PagingParams; PagingParams = PagingParams

class PagingParams {

	int current = 1;
  Map<String.dynamic>? extra = {};
	Map<String.dynamic> model = {};
	String? order = 'descending';
	int size = 10;
	String? sort = "id";

  factory PagingParams.create({required int pageIndex}){
    var params = PagingParams();
    params.current = pageIndex;
    returnparams; }}Copy the code

Fields include the current page number, number of data items per page, sorting field, sorting mode, and extended service parameters. This class can be created from the background interface paging request protocol document.

PagingData (PagingData); PagingData (PagingData); PagingData (PagingData)

class PagingData<T> {

	int? current;
	int? pages;
	List<T>? data;
	int? size;
	int? total;
 
  PagingData();

  factory PagingData.fromJson(Map<String.dynamic> json) => $PagingDataFromJson<T>(json);

  Map<String.dynamic> toJson() => $PagingDataToJson(this);

  @override
  String toString() {
    return jsonEncode(this); }}Copy the code

This entity contains the real data data of the list, as well as paging-related parameters, such as the current page, total pages, total entries, etc., which can be adjusted according to the actual data returned by the background paging interface. FromJson, toJson is used for JSON data parsing and conversion.

Json data parsing can be described in the following section: Build a FRAMEWORK for the Flutter application

After the data is loaded, determine whether the data is empty. If it is not, add the data to the data collection and increment the number of paging pages by one. Then determine whether there is more data, which is judged by comparing the number of data items in data with the total number of pages returned. Different teams may implement different rules of paging interfaces, which can be adjusted according to the actual situation, such as using the number of pages for judgment.

Method finally calls the Controller’s update method to refresh the interface data.

The process is as follows:

View

The View layer encapsulates SmartRefresher of ListView and pull_to_REFRESH for list data presentation and pull-down refresh/pull-up loading. Its encapsulation is mainly the encapsulation of Widget parameter configuration, and there is not much business logic code involved. Therefore, it is not encapsulated as a Widget control, but encapsulated as a method to call. There are three methods in total:

  • BuildListView: ListView control encapsulation
  • BuildRefreshWidget: Pull down to refresh/pull up to load more control packages
  • BuildRefreshListWidget: Encapsulation of the ListView control with paginated loading

The first two are separate packages of ListView and SmartRefresher, and the third is a combination of the two.

BuildListView:

Widget buildListView<T>(
    {required Widget Function(T item, int index) itemBuilder, 
    required List<T> data,
    Widget Function(T item, int index)? separatorBuilder,
    Function(T item, int index)? onItemClick,
    ScrollPhysics? physics,
    bool shrinkWrap = false,
    Axis scrollDirection = Axis.vertical}) {
  returnListView.separated( shrinkWrap: shrinkWrap, physics: physics, padding: EdgeInsets.zero, scrollDirection: scrollDirection, itemBuilder: (ctx, index) => GestureDetector( child: itemBuilder.call(data[index], index), onTap: () => onItemClick? .call(data[index], index), ), separatorBuilder: (ctx, index) => separatorBuilder? .call(data[index], index) ?? Container(), itemCount: data.length); }Copy the code

There’s not much code, but it wraps around the usual parameters of the ListView and adds a generic T, which is the type of the list item. Second, itemCount and itemBuilder are processed specially. ItemCount is assigned to data.length, the length of the list data. The ListView itemBuilder calls the passed itemBuilder method, which takes the item data and the index index. And onItemClick is called using the GestureDetector package wrapped around the Item click event.

BuildRefreshWidget:

Widget buildRefreshWidget({
  required Widget Function() builder,
  VoidCallback? onRefresh,
  VoidCallback? onLoad,
  required RefreshController refreshController,
  bool enablePullUp = true.bool enablePullDown = true{})return SmartRefresher(
    enablePullUp: enablePullUp,
    enablePullDown: enablePullDown,
    controller: refreshController,
    onRefresh: onRefresh,
    onLoading: onLoad,
    header: const ClassicHeader(idleText: "Drop-down refresh",
      releaseText: "Release refresh",
      completeText: "Refresh complete",
      refreshingText: "Loading......",),
    footer: const ClassicFooter(idleText: "Pull up to load more",
      canLoadingText: "Loosen up and load more",
      loadingText: "Loading......",),
    child: builder(),
  );
}
Copy the code

Encapsulate SmartRefresher parameters, add header and footer unified processing, which can be encapsulated according to the actual needs of the project, you can use other pull-down refresh/pull-up load style or custom implementation effect, For details on how to use SmartRefresher, see flutter_pullToRefresh.

BuildRefreshListWidget:

Widget buildRefreshListWidget<T, C extends PagingController<T, PagingState<T>>>(
    {
      required Widget Function(T item, int index) itemBuilder,
      bool enablePullUp = true.bool enablePullDown = true.String? tag,
      Widget Function(T item, int index)? separatorBuilder,
      Function(T item, int index)? onItemClick,
      ScrollPhysics? physics,
      bool shrinkWrap = false,
      Axis scrollDirection = Axis.vertical
    }) {
  C controller = Get.find(tag: tag);
  return GetBuilder<C>(builder: (controller) {
    return buildRefreshWidget(
        builder: () =>
            buildListView<T>(
                data: controller.pagingState.data,
                separatorBuilder: separatorBuilder,
                itemBuilder: itemBuilder,
                onItemClick: onItemClick,
                physics: physics,
                shrinkWrap: shrinkWrap,
                scrollDirection: scrollDirection
            ),
        refreshController: controller.refreshController,
        onRefresh: controller.refreshData,
        onLoad: controller.loadMoreData,
        enablePullDown: enablePullDown,
        enablePullUp: enablePullUp && controller.pagingState.hasMore,
    );
  }, tag: tag, id: controller.pagingState.refreshId,);
}
Copy the code

BuildRefreshListWidget is a re-encapsulation of the previous two, and the parameter is basically a combination of the two. The buildRefreshWidget builder is passed in buildListView.

In order to encapsulate more operations of pull-down refresh and pull-up load, we introduce the generic type C of PagingController and get the current PagingController instance Controller through GetX dependency management:

  • buildListViewThe data passed in to the PagingState is paging data, i.econtroller.pagingState.data
  • RefreshController is passed to the refreshController object created in PagingController, i.econtroller.refreshController
  • OnRefresh/onRefresh calls the PagingControllerrefreshData / loadMoreDatamethods
  • EnablePullUp Uses the enablePullUp passed in by the method and the hasMore of PagingState to determine whether there is more data

After the list data is loaded, the buildRefreshWidget will be refreshed automatically. In this case, GetBuilder is used to wrap buildRefreshWidget with tag and ID parameters, where tag is the GetX dependency injection tag, which is used to distinguish the injected instance, and ID is the refreshed ID. The specified control can be refreshed by ID, and in this case the refreshId defined in PagingState is passed to refresh the specified list.

The overall View structure is as follows:

0 x02 summary

After the encapsulation of appeal, it can quickly realize the effect of list paging loading shown at the beginning of the article. Through simple code, it can realize the complete list paging loading function, so that developers focus on the business itself, so as to save development workload, improve development efficiency and quality. Finally, an overall structure diagram is attached: