分页列表父类-快速搭建页面

1,214 阅读3分钟

每次复制一份代码

看着重复的代码发呆

是时候拿起重构的武器了

前言

编写了一个PagingListWidget组件,提取了分页列表通用逻辑,其他业务相关的采用虚方法,由子类来实现。

通用逻辑包括:(1)下拉刷新。(2)上拉加载。(3)空数据展示。

其他业务逻辑:(1)导航栏信息。(2)获取列表信息。(3)列表项的视图。

使用方法,直接继承PagingListWidget并实现虚方法。完整代码和样例,请参看分页列表;

一、 起因

作为业务开发工程师,每次新需求来,看到都是列表展示,毫无技术难度。直接ctrl + c 然后ctrl + v,一顿接口修改,开发完成。

一直觉得这已经挺良心了。可突然腻了。

想着是不是可以再懒一点,想多点自由时间来研究怎么能够更懒。

二、 提取

暴力开启,直接拷贝了一个列表页面的代码,然后将业务相关代码删除,仅仅留下通用的逻辑代码。如下:

/// 分页列表的页面
/// navigationTitle返回标题,页面展示一个title,一个列表。
/// navigationTitle返回null。页面仅仅为一个列表。此时可以重载build添加其他widgets。
​
abstract class PagingListWidget extends StatefulWidget {
  const PagingListWidget({Key? key}) : super(key: key);
​
  @override
  PagingListWidgetState createState();
}
​
abstract class PagingListWidgetState<T extends PagingListWidget, S> extends State<T> {
  // 实现类 获取数据时使用
  final int pageSize = 20;
  int page = 1;
  final List<S> dataList = [];
  bool showLoadingMore = false;
  int total = 0;
  bool hasInitialed = false;
​
  bool _disposed = false;
  final ScrollController _scrollController = ScrollController();
  final GlobalKey<RefreshIndicatorState> _refreshKey = GlobalKey();
​
  @override
  void initState() {
    super.initState();
​
    _scrollController.addListener(() {
      if (showLoadingMore) {
        return;
      }
      if (_scrollController.position.pixels > (_scrollController.position.maxScrollExtent - 20) && total > dataList.length) {
        setState(() {
          showLoadingMore = true;
        });
        _loadMore();
      }
    });
​
    Future.delayed(const Duration(seconds: 0), () {
      _onRefresh();
    });
  }
​
  @override
  void dispose() {
    _disposed = true;
    _scrollController.dispose();

​
    super.dispose();
  }
​
  @override
  Widget build(BuildContext context) {
    var title = navigationTitle();
    if (null == title) {
      return _contentWidget();
    }
​
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: _contentWidget(),
    );
  }
​
// Widget __START__
​
  Widget _contentWidget() {
    return SafeArea(
      child: !hasInitialed
          ? const MyCircularProgress()
          : RefreshIndicator(
              key: _refreshKey,
              onRefresh: _onRefresh,
              child: dataList.isEmpty
                  ? const NoData()
                  : ListView.builder(
                      itemBuilder: (context, index) {
                        if (index == dataList.length) {
                          return showLoadingMore
                              ? Container(
                                  margin: const EdgeInsets.all(10),
                                  child: const Align(
                                    child: CircularProgressIndicator(
                                      color: MyColors.mainColor,
                                    ),
                                  ),
                                )
                              : total <= dataList.length
                                  ? Container(
                                      margin: const EdgeInsets.all(10),
                                      alignment: Alignment.center,
                                      child: const Text("没有更多数据"),
                                    )
                                  : const SizedBox(
                                      height: 44,
                                    );
                        }
                        return listItem(index);
                      },
                      physics: const AlwaysScrollableScrollPhysics(),
                      itemCount: dataList.length + 1,
                      controller: _scrollController,
                    ),
            ),
    );
  }
​
// Widget __END__// Network __START__
  Future<void> _onRefresh() async {
    dataList.clear();
​
    page = 1;
​
    if (_disposed) {
      return;
    }
​
    fetchData();
  }
​
  Future<void> _loadMore() async {
    page++;
​
    if (_disposed) {
      return;
    }
​
    fetchData();
  }
​
// Network __END__
​
  // 虚方法 __START__
​
  // 标题
  // 返回为null 则说明此页面不要包含导航栏,仅仅返回列表页面。
  // 返回标题时,那么直接展示完整的页面
  String? navigationTitle();
​
  // 获取列表的数据。
  Future<void> fetchData();
​
  // 列表视图
  Widget listItem(int index);
​
// 虚方法 __END__
}

三、 思路

1. 为了使用尽量方便,直接继承StatefulWidgetConsumerStatefulWidget
2. 将所有的值都放在父类,子类通过super来访问。
3. 列表信息的data model,采用泛型传入。abstract class PagingListWidgetState<T extends PagingListWidget, S>避免类型强转。
4. 提高复用性。根据navigationTitle来判断是否是带导航栏。
5. 提供自定义的部分,采用虚方法,让子类来实现。

四、样例

将原来的页面采用PagingListWidget重写下,代码量减少了一半。并且以后编写分页列表的新页面,开发时间减少90%吧。爽了。

举一个例子,如下:

class MyList extends PagingListWidget {
  const MyList({super.key});
​
  @override
  PagingListWidgetState<PagingListWidget, dynamic> createState() => _MyListState();
}
​
class _MyListState extends PagingListWidgetState<MyList, String> {
  var random = Random();
​
  @override
  Future<void> fetchData() async {
    return Future.delayed(const Duration(seconds: 3), () {
      super.total = 100000;
      super.dataList.addAll(List.generate(super.pageSize, (index) => "测试下 ${random.nextInt(10000000)}"));
​
      setState(() {
        super.showLoadingMore = false;
        super.hasInitialed = true;
      });
    });
  }
​
  @override
  Widget listItem(int index) {
    return Container(
      color: MyColors.randomColor(),
      height: 44,
      padding: const EdgeInsets.only(top: 8, left: 8),
      child: Text(super.dataList[index], style: TextStyle(color: MyColors.randomColor(), fontSize: 16),),
    );
  }
​
  @override
  String? navigationTitle() {
    return "测试分页列表";
  }
}
1. 创建子类。
2. 实现虚方法。
3. 完成

五、结尾

思路突然打开了。重复的代码以后就用这个方式重构下,代码量和开发量下降好多。

抛砖引玉。请赐教更好的思路。