Flutter高性能列表滚动插件jk_fast_listview的使用
Flutter高性能列表滚动插件jk_fast_listview的使用
JkFastListView
ListView
/ GridView
适用于 Flutter。支持在大量数据之间快速滚动,灵活的组件大小,基于索引的滚动,项目回收。
特性
- 大部分 API 与官方 Flutter 的
ListView
和GridView
相同。 - 支持在大量数据之间快速滚动。
- 灵活的组件大小。
- 按指定索引滚动。
- 监听当前可见的第一个项目的索引。
- 项目回收。
谁需要这个包?
- 如果所有项目具有相同的宽度和高度,请使用官方 Flutter 的
ListView
和GridView
。这两个类在固定大小项目的情况下工作良好,并且也支持在大量数据之间快速滚动。 - 如果项目的宽度和高度是可变的,并且你需要快速跳转到远距离的项目(通过调用
jumpToIndex
或animateToIndex
,或通过用户拖动滚动条),请使用此包。这在某些应用程序中非常有用(例如聊天应用)。
快速开始
安装
在你的包的 pubspec.yaml
文件中添加以下依赖:
dependencies:
jk_fast_listview: ^0.8.0
使用
构建一个 ListView
注意:大多数参数的名称与官方 Flutter 的 ListView
和 GridView
相同。我们不再解释每个参数。
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
更多关于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]),
);
},
),
);
}
}
解释
-
依赖导入:
- 在
pubspec.yaml
中添加jk_fast_listview
依赖。
- 在
-
基本结构:
- 使用
MaterialApp
和Scaffold
来构建基本的 Flutter 应用结构。
- 使用
-
数据准备:
- 在
_MyHomePageState
类中,我们生成了一个包含 10000 个字符串的列表。
- 在
-
高性能列表:
- 使用
JKFastListView.builder
来构建高性能列表。 itemCount
指定了列表项的数量。itemBuilder
是一个函数,用于构建每个列表项。
- 使用
注意事项
jk_fast_listview
通过优化渲染机制来提高滚动性能,特别适用于大数据量的情况。- 确保你已经仔细阅读了插件的文档,以了解更多高级功能和配置选项。
这个示例展示了如何使用 jk_fast_listview
来创建一个简单的高性能列表。你可以根据自己的需求进一步自定义和扩展这个示例。