Flutter resizeToAvoidBottomInset true not working with Expanded ListView

534

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:

  1. 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.

  2. 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.

  3. 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:

enter image description here

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')),
        ],
      ),
    ),
  );
}
Share:
534
genericUser
Author by

genericUser

🔭 looking for knowledge to grab 🤓 🎯 Learn, Contribute and Grow 🌱

Updated on January 02, 2023

Comments

  • genericUser
    genericUser over 1 year

    The keyboard hides my ListView (GroupedListView). I think it's because of the Expanded 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])
            ],
          );
    

    enter image description here

    Why the resizeToAvoidBottomInset isn't working?

    I have opened an issue to the Flutter team

  • genericUser
    genericUser over 2 years
    I'm not facing "overflow borders". My problem is that the keyboard AND my WriteMessageBox are hiding my GroupedListView. Also, I tried your solution, It didn't work and I don't think it's a good idea to put an Expanded inside a SingleChildScrollView.
  • genericUser
    genericUser over 2 years
    Have you just added shrinkWrap: true? No, it's still not working
  • fravolt
    fravolt over 2 years
    This 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
    genericUser over 2 years
    Hey @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
    genericUser over 2 years
    Thanks 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
    genericUser over 2 years
    Also, 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
    CopsOnRoad over 2 years
    WhatsApp 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
    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
    genericUser over 2 years
    WhatsApp will act as resizeToAvoidBottomInset: true if you were at the bottom of the ListView. I have already created another chat application, where the resizeToAvoidBottomInset worked like a charm. I don't understand why it's not working in this scenario.
  • genericUser
    genericUser over 2 years
    I 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
    genericUser over 2 years
    Also, 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
    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
    genericUser over 2 years
    Hey @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 with viewInsets.bottom and controller.offset isn't so accurate.
  • genericUser
    genericUser over 2 years
    I have edited my question, added my layout if it helps.
  • CopsOnRoad
    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.