背景
用户在频繁操作中,无论是进入详情页还是操作页,再次返回列表后,表格恢复到了初始状态。用户连贯操作不顺畅,用户提出要优化,有些操作需要刷新并保持原始操作位置,有些操作需要刷新重置。
需求梳理
用户诉求合情合理,操作其它APP(eg:掘金)也是如此。区分场景:
需要保持原始位置的操作,梳理来看是本身业务的操作,需要进行连续性操作的页面。连续在A数据上进行操作
、操作完A数据立即对下一个B数据进行操作
。
- 进入详情
- 进入编辑页
- 进入揽收页
- ...
刷新重置的操作,进入其它业务页面,连续性操作必须打断的场景。
- 订单操作完从订单入口生成运费进行操作
- ...
搜索方案
在 Flutter 应用中,实现从分页列表点进详情页后返回列表并保持之前点击的位置,可以通过以下几种方法实现:
方法一:使用 AutomaticKeepAliveClientMixin
AutomaticKeepAliveClientMixin
可以帮助你在返回页面时保持页面的状态。这在某些情况下非常有用,特别是当列表数据不需要重新获取时。
-
在详情页中使用
AutomaticKeepAliveClientMixin
: -
确保在返回列表页时数据不会刷新:
如果列表数据在每次构建时都重新获取,那么即便使用了
AutomaticKeepAliveClientMixin
,用户返回时看到的也可能还是初始状态。确保你的列表数据在不需要时不会重新获取。
方法二:保存滚动位置
另一种方法是在进入详情页前保存列表的滚动位置,并在返回时恢复该位置。
-
在列表页中保存滚动位置:
dart@override void initState() { super.initState(); _scrollController.addListener(() { setState(() { _scrollPosition = _scrollController.position.pixels; }); }); }
-
详情页保持不变:
详情页可以像之前一样实现,不需要特别处理。
方法三:使用状态管理(如 Provider、Riverpod 或 BLoC)
对于更复杂的应用,可以使用状态管理库来保存和恢复状态。例如,使用 Riverpod 或 Provider 来管理滚动位置的状态。
-
设置状态管理:
lessimport 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; final listScrollPositionProvider = StateProvider<double>((ref) => 0.0); class ListPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final scrollPosition = ref.watch(listScrollPositionProvider); final scrollController = ScrollController(); useEffect(() { scrollController.addListener(() { ref.update(listScrollPositionProvider, scrollController.position.pixels); }); return () { scrollController.dispose(); }; }, []); return Scaffold( appBar: AppBar( title: Text('List Page'), ), body: ListView.builder( controller: scrollController, itemCount: 100, itemBuilder: (context, index) { return ListTile( title: Text('Item $index'), onTap: () { Navigator.push( context, MaterialPageRoute(builder: (context) => DetailPage()), ).then((_) { scrollController.animateTo( scrollPosition, duration: Duration(milliseconds: 300), curve: Curves.easeInOut, ); }); }, ); }, ), ); } } class DetailPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Detail Page'), ), body: Center( child: Text('Detail Page Content'), ), ); } } void main() { runApp( ProviderScope( child: MaterialApp( home: ListPage(), ), ), ); }
这些方法可以根据你的应用需求和复杂度选择使用。对于简单应用,方法一和方法二通常已经足够;对于更复杂的应用,使用状态管理库(如 Riverpod 或 Provider)可能更合适。
应用方案
根据搜索到的方案结合应用实际场景,发现搜索方案只能提供一种思路方向,不能完全落地到实际应用中。
- 采用页面缓存方式,虽然能保持原始位置,但是不能刷新。很多操作页处理完返回时数据展示需要更新。
js
// with AutomaticKeepAliveClientMixin
@override
bool get wantKeepAlive => true;
- 缓存当前页信息,返回时请求目标页数据,虽然能返回刷新并保持原始位置,但是对于列表插件来说,下拉操作只是刷新当前页,不能
page--
去补齐前面数据。
应用方案落地
缓存当前页信息,返回时计算一次性还原多少条数据。当用户操作到第2页第4条数据时,记录prevPage = 2
,记录第4条的滚动条位置_scrollPosition = 89.0
,返回列表时进行page = 1;limit = 2 * 10
的数据请求,请求完成后滚动条定位到89.0
。
js
// 根据之前分页情况,计算出一次性要还原多少条数据
limit = prevPage * 10
// 每次返回请求第一页
page = 1;
// 记录滚动条位置
_controller.addListener(() {
setState(() {
_scrollPosition = _controller.position.pixels;
});
});
// 加载完数据后定位到记录的滚动条为止
if (_controller.hasClients) _controller.animateTo(_scrollPosition, duration: Duration(milliseconds: 100), curve: Curves.decelerate);
信心满满,一进行测试傻眼(⊙_⊙)?
第一次操作返回效果很完美继续操作就失效了,一次操作后page还原成1了,再次操作后返回数据请求变成了page = 1;limit = 1 * 10
。
继续尝试,缓存page不行,直接缓存请求到的条数list.length
(不是总条数totalCount)。list.length存在可能不是10的倍数,因为操作最后一页时可能不满10条。返回列表时进行page = 1;limit = Utils.roundUpTen(list.length)
的数据请求,向上补齐10的倍数。
再次测试,实现完成。
调用执行链
调用执行链包含如下六步走:
- 跳转详情页(
用户行为
) - 跳转详情页,返回触发回调处理
- 根据之前分页情况,计算出一次性要还原多少条数据
- 请求之前加载到的所有数据
- 数据完成加载
- 滚动条定位到之前位置
js
// 1.跳转详情页
NavigatorUtils.push(context, OrderRouter.orderInfoPage, params: {'orderNo': item.orderNo}).then((value) {
// 2.跳转详情页,返回时回调处理
doRefreshList(true);
}),
void doRefreshList(isRefresh) {
if (isRefresh) {
// 3. 根据之前分页情况,计算出一次性要还原多少条数据
_page = 1;
_getOrderList(limit: Utils.roundUpTen(_list.length));
}
}
// 刷新重置
Future<void> _onRefresh() async {
_page = 1;
_scrollPosition = 0.0;
_getList();
}
_getList({int? limit, bool? isMore}) async {
_isLoading = isMore != true;
4. 请求数据
OrderPageResp? resp = await G.req.order.fetchOrderList(page: _page, limit:limit ?? 10);
if (mounted) {
setState(() {
var records = resp.orderList;
this._totalPage = resp.totalPage!;
if (_page == 1) {
// 5. 数据加载完成
this._list = records;
} else {
this._list.addAll(records);
}
// 6.滚动条定位到之前位置
if (_controller.hasClients) _controller.animateTo(_scrollPosition, duration: Duration(milliseconds: 100), curve: Curves.decelerate);
_isLoading = false;
});
}
}
后记
当前方案存在一个性能问题,在操作数据很靠后时,返回再次进入时请求数据量会比较大(因为要还原之前所有数据)。不过上线后用户用的很顺畅,可见实际应用场景并没有程序员预想的那么极端。