10

I have looked and found several solutions but none seem to fit my configuration and need some assistance. I will put all my code here and see if anyone knows where to apply the ScrollController. I have tried on the originating ListView, but I dynamically build other item under in the ListView builder from a futureResponse.

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';


import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cswauthapp/models.dart';
import 'package:cswauthapp/ChatConvoDetail.dart';
import 'package:cswauthapp/Settings.dart' as Admin;
import 'package:cswauthapp/HomePage.dart' as HomePage;
import 'package:cswauthapp/main.dart' as MyHomePage;
import 'package:cswauthapp/PostAuthHome.dart' as PostAuthHome;
import 'package:image_picker/image_picker.dart';
import 'package:cswauthapp/ShowPic.dart';
import 'package:path/path.dart' as path;
import 'package:video_player/video_player.dart';
import 'package:cswauthapp/vplayer.dart' as vplayer;

class ChatDivided extends StatefulWidget {
  ChatDivided({Key key, this.title, this.mychat}) : super(key: key);

  static const String routeName = "/ChatDivided";

  final ChatList mychat;
  final String title;

  @override
  _ChatDividedState createState() => new _ChatDividedState();
}

class _ChatDividedState extends State<ChatDivided> {
  SharedPreferences prefs;
  int oid = 0;
  int pid = 0;
  int authlevel = 0;
  bool admin = false;
  int type = 0;
  String msgid = '';
  List chatlist;
  int listcount = 0;
  bool grpmsg = true;
  String sender = '';
  String receiver = '';
  String message = '';
  String oname = '';
  String pname = '';
  String sendname;
  String receivename;
  String replyto = '';
  String replyfrom = '';
  String replysub = '';
  final TextEditingController _newreplycontroller = new TextEditingController();
  String myfcmtoken = 'NONE';
  Future<http.Response> _responseFuture;
  var _urlDates = '';
  Future<File> _imageFile;
  String myimage;
  String myvideo;
  File myimagefile;
  File myvidfile;
  Future<int> myimagelength;
  String myext;
  VideoPlayerController vcontroller;
  bool isImage = false;
  bool isVideo = false;
  //ScrollController scontroller = new ScrollController();

  _getPrefs() async {
    prefs = await SharedPreferences.getInstance();

    if (mounted) {
      setState(() {
        oid = prefs.getInt('oid');
        pid = prefs.getInt('pid');
        authlevel = prefs.getInt('authlevel');
        admin = prefs.getBool('admin');
        type = 1;
        msgid = widget.mychat.msgkey;
        if (widget.mychat.grpid == '0') {
          grpmsg = false;
        } else {
          grpmsg = true;
        }
        oname = widget.mychat.oname;
        pname = widget.mychat.pname;
        myfcmtoken = prefs.getString('fcmtoken');
        if (authlevel == 0) {
          sender = 'o';
          receiver = 'p';
          sendname = widget.mychat.oname;
          receivename = widget.mychat.pname;
        } else if (authlevel == 1) {
          sender = 'p';
          receiver = 'o';
          sendname = widget.mychat.pname;
          receivename = widget.mychat.oname;
        }
        //_getChats();
      });
    }
  }

  @override
  void initState() {
    super.initState();
    //controller = new TabController(length: 4, vsync: this);
    _getPrefs();
    _urlDates =
        'http://$baseurl/chat/messages/getdates/${widget
        .mychat.msgkey}';
    _responseFuture = http.get(_urlDates, headers: getAuthHeader());
  }

  var jsonCodec = const JsonCodec();
  var _focusnode = new FocusNode();


  _getChats() async {

    var _url =
        'http://$baseurl/chat/messages/getdates/$msgid';

    var http = createHttpClient();
    var response = await http.get(_url, headers: getAuthHeader());

    var chats = await jsonCodec.decode(response.body);

    if (mounted) {
      setState(() {
        chatlist = chats.toList();
        listcount = chatlist.length;
        //replysub = 'Re: ' + chatlist[0]['sub'];
      });
    }
  }

  Future<Null> _onRefresh() {
    Completer<Null> completer = new Completer<Null>();
    Timer timer = new Timer(new Duration(seconds: 1), () {
      setState(() {
        _responseFuture = http.get(_urlDates, headers: getAuthHeader());
        print('RUNNING LOAD AFTER REFRESH AGAIN');
      });
      completer.complete();
    });
    return completer.future;
  }

  Future<String> doImageString() async {

    return (await _imageFile).path.substring((await _imageFile).path.length - 3);
  }

  @override
  Widget build(BuildContext context) {
    Widget mytitle;
    if (grpmsg) {
      mytitle = new Row(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          new Icon(Icons.people),
          new Text('  '),
          new Text(widget.mychat.referralname)
        ],
      );
    } else {
      mytitle = new Row(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          new Icon(Icons.person),
          new Text('  '),
          new Text(widget.mychat.referralname)
        ],
      );
    }

    var _children = <Widget>[
      new Flexible(
        child: new RefreshIndicator(
            child: new FutureBuilder(
              future: _responseFuture,
              builder: (BuildContext context,
                  AsyncSnapshot<http.Response> response) {
                if (!response.hasData) {
                  return const Center(
                    //child: const Text('Loading Dates...'),
                    child: const CircularProgressIndicator(),
                  );
                } else if (response.data.statusCode != 200) {
                  return const Center(
                    child: const Text('Error loading data'),
                  );
                } else {
                  List<dynamic> json = JSON.decode(response.data.body);
                  return new MyChatList(json);
                }
              },
            ),
            onRefresh: _onRefresh),
      ),
      new Container(
          alignment: Alignment.bottomLeft,
          padding: new EdgeInsets.only(left: 10.0),
          child: new FutureBuilder<File>(
              future: _imageFile,
              builder: (BuildContext context, AsyncSnapshot<File> snapshot) {
                if (snapshot.connectionState == ConnectionState.done) {
                  //return new Image.file(snapshot.data);
                  myimagefile = snapshot.data;
                  myext = path.extension(myimagefile.path);
                  if (myext == '.jpg') {
                    isImage = true;
                    return new Column(
                      mainAxisAlignment: MainAxisAlignment.start,
                      children: <Widget>[
                        new Container(
                          alignment: Alignment.bottomLeft,
                          width: 150.0,
                          child: new Image.file(snapshot.data),
                        ),
                        new FlatButton(
                            onPressed: _doClear, child: new Text('Clear Image'))
                      ],
                    );
                  } else {
                    isVideo = true;
                    myvidfile = new File(snapshot.data.path.replaceAll('file://', ''));
                    vcontroller = new VideoPlayerController(myimagefile.path)..initialize();
                    return new Column(
                      mainAxisAlignment: MainAxisAlignment.start,
                      children: <Widget>[
                        new Container(
                          alignment: Alignment.bottomLeft,
                          width: 300.0,
                          child: new vplayer.VideoCard(controller: vcontroller, title: widget.mychat.referralname,subtitle: 'Video',),
                        ),
                        new FlatButton(
                            onPressed: _doClear, child: new Text('Clear Video'))
                      ],
                    );
                  }

                } else {
                  return const Text('');
                }
              })
      ),
      new Divider(
        height: 5.0,
        color: Colors.grey,
      ),
      new Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: <Widget>[
          new Container(
            alignment: Alignment.bottomLeft,
            //width: 50.0,
            child: new IconButton(
              icon: new Icon(Icons.add_a_photo),
              onPressed: _pickImage,
              alignment: Alignment.bottomLeft,
            ),
          ),
          new Flexible(
            child: new Container(
              alignment: Alignment.center,
              //width: 350.0,
              child: new TextField(
                decoration: const InputDecoration(
                  hintText: 'Reply',
                  labelText: 'Reply:',
                ),
                autofocus: false,
                focusNode: _focusnode,
                maxLines: 1,
                controller: _newreplycontroller,
                keyboardType: TextInputType.text,
              ),
            ),
          ),
          new Container(
              alignment: Alignment.bottomRight,
              //width: 50.0,
              child: new IconButton(
                icon: new Icon(Icons.send),
                onPressed: _sendReply,
                alignment: Alignment.centerRight,
                disabledColor: Colors.grey,
              )),
        ],
      ),
    ];

    return new Scaffold(
      appBar: new AppBar(
        title: mytitle,
        actions: getAppBarActions(context),
      ),
      body: new Column(
        children: _children,
      ),
    );
  }

  DateTime getDateDiv(int index) {
    DateTime msgdate = DateTime.parse(chatlist[index]['chatdate']).toLocal();
    return msgdate;
  }

  _doClear() {
    setState(() {
      _imageFile = null;
    });
  }

  _pickImage() async {
    await setState(()  {
      _imageFile = ImagePicker.pickImage(maxWidth: 600.0);
    });
  }

  _sendReply() {
    if (_newreplycontroller.text.isEmpty && myimagefile == null) {
      showDialog(
        context: context,
        child: new AlertDialog(
          content: new Text("There is no message to submit"),
          actions: <Widget>[
            new FlatButton(
                child: const Text('OK'),
                onPressed: () {
                  Navigator.pop(context, false);
                }),
          ],
        ),
      );
    } else {
      TextInputAction.done;
      DateTime dateSubmit = new DateTime.now();
      if (myimagefile != null) {
        if (isImage) {
          List<int> imageBytes = myimagefile.readAsBytesSync();
          myimage = BASE64.encode(imageBytes);
        }
        if (isVideo) {
          List<int> imageBytes = myvidfile.readAsBytesSync();
          myvideo = BASE64.encode(imageBytes);
        }

      } else {
        myimage = '';
        myvideo = '';
      }
      var mymessage = _newreplycontroller.text;
      ChatMessage mychat = new ChatMessage(
          widget.mychat.msgkey,
          widget.mychat.referralname,
          replysub,
          oid,
          oname,
          pid,
          pname,
          sender,
          sendname,
          receiver,
          receivename,
          mymessage,
          dateSubmit.toString(),
          widget.mychat.grpid,
          widget.mychat.prid,
          myfcmtoken,
          myimage,
          myvideo,
          myext
      );
      _doSendReply(mychat);
    }
  }

  _doSendReply(mychat) async {
    var json = jsonCodec.encode(mychat);
    var _url = 'http://$baseurl/chat/messages/send';

    //var request = new http.MultipartRequest('POST', _url)
    var http = createHttpClient();
    var response = await http.post(_url, body: json, headers: getJSONHeader());

    var chatresp = await jsonCodec.decode(response.body);
    if (chatresp.contains('GOOD')) {
      setState(() {
        _responseFuture = http.get(_urlDates, headers: getAuthHeader());
        _doClear();
        print('RUNNING LOAD AFTER SEND AGAIN');
      });
      _newreplycontroller.text = '';
      _focusnode.unfocus();
    } else if (chatresp.contains('EMPTY')) {
      showDialog(
        context: context,
        child: new AlertDialog(
          content: new Text("There is no message to submit"),
          actions: <Widget>[
            new FlatButton(
                child: const Text('OK'),
                onPressed: () {
                  Navigator.pop(context, false);
                }),
          ],
        ),
      );
    } else {}
  }
}

class MyChatList extends StatelessWidget {
  final List<dynamic> elementList;
  static ScrollController _scrollController;

  MyChatList(this.elementList);

  List<Widget> _getChildren() {
    List<Widget> children = [];
    elementList.forEach((element) {
      children.add(
        new MyChatWidget(
            datediv: element['msgdate'], msgkey: element['msgkey']),
      );
      //_scrollController.animateTo(0.0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
    });
    return children;
  }

  @override
  Widget build(BuildContext context) {
    //_scrollController.position.maxScrollExtent;
    return new ListView(
      shrinkWrap: true,
      controller: _scrollController,
      reverse: false,
      children: _getChildren(),
    );

  }
}

class MyChatWidget extends StatefulWidget {
  MyChatWidget({Key key, this.datediv, this.msgkey}) : super(key: key);

  final String datediv;
  final String msgkey;

  @override
  _MyChatWidgetState createState() => new _MyChatWidgetState();
}

class _MyChatWidgetState extends State<MyChatWidget> {
  List<Widget> messagelist;
  int messagecount = 0;
  var jsonCodec = const JsonCodec();
  var mydate = '';
  var _urlMessages = '';
  PageStorageKey _key;
  Future<http.Response> _responseFuture;
  List messList;
  var mybytes;
  File myimageview;
  Image newimageview;
  String imgStr;
  String vidStr;

  @override
  void initState() {
    super.initState();
    if (new DateFormat.yMd().format(DateTime.parse(widget.datediv)) ==
        new DateFormat.yMd().format(new DateTime.now())) {
      mydate = 'Today';
    } else {
      mydate = new DateFormat.yMMMEd().format(DateTime.parse(widget.datediv));
    }
    DateChatMessage dcm =
        new DateChatMessage(widget.msgkey, widget.datediv.toString());
    var json = jsonCodec.encode(dcm);
    _urlMessages =
        'http://loop-dev.clinicalsoftworks.com/chat/messages/getbydate';
    _responseFuture =
        http.post(_urlMessages, body: json, headers: getAuthHeader());

    //controller = new TabController(length: 4, vsync: this);
    //_getMessages();
  }

  @override
  Widget build(BuildContext context) {
    _key = new PageStorageKey('${widget.datediv.toString()}');
    VideoPlayerController vcontroller;
    return new Column(
      children: <Widget>[
        new Container(
          child: new Text(
            mydate,
            textAlign: TextAlign.left,
            style: new TextStyle(
              color: Colors.grey,
              fontWeight: FontWeight.bold,
            ),
          ),
          alignment: Alignment.centerLeft,
          padding: new EdgeInsets.only(left: 10.0),
        ),
        new Container(
          child: new Divider(
            height: 5.0,
            color: Colors.grey,
          ),
          padding: new EdgeInsets.only(left: 10.0, right: 10.0),
        ),
        /**/
        new FutureBuilder(
          future: _responseFuture,
          builder:
              (BuildContext context, AsyncSnapshot<http.Response> response) {
            if (!response.hasData) {
              return const Center(
                child: const Text('Loading Messages...'),
              );
            } else if (response.data.statusCode != 200) {
              return const Center(
                child: const Text('Error loading data'),
              );
            } else {
              List<dynamic> json = JSON.decode(response.data.body);
              messagelist = [];
              json.forEach((element) {
                DateTime submitdate =
                    DateTime.parse(element['submitdate']).toLocal();
                String myvideo = element['chatvideo'];
                String myimage = element['chatimage'];
                if (myimage != null) {
                  imgStr = 'http://loop-dev.clinicalsoftworks.com/chat/getimage/'+element['chatimage'];

                } else if (myvideo != null) {
                  vidStr = 'http://loop-dev.clinicalsoftworks.com/chatuploads/'+element['chatvideo'];
                  vcontroller = new VideoPlayerController(vidStr)..initialize();
                }
                _showLgPic() {
                  Route route = new MaterialPageRoute(
                    settings: new RouteSettings(name: "/ShowPic"),
                    builder: (BuildContext context) => new ShowPic(
                          image: imgStr,
                        ),
                  );
                  Navigator.of(context).push(route);
                }

                _addImage() {
                  //assert(imgStr != null);
                  //myimageview = new Image.memory(mbytes);
                  new GestureDetector(
                    /*child: new Image(
                      image: newimageview.image,
                      width: 300.0,
                    ),*/
                    child: new Image.network(imgStr),
                    onTap: _showLgPic,
                  );
                }

                _addNoImage() {
                  assert(imgStr == null);
                  new Text('');
                }

                _showAssets() {
                  if (imgStr != null) {
                    new GestureDetector(
                      child: new Image.network(
                        imgStr,
                        width: 300.0,
                      ),
                      onTap: _showLgPic,
                    );
                  } else if (vidStr != null) {
                    new vplayer.VideoCard(controller: vcontroller,title: element['referralname'],subtitle: 'video',);
                  } else {
                    new Container();
                  }
                }

                messagelist.add(
                  new Container(
                    //width: 300.0,
                    padding: new EdgeInsets.all(10.0),
                    child: new Column(
                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      mainAxisSize: MainAxisSize.min,
                      children: <Widget>[
                        new Container(
                          padding: new EdgeInsets.only(bottom: 5.0),
                          child: new Row(
                            mainAxisSize: MainAxisSize.min,
                            children: <Widget>[
                              new CircleAvatar(
                                child: new Text(
                                  element['sendname'][0],
                                  style: new TextStyle(fontSize: 15.0),
                                ),
                                radius: 12.0,
                              ),
                              new Text('    '),
                              new Text(
                                element['sendname'],
                                style: new TextStyle(
                                    fontSize: 15.0,
                                    fontWeight: FontWeight.bold),
                              ),
                              new Text('    '),
                              new Text(
                                new DateFormat.Hm().format(submitdate),
                                style: new TextStyle(
                                    color: Colors.grey, fontSize: 12.0),
                              ),
                              //new Text(submitdate.toLocal().toString())
                            ],
                          ),
                        ),

                        new Row(
                          children: <Widget>[
                            new Text('          '),
                            new Flexible(
                              child: new Text('${element['message']}'),
                            )
                          ],
                        ),
                        new Container(
                            width: 300.0,
                            child: new Row(
                              children: <Widget>[
                                new Text('          '),
                                //_showAssets(),
                                imgStr != null
                                    ? new GestureDetector(
                                        child: new Image.network(
                                          imgStr,
                                          width: 300.0,
                                        ),
                                        onTap: _showLgPic,
                                      )
                                    : vidStr != null
                                    ? new Flexible(child: new vplayer.VideoCard(controller: vcontroller,title: element['referralname'],subtitle: 'video',),)
                                    : new Container(),
                              ],
                            )
                        ),
                      ],
                    ),
                  ),
                );
              });
              return new Column(children: messagelist);
            }
          },
        ),
      ],
    );
  }
}

Any help will be greatly appreciated.

Robert
  • 5,347
  • 13
  • 40
  • 57
  • I tried all answers below. Nothing is suitable when you fetch data from firebase database. _scrollController.animateTo is always called before the list fetched from server. I can put some delayed function but that's not a good idea. Because time to fetch data from from server is completetly diffrent in each users. – salih kallai Oct 23 '19 at 04:14
  • @salihkallai that's a race condition. You shouldn't perform the scroll until the list is updated. – Kevin B Oct 20 '22 at 14:36

5 Answers5

27

To scroll bottom of dynamic ListView do as follow

ScrollController _scrollController = new ScrollController();

then

ListView.builder(
    controller: _scrollController,
    itemCount: list.lenght,
    itemBuilder: (BuildContext ctxt, int index) {
        return Text("GMF ${list[index]}");
    }
)

and finally

_scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 500), curve: Curves.easeOut);
Mohammad Faisal
  • 1,977
  • 1
  • 18
  • 15
9

The easiest way to achieve it is set reverse property of ListView to true and scroll to 0.0 using controller.

ListView(
  shrinkWrap: true,
  controller: _scrollController,
  reverse: true, \\ <- set this true
  children: _getChildren(),
);

then

_scrollController.animateTo(
        0.0,
        curve: Curves.easeOut,
        duration: const Duration(milliseconds: 300),
      );
Hemanth Raj
  • 32,555
  • 10
  • 92
  • 82
6

my solution... Inside ListView add:

reverse: true,
shrinkWrap: true,

and in my list:

listModel = List.from(listModel.reversed);
Ruben Helsloot
  • 12,582
  • 6
  • 26
  • 49
  • Found this helpful for a chat list. It scrolls to the bottom automatically. The other option of using the controller jump to wasn't as smooth and doesn't allow for a user to scroll up to the top without jumping to the end. – Miller Adulu Nov 17 '20 at 19:20
2

I am working on the Chat screen with requirement is scroll to bottom of the list any time new message sent. None of these solutions work for me. I even try the scrollable_positioned_list package but any luck. Then I try to use the SingleChildScrollView with reverse: true. The cool thing of SingleChildScrollView is that you don't have to reverse the datasource, just only set the reverse of the SingleChildScrollView to true.

Here are my codes.

class ChatsWidget extends StatefulWidget {
  const ChatsWidget({Key? key}) : super(key: key);

  @override
  _ChatsWidgetState createState() => _ChatsWidgetState();
}

class _ChatsWidgetState extends State<ChatsWidget> {
  @override
  Widget build(BuildContext context) {
    final ChatController controller = Provider.of<ChatController>(context);
    final ScrollController scrollController = controller.scrollController;

    return Flexible(
      fit: FlexFit.loose,
      child: Container(
        padding: EdgeInsets.zero,
        decoration: BoxDecoration(
          color: AppColors.background,
        ),
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints viewportConstraints) {
            return SingleChildScrollView(
              controller: scrollController,
              reverse: true,
              padding: EdgeInsets.only(left: 16, right: 16),
              child: ConstrainedBox(
                constraints: BoxConstraints(
                  minWidth: viewportConstraints.maxWidth,
                  minHeight: viewportConstraints.maxHeight,
                ),
                child: StreamProvider<List<Message>>.value(
                  value: controller.listStream,
                  initialData: [],
                  updateShouldNotify: (previous, current) => true,
                  child: Consumer<List<Message>>(
                    builder: (context, messages, child) {
                      return Column(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: <Widget>[
                          SizedBox(
                            height: 9,
                          ),
                          ...List<Widget>.generate(messages.length, (int index) {
                            final Message message = messages[index];

                            return (message.type == MessageType.incoming)
                                ? ChangeNotifierProvider<IncomingMessageController>(
                                    create: (_) => IncomingMessageController(message, controller),
                                    child: IncomingMessage(),
                                  )
                                : ChangeNotifierProvider<OutgoingMessageController>(
                                    create: (_) => OutgoingMessageController(message, controller),
                                    child: OutgoingMessage(),
                                  );
                          }).toList(),
                          SizedBox(
                            height: 18,
                          ),
                        ],
                      );
                    },
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

And whenever sending new message, should scroll to "top" of the list.

  void _sendMessage(dynamic content) {
    Rx.fromCallable(() => _messageService.sendMessage(content))
        .doOnError((error, stackTrace) => Stream.fromFuture(Future.value(null)))
        .listen((value) {
      if (value is Message) {
        _messages.add(value);

        this.listSink.add(_messages);
        this.scrollController.animateTo(
              0,
              duration: Duration(milliseconds: 250),
              curve: Curves.easeInOutCubic,
            );
      }
    });
  }

It works well with both text and image messages. Hope it's helpful for someone else.

chrisK
  • 148
  • 6
  • this is what i was looking for. the animate to max scroll extent is pretty janky if you want to immediately go to the bottom – テッド Feb 24 '22 at 16:08
0

You can reliably scroll to the bottom without reverse using the next code. Note: only Curves.linear looks good.

    Future.doWhile(() {
      if (scrollController.position.extentAfter == 0)
        return Future.value(false);
      return scrollController
          .animateTo(scrollController.position.maxScrollExtent,
              duration: Duration(milliseconds: 100), curve: Curves.linear)
          .then((value) => true);
    });
Taras Mazepa
  • 604
  • 8
  • 24