Flutter resizeToAvoidBottomInset true not working with Expanded ListView
Solution 1
In short: use reversed: true
.
What you see is the expected behavior for the following reason:
ListView
preserves its scroll offset when something on your screen resizes. This offset is how many pixels the list is scrolled to from the beginning. By default the beginning counts from the top and the list grows to bottom.
If you use reversed: true
, the scroll position counts from the bottom, so the bottommost position is 0
, and the list grows from bottom to the top. It has many benefits:
-
The bottommost position of
0
is preserved when the keyboard opens. So does any other position. At any position it just appears that the list shifts to the top, and the last visible element remains the last visible element. -
Its easier to sort and paginate messages when you get them from the DB. You just sort by datetime descending and append to the list, no need to reverse the object list before feeding it to the ListView.
-
It just works with no listeners and the controller manipulations. Declarative solutions are more reliable in general.
The rule of thumb is to reverse the lists that paginate with more items loading at the top.
Here is the example:
import 'package:flutter/material.dart';
void main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: ListView.builder(
itemCount: 30,
reverse: true,
itemBuilder: (context, i) => ListTile(title: Text('Item $i')),
),
),
const TextField(),
],
),
),
);
}
}
As for resizeToAvoidBottomInset
, it does its job. The Scaffold
is indeed shortened with the keyboard on. So is ListView
. So it shows you less items. For non-reversed list, gone are the bottommost.
Solution 2
It looks like you want the GroupedListView to be visible from the last line. The WriteMessageBox is pushed up by the keyboard and obscures the last messages. The most direct solution is to scroll the list to the bottom when the keyboard is visible. That is, when the WriteMessageBox gains focus.
Add a FocusScope to the WriteMessageBox in the build() method. It becomes
FocusScope(
child: Focus(
child: WriteMessageBox(),
onFocusChange: (focused) {
if (focused) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
)
)
Solution 3
Screenshot:
Code:
You can use MediaQueryData
to get the height of keyboard, and then scroll the ListView
up by that number.
Create this class:
class HandleScrollWidget extends StatefulWidget {
final BuildContext context;
final Widget child;
final ScrollController controller;
HandleScrollWidget(this.context, {required this.controller, required this.child});
@override
_HandleScrollWidgetState createState() => _HandleScrollWidgetState();
}
class _HandleScrollWidgetState extends State<HandleScrollWidget> {
double? _offset;
@override
Widget build(BuildContext context) {
final bottom = MediaQuery.of(widget.context).viewInsets.bottom;
if (bottom == 0) {
_offset = null;
} else if (bottom != 0 && _offset == null) {
_offset = widget.controller.offset;
}
if (bottom > 0) widget.controller.jumpTo(_offset! + bottom);
return widget.child;
}
}
Usage:
final ScrollController _controller = ScrollController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ListView')),
body: HandleScrollWidget(
context,
controller: _controller,
child: Column(
children: [
Expanded(
child: ListView.builder(
controller: _controller,
itemCount: 100,
itemBuilder: (_, i) => ListTile(title: Text('Messages #$i')),
),
),
TextField(decoration: InputDecoration(hintText: 'Write a message')),
],
),
),
);
}
genericUser
🔭 looking for knowledge to grab 🤓 🎯 Learn, Contribute and Grow 🌱
Updated on January 02, 2023Comments
-
genericUser over 1 year
The keyboard hides my
ListView
(GroupedListView). I think it's because of theExpanded
Widget.My body:
Column( children: [ Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: GroupedListView<dynamic, String>( controller: _scrollController, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics()), itemBuilder: (context, message) { return ListTile( title: ChatBubble(message), ); }, elements: messages, groupBy: (message) => DateFormat('MMMM dd,yyyy') .format(message.timestamp.toDate()), groupSeparatorBuilder: (String groupByValue) => getMiddleChatBubble(context, groupByValue), itemComparator: (item1, item2) => item1.timestamp.compareTo(item2.timestamp), useStickyGroupSeparators: false, floatingHeader: false, order: GroupedListOrder.ASC, ), ), ), WriteMessageBox( group: group, groupId: docs[0].id, tokens: [widget.friendToken]) ], );
Why the
resizeToAvoidBottomInset
isn't working?I have opened an issue to the Flutter team
-
genericUser over 2 yearsI'm not facing "overflow borders". My problem is that the keyboard AND my
WriteMessageBox
are hiding myGroupedListView
. Also, I tried your solution, It didn't work and I don't think it's a good idea to put anExpanded
inside aSingleChildScrollView
. -
genericUser over 2 yearsHave you just added
shrinkWrap: true
? No, it's still not working -
fravolt over 2 yearsThis seems fine, though it could be the case that the user starts typing while not scrolled to the bottom. In Whatsapp for example it only 'scrolls down' for you if you're already at the very bottom, otherwise it overlays like the current behaviour outlined by GenericUser. I am however uncertain if you can still know whether the scroll controller was at the bottom once the keyboard has appeared, it might be more trouble than it's worth, since you can mostly assume that the user wants to type a new message if they tap the chat bar :p
-
genericUser over 2 yearsHey @Paul thanks for your answer, but it's not the solution I was looking for. As @fravolt mentioned, a user can start typing from any position in the list, it should not scroll to the bottom. Also, I don't want to mess up my code with scroll jumping and positions, I want a proper answer to why the
resizeToAvoidBottomInset true
is not working in that case, and how to fix that. -
genericUser over 2 yearsThanks for your answer @CopsOnRoad. It scrolls me always to the bottom. If I want to open the keyboard in the middle of the chat, I don't want it to be activated (same as WhatsApp). How do I do that?
-
genericUser over 2 yearsAlso, it still bothers me that I have to perform all these unnecessary actions since the
resizeToAvoidBottomInset
is not working in that edge case. Why do you think it's happening? How can I fix that without controlling the scroll position? -
CopsOnRoad over 2 yearsWhatsApp doesn't scroll the messages when the keyboard is opened (which also doesn't require any work) but Telegram on the other hand scrolls the messages with keyboard height. So, you need to handle the scrolling yourself. I can provide you a workaround for last item part you mentioned. Give me a minute please.
-
CopsOnRoad over 2 years
var bottom = MediaQuery.of(context).viewInsets.bottom; if (bottom >= 10) { _timer?.cancel(); _timer = Timer(Duration(milliseconds: 200), () { _controller.jumpTo(_controller.offset + bottom); }); }
You can try something like this, copy and paste this code to view it clearly on your IDE -
genericUser over 2 yearsWhatsApp will act as
resizeToAvoidBottomInset: true
if you were at the bottom of theListView
. I have already created another chat application, where theresizeToAvoidBottomInset
worked like a charm. I don't understand why it's not working in this scenario. -
genericUser over 2 yearsI have pasted your code, but it seems always to scroll till the end. I don't want it to scroll if it was not at the bottom in the first place.
-
genericUser over 2 yearsAlso, if you could suggest a better Widgets aggregation, that will solve the
resizeToAvoidBottomInset
issue, it would be great. That is what I'm looking for. -
CopsOnRoad over 2 years@genericUser I've abstracted the logic in a separate class, and it should now be easy to use it. Please check the updated code.
-
genericUser over 2 yearsHey @CopsOnRoad, Just tested your class. It still always scrolls to the end (regardless of the
ListView
position). I tried to figure out why, but it just seems that working withviewInsets.bottom
andcontroller.offset
isn't so accurate. -
genericUser over 2 yearsI have edited my question, added my layout if it helps.
-
CopsOnRoad over 2 years@genericUser Did you test my code (without adding yours widget to it)? It doesn't scroll to the end. Working with
viewInsets.bottom
is the real deal in situation like this, you can see in the screenshot how much pixel accurate the solution is.