Issue
I have a Column with a header text and a nested StreamBuilder through which I build out a list of custom items that I receive from Firebase through my BloC. I noticed however that if I scroll downwards and then try to scroll back up, either slowly or fast, the scroll seems to oscillate (up/down) very quickly and makes very small progress towards going up.
I ran the app in Profile mode and did witness shader junk (which I read can be fixed by warming up Skia shaders) and some other lag but nothing during the issue that I describe. Everything is under 16ms. The grouped_list library doesn’t seem to have any active, related issues either so I’m not sure if it’s something on that end. Here’s my page’s code and a video to better describe the 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();
}
// used to rebuild the page when a user logged-in and returned to the list page
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'))
],
),
],
),
),
],
),
);
});
}
}
Video link can be found here
Solution
Add mainAxisSize : MainAxisSize.min to the column widget
Add shrinkWrap : true to the list view widget.
Add physics NeverScrollPhysics to the list view.
Wrap the column with a singleChildScrollView.
If it throws an overflow in the bottom wrap the singleChildScrollView with a container of height same as the device and width as device width.
Answered By – Kaushik Chandru
Answer Checked By – Clifford M. (FlutterFixes Volunteer)