10

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

7 Answers 7

11
+50

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.

Sign up to request clarification or add additional context in comments.

Comments

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);
    }
  )
)

2 Comments

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

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')),
        ],
      ),
    ),
  );
}

11 Comments

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?
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?
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.
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
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.
|
0

It appears that you are using text fields so it hides data or sometimes it may overflow borders by black and yellow stripes

better to use SingleChildScrollView and for scrolling direction use scrollDirection with parameters Axis.vertical or Axis.horizontal

return SingleChildScrollView(
      scrollDirection: Axis.vertical,
      child :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])
        ],
      );


);

1 Comment

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

Please try this solution. Hope it will work for you. Thanks.

 Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: GroupedListView<dynamic, String>(
              scrollDirection: Axis.vertical,
              shrinkWrap: true,
              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])
     

1 Comment

Have you just added shrinkWrap: true? No, it's still not working
0

In short: use reversed: true, jump the scrolling position to 0.

  final scrollController = ScrollController();

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (scrollController.hasClients) {
        scrollController.jumpTo(scrollController.position.maxScrollExtent);
    }
   });
  }
  

  Widget _buildScrollView(){
    return SingleChildScrollView(
      reverse: true,
      controller: scrollController,
      child: [....],
    );
  }

Comments

0

await Future.delayed(const Duration(milliseconds: 100)); before scrolling

1 Comment

As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.