Flutter高性能列表滚动插件jk_fast_listview的使用

发布于 1周前 作者 htzhanglong 来自 Flutter

Flutter高性能列表滚动插件jk_fast_listview的使用

JkFastListView

ListView / GridView 适用于 Flutter。支持在大量数据之间快速滚动,灵活的组件大小,基于索引的滚动,项目回收。

特性

  • 大部分 API 与官方 Flutter 的 ListViewGridView 相同。
  • 支持在大量数据之间快速滚动。
  • 灵活的组件大小。
  • 按指定索引滚动。
  • 监听当前可见的第一个项目的索引。
  • 项目回收。

谁需要这个包?

  • 如果所有项目具有相同的宽度和高度,请使用官方 Flutter 的 ListViewGridView。这两个类在固定大小项目的情况下工作良好,并且也支持在大量数据之间快速滚动。
  • 如果项目的宽度和高度是可变的,并且你需要快速跳转到远距离的项目(通过调用 jumpToIndexanimateToIndex,或通过用户拖动滚动条),请使用此包。这在某些应用程序中非常有用(例如聊天应用)。

快速开始

安装

在你的包的 pubspec.yaml 文件中添加以下依赖:

dependencies:
  jk_fast_listview: ^0.8.0

使用

构建一个 ListView

注意:大多数参数的名称与官方 Flutter 的 ListViewGridView 相同。我们不再解释每个参数。

var listview = JkFastListView(
  itemCount: 999999,
  itemBuilder: (context, index) => Text("Item $index"),

  // 可选,不能用于网格类型
  separatorBuilder: (context, index) => const Divider(), // 可选
);

构建一个 GridView

var listview = JkFastListView(
  itemCount: 999999,
  itemBuilder: (context, index) => Text("Item $index"),

  crossAxisCount: 3, // 添加此行以使每行有 3 个项目

  // 另一种指定每行项目数的方法,不能与 'crossAxisCount' 同时使用
  // maxCrossAxisExtent: 300,

  mainAxisSpacing: 20,  // 可选
  crossAxisSpacing: 20, // 可选
);

其他常见可选参数

这些参数的定义可以在 ListView 中找到。

var listview = JkFastListView(
  ...
  cacheExtent: 300,                 // 可选
  scrollDirection: Axis.horizontal, // 可选
  reverse: true,                    // 可选
);

设置初始项目索引

final itemController = JkItemController();

// [可选] 设置从一开始应该可见的项目
// initialIndex: 应该从一开始可见的项目的索引
// alignment: null 或 0~1 (double)
//   null: 不指定项目的放置位置
//   0: 将项目放在列表视图的开始位置
//   1: 将项目放在列表视图的结束位置
//   0.5: 将项目放在列表视图的中心
itemController.setInitialIndex(initialIndex: 1000, alignment: 0.5);

var listview = JkFastListView(
  ...
  itemController: itemController,
);

滚动到/动画到指定索引的项目

// 跳转到索引为 1000 的项目
itemController.jumpToIndex(1000, alignment: 1);

// 类似于 'jumpToIndex',但带有动画
itemController.animateToIndex(1000,
  alignment: 1,
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeIn);

监听当前第一个可见项目的索引

var listview = JkFastListView(
  ...
  onScrollPosition: (index, ratio) {
    log("第一个可见项目索引是 $index");
    // ratio: 0~1 (double),
    //   0: 不可见
    //   1: 完全可见
    //   0.5: 只有项目下半部分可见
  },
);

获取当前第一个可见项目的索引

int topIndex = itemController.getFirstVisibleIndex();

关于回收器的注意事项

这个包使用回收器来重用项目而不是删除它们,当项目滚动出可见区域时,恢复项目而不是重新创建它们。

对于某些情况(例如您的项目具有动画效果),您应该覆盖 didUpdateWidget 方法在 StatefulWidget 的状态中,并在此处执行一些清理资源或状态的操作(例如停止动画)。

每当项目从回收器恢复时,都会调用 didUpdateWidget 方法,并传入新的小部件配置。

性能调优

尽可能不要分配键。

  • 无键的小部件将使用回收器来提高性能。这种小部件会在滚动出屏幕时被回收,并在另一个新项目(可能具有不同的索引)滚动进入时被重用。
  • 带有键的小部件不会被回收。这种小部件会在滚动进入和离开可见区域时始终被创建和销毁。
  • 只给需要重新排序/移除可见区域内项目的项分配键,并且您的小部件具有您希望在项目重新排序/移除后保留的状态。
  • 我认为最好记住项目状态而不是在项目小部件内部。

完整示例

以下是完整的示例代码:

import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:jk_fast_listview/jk_fast_listview.dart';

import 'input_dialog.dart';

void main() {
  //debugRepaintRainbowEnabled = true;
  runApp(const MaterialApp(home: MyApp()));
}

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

  [@override](/user/override)
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final List<MyItemWidget> widgetList;

  static const int initialItemCount = 100000;
  int lastIndex = initialItemCount;
  MyItemWidget createItem(int index) {
    Key? key;
    if (index % 3 == 0) key = ValueKey(index);
    return MyItemWidget(
      index,
      key: key,
      onSwapClicked: (index) {
        int pos = widgetList.indexWhere((element) => element.index == index);
        var child1 = widgetList.removeAt(pos + 3);
        var child2 = widgetList.removeAt(pos);
        widgetList.insert(pos, child1);
        widgetList.insert(pos + 3, child2);
        setState(() {});
      },
      onAddClicked: (index) {
        int pos = widgetList.indexWhere((element) => element.index == index);
        widgetList.insert(pos + 1, createItem(lastIndex++));
        setState(() {});
      },
      onRemoveClicked: (index) {
        int pos = widgetList.indexWhere((element) => element.index == index);
        widgetList.removeAt(pos);
        setState(() {});
      },
    );
  }

  [@override](/user/override)
  void initState() {
    super.initState();
    widgetList = List.generate(lastIndex++, createItem);
  }

  int? findIndexByKey(Key key) {
    int pos = widgetList.indexWhere((element) => element.key == key);
    if (pos < 0) return null;
    log("findIndexByKey() : $pos");
    return pos;
  }

  final scrollController = ScrollController();
  //final itemController = JkItemController();
  final itemController = JkItemController(initialIndex: 1000, alignment: 0.5);
  final itemCountValue = ValueNotifier<int>(initialItemCount);

  Widget buildListView(BuildContext context) {
    Widget list = ValueListenableBuilder<int>(
        valueListenable: itemCountValue,
        builder: (context, value, child) {
          log("ListView rebuild now");

          return JkFastListView(
            //return JkFastSliverList(
            itemController: itemController,
            onScrollPosition: (index, ratio) {
              //log("onScroll: item $index, visible: $ratio, ${itemController.getFirstVisibleIndex()}");
            },

            //return ListView.separated(
            //findChildIndexCallback: findIndexByKey,
            //return ListView.builder(
            /*
          //return GridView.builder(

            gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 300),
            */
            /*
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 1,
                mainAxisSpacing: 20,
                crossAxisSpacing: 20,
              ),
              */

            //findChildIndexCallback: findIndexByKey,

            mainAxisSpacing: 20,
            crossAxisSpacing: 20,
            //crossAxisCount: 2,
            maxCrossAxisExtent: 300,
            //separatorBuilder: (context, index) => MySeparator(index),

            controller: scrollController,
            cacheExtent: 300,
            reverse: true,
            scrollDirection: Axis.horizontal,
            itemCount: value,
            //itemCount: 300000,
            itemBuilder: (context, index) {
              return widgetList[index];
              //if (true) return Text("test now $index");
              //log("widget created: $index");
            },
          );
        });

    return list;
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    Widget list = buildListView(context);
    list = Scrollbar(
      controller: scrollController,
      child: list,
    );

    Widget controls = Row(children: [
      TextButton(
          onPressed: () => setState(() {}),
          child: const Text("rebuild")),
      TextButton(
          onPressed: () async {
            int? num = await showIntInputDialog(context, "set itemCount");
            if (num != null) {
              itemCountValue.value = num;
            }
          },
          child: const Text("itemCount")),
      TextButton(
          onPressed: () async {
            int? num = await showIntInputDialog(context, "jumpToIndex");
            if (num != null) {
              //scrollController.animateTo(num.toDouble(), duration: const Duration(milliseconds: 2000), curve: Curves.easeIn);
              //itemController.jumpToIndex(num, alignment: 0.5);

              itemController.animateToIndex(num,
                  alignment: 1,
                  duration: const Duration(milliseconds: 2000),
                  curve: Curves.easeIn);
            }
          },
          child: const Text("jumpToIndex")),
    ]);

    Widget body = Column(children: [controls, Expanded(child: list)]);

    return Scaffold(
      appBar: AppBar(
        title: const Text('插件示例应用'),
      ),
      body: body,
    );
  }
}

typedef ParamCallback<T> = void Function(T param);

class MyItemWidget extends StatefulWidget {
  final int index;
  final ParamCallback<int>? onAddClicked;
  final ParamCallback<int>? onRemoveClicked;
  final ParamCallback<int>? onSwapClicked;

  const MyItemWidget(
    this.index, {
    super.key,
    this.onAddClicked,
    this.onRemoveClicked,
    this.onSwapClicked,
  });

  [@override](/user/override)
  State<StatefulWidget> createState() => MyItemWidgetState();
}

class MyItemWidgetState extends State<MyItemWidget>
    with SingleTickerProviderStateMixin {
  int tickCount = 0;

  [@override](/user/override)
  void initState() {
    super.initState();
    initAnimation();
  }

  [@override](/user/override)
  void deactivate() {
    super.deactivate();
    //log("deactivate item widget ${widget.index}");
  }

  [@override](/user/override)
  void activate() {
    super.activate();
    //log("activate item widget ${widget.index}");
  }

  [@override](/user/override)
  void dispose() {
    controller.dispose();
    super.dispose();
    //log("dispose item widget ${widget.index}");
  }

  late Animation<double> animation;
  late AnimationController controller;
  double widgetSizeDiff = 0;
  void initAnimation() {
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
  }

  bool stopAnimation = false;
  [@override](/user/override)
  [@protected](/user/protected)
  void didUpdateWidget(covariant MyItemWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    //log("didUpdateWidget");
    stopAnimation = true;
    controller.reset();
  }

  void startAnimation() async {
    animation = Tween<double>(begin: 200, end: 500).animate(controller);
    animation.addListener(() {
      widgetSizeDiff = animation.value;
      setState(() {});
    });
    //controller.forward();
    stopAnimation = false;
    await controller.forward();
    if (!stopAnimation) {
      await controller.reverse();
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    //log("build widget ${widget.index}");
    //if (true) return Text("Item ${widget.index}");
    Widget child = Row(children: [
      tickCount == 0
          ? Text("Item ${widget.index}")
          : Text("Item ${widget.index} ($tickCount)"),
      //Text(" CC"),

      Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextButton(
            onPressed: () {
              log("button clicked: ${widget.index}");
            },
            child: const Text("log"),
          ),
          TextButton(
            onPressed: () {
              tickCount++;
              setState(() {});
            },
            child: const Text("tick"),
          ),
          IconButton(
            onPressed: () {
              log("animation");
              startAnimation();
            },
            icon: const Icon(Icons.animation),
          ),
          TextButton(
            onPressed: () => widget.onSwapClicked!(widget.index),
            child: const Text("swap+3"),
          ),
          IconButton(
            onPressed: () => widget.onAddClicked!(widget.index),
            icon: const Icon(Icons.add),
          ),
          IconButton(
            onPressed: () => widget.onRemoveClicked!(widget.index),
            icon: const Icon(Icons.delete),
          ),
        ],
      ),
    ]);

    child = Container(
      color: Colors.black12,
      width: widgetSizeDiff == 0 ? null : widgetSizeDiff,
      height: widgetSizeDiff == 0 ? null : widgetSizeDiff,
      child: child,
    );

    return child;
  }
}

// --------------------------------------------------------------------------

class MySeparator extends StatefulWidget {
  final int index;

  const MySeparator(this.index, {super.key});

  [@override](/user/override)
  State<MySeparator> createState() => _MySeparatorState();
}

class _MySeparatorState extends State<MySeparator> {
  [@override](/user/override)
  void deactivate() {
    super.deactivate();
    //log("deactivate separator widget ${widget.index}");
  }

  [@override](/user/override)
  void activate() {
    super.activate();
    //log("activate separator widget ${widget.index}");
  }

  [@override](/user/override)
  void dispose() {
    super.dispose();
    log("dispose separator widget ${widget.index}");
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Container(
      color: Colors.grey,
      child: Text("${widget.index}"),
    );
  }
}

更多关于Flutter高性能列表滚动插件jk_fast_listview的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter高性能列表滚动插件jk_fast_listview的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何使用 jk_fast_listview 插件的 Flutter 代码示例。jk_fast_listview 是一个用于提高列表滚动性能的插件,尤其适用于处理大量数据的情况。

首先,确保你已经在 pubspec.yaml 文件中添加了 jk_fast_listview 依赖:

dependencies:
  flutter:
    sdk: flutter
  jk_fast_listview: ^最新版本号  # 替换为实际最新版本号

然后,运行 flutter pub get 以安装依赖。

接下来是一个简单的示例代码,展示了如何使用 jk_fast_listview 来创建一个高性能的列表:

import 'package:flutter/material.dart';
import 'package:jk_fast_listview/jk_fast_listview.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final List<String> items = List.generate(10000, (index) => "Item $index");

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('jk_fast_listview Demo'),
      ),
      body: JKFastListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index]),
          );
        },
      ),
    );
  }
}

解释

  1. 依赖导入

    • pubspec.yaml 中添加 jk_fast_listview 依赖。
  2. 基本结构

    • 使用 MaterialAppScaffold 来构建基本的 Flutter 应用结构。
  3. 数据准备

    • _MyHomePageState 类中,我们生成了一个包含 10000 个字符串的列表。
  4. 高性能列表

    • 使用 JKFastListView.builder 来构建高性能列表。
    • itemCount 指定了列表项的数量。
    • itemBuilder 是一个函数,用于构建每个列表项。

注意事项

  • jk_fast_listview 通过优化渲染机制来提高滚动性能,特别适用于大数据量的情况。
  • 确保你已经仔细阅读了插件的文档,以了解更多高级功能和配置选项。

这个示例展示了如何使用 jk_fast_listview 来创建一个简单的高性能列表。你可以根据自己的需求进一步自定义和扩展这个示例。

回到顶部