Flutter长列表向上滚动出现抖动异常,使用BloC、Firebase与grouped_list库
Flutter 分组列表滚动抖动异常问题
问题现象
使用带标题文本的Column布局,内部嵌套StreamBuilder,通过BloC从Firebase获取数据后构建自定义列表项。向下滚动后再尝试向上滚动时,无论滚动速度快慢,滚动都会快速上下抖动,向上滚动的进度非常缓慢。
排查情况
在Profile模式下运行应用时,曾观测到着色器卡顿(已知可通过预热Skia着色器修复)和其他延迟现象,但复现上述滚动问题时无相关异常,所有操作耗时均在16ms以内。grouped_list库目前也没有相关活跃issue,暂不确定是否为该库的问题。
相关代码
class PickUpPage extends StatefulWidget { const PickUpPage({Key? key}) : super(key: key); @override State<StatefulWidget> createState() => PickUpPageState(); } class PickUpPageState extends State<PickUpPage> { late PickUpScreenBloc _bloc; late GameDetailsBloc _detailsBloc; late final StreamSubscription _idsStreamSub; @override void initState() { super.initState(); _bloc = PickUpScreenBloc(); _detailsBloc = GameDetailsBloc(); _setListeners(); } void _setListeners() { _idsStreamSub = _bloc.idsStream.listen((ids) { _detailsBloc.getDetailsUsingIds(ids); }); } @override void dispose() { _idsStreamSub.cancel(); _bloc.dispose(); _detailsBloc.dispose(); super.dispose(); } @override void deactivate() { _bloc.dispose(); _detailsBloc.dispose(); super.deactivate(); } // 用于用户登录后返回列表页时刷新页面 FutureOr _onNavigateBack(dynamic val) { setState(() {}); } void _handleGameSelected(PickUpGameDetails details) { Navigator.push( context, MaterialPageRoute( builder: (context) => GameDetailsPage( details: details, ), ), ).then((value) => _onNavigateBack(value)); } @override Widget build(BuildContext context) { User? user = FirebaseAuth.instance.currentUser; return Column( children: [ Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.only(top: 15, left: 15, right: 15, bottom: 15), child: user == null ? const Text( 'Choose a pick-up game to play in:', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black, ), ) : Text( 'Hey ${user.displayName == null ? '{no display name}' : user.displayName!}, choose a pick-up game to play in:', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black, ), ), ), const SizedBox( height: 10, ), BlocProvider.value( value: _detailsBloc, child: StreamBuilder<PickUpGameDetails>( stream: _detailsBloc.gameDetailsStream, builder: (context, snapshot) { if (!snapshot.hasError) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } else { return Expanded( child: GroupedListView<PickUpGameDetails, String>( elements: _detailsBloc.gameDetailsList, sort: true, order: GroupedListOrder.ASC, groupComparator: (group1, group2) => group1.compareTo(group2), groupBy: (gameItem) => gameItem.gameData!.dateTime!.substring(4, 8), itemComparator: (item1, item2) => GameData.getGame24hTime(item1.gameData!.dateTime!) .compareTo(GameData.getGame24hTime( item2.gameData!.dateTime!)), indexedItemBuilder: (BuildContext context, PickUpGameDetails details, int index) => InkWell( splashColor: const Color(0xffff5a5f), child: PickUpGameItem( details.gameId!, details, Key(index.toString())), onTap: () => {_handleGameSelected(details)}, ), groupHeaderBuilder: (PickUpGameDetails details) => Padding( padding: const EdgeInsets.only( left: 20, top: 5, bottom: 5), child: Text( details.gameData!.formattedDateTime, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 20, color: Colors.black), ), ), ), ); } } else { return ErrorWidget('Something went wrong!'); } }), ), ], ); } } class PickUpGameItem extends StatefulWidget { final String gameId; final PickUpGameDetails details; const PickUpGameItem(this.gameId, this.details, Key? key) : super(key: key); @override _PickUpGameItemState createState() => _PickUpGameItemState(); } class _PickUpGameItemState extends State<PickUpGameItem> { PickUpGameDetails? _gameDetails; GameDetailsBloc? _detailsBloc; @override void initState() { super.initState(); _detailsBloc = BlocProvider.of<GameDetailsBloc>(context); _detailsBloc!.subscribeToGameDetailsUpdatesWithId(widget.gameId); } @override Widget build(BuildContext context) { return StreamBuilder<Tuple2<String, PickUpGameDetails>>( stream: _detailsBloc!.detailsUpdatesStream, builder: (context, snapshot) { if (snapshot.hasError || snapshot.data == null || snapshot.connectionState == ConnectionState.waiting) { _gameDetails = widget.details; } else { if (snapshot.data!.item1 == widget.gameId) { _gameDetails = snapshot.data!.item2; } else { _gameDetails = widget.details; } } return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row( children: [ ClipRRect( borderRadius: BorderRadius.circular(15.0), child: _gameDetails!.locationInfo == null ? const SizedBox() : CachedNetworkImage( imageUrl: _gameDetails!.locationInfo!.pictures.elementAt(0), width: 80, height: 80, fit: BoxFit.fill, placeholder: (context, url) => const SizedBox( child: Center(child: CircularProgressIndicator()), width: 10, height: 10), ), ), const SizedBox( width: 10, ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _gameDetails!.locationInfo == null ? 'Loading...' : _gameDetails!.locationInfo!.nam, style: const TextStyle( color: Colors.black, fontWeight: FontWeight.bold, fontSize: 16), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox( height: 10, ), _gameDetails!.gameData!.hostInfo == null ? Text( _gameDetails!.gameData!.gameTypeMsg!, style: const TextStyle( color: Colors.black, fontWeight: FontWeight.normal, fontSize: 14), maxLines: 2, overflow: TextOverflow.ellipsis, ) : Text( '${_gameDetails!.gameData!.gameTypeMsg!} with ${_gameDetails!.gameData!.hostInfo!.hostNickname}.', style: const TextStyle( color: Colors.black, fontWeight: FontWeight.normal, fontSize: 14), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox( width: 5, ), Flexible( flex: 0, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( GameData.getGameTimestamp( _gameDetails!.gameData!.dateTime!), style: const TextStyle(color: Colors.black, fontSize: 15), ), const SizedBox( height: 10, ), Row( children: [ Text( '${_gameDetails!.gameData!.getCurrentPlayerNumber()}/${_gameDetails!.gameData!.maxPlayers}', style: const TextStyle( color: Colors.grey, fontWeight: FontWeight.normal, fontSize: 14), ), const SizedBox( width: 5, ), const ImageIcon( AssetImage('assets/icons/profile.png')) ], ), ], ), ), ], ), ); }); } }
另有问题复现演示视频,可访问原提问链接查看。
内容的提问来源于stack exchange,提问作者Stelios Papamichail
相关产品推荐
相关产品推荐

