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

genericUser
  • 4,417
  • 1
  • 28
  • 73

6 Answers6

9

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.

Alexey Inkin
  • 1,853
  • 1
  • 12
  • 32
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);
    }
  )
)
Paul
  • 352
  • 2
  • 3
  • 2
    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 – fravolt Jan 04 '22 at 13:32
  • 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 Jan 04 '22 at 15:24
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')),
        ],
      ),
    ),
  );
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • 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 Jan 06 '22 at 20:16
  • 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? – genericUser Jan 06 '22 at 20:20
  • 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 Jan 06 '22 at 20:30
  • `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 – CopsOnRoad Jan 06 '22 at 20:32
  • 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 Jan 06 '22 at 20:49
  • 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 Jan 06 '22 at 20:52
  • 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. – genericUser Jan 06 '22 at 21:01
  • @genericUser I've abstracted the logic in a separate class, and it should now be easy to use it. Please check the updated code. – CopsOnRoad Jan 07 '22 at 09:25
  • 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 Jan 07 '22 at 10:10
  • I have edited my question, added my layout if it helps. – genericUser Jan 07 '22 at 11:36
  • @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. – CopsOnRoad Jan 07 '22 at 13:47
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])
        ],
      );


);
Vamsee.
  • 181
  • 3
  • 12
  • 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 Jan 04 '22 at 12:40
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])
     
Vishal_VE
  • 1,852
  • 1
  • 6
  • 9
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: [....],
    );
  }
Leaf
  • 1
  • 1