TatsuUkraine / flutter_sticky_infinite_list

Multi directional infinite list with Sticky headers for Flutter applications
BSD 2-Clause "Simplified" License
341 stars 31 forks source link

Simple example for quick start #21

Closed pkitatta closed 4 years ago

pkitatta commented 4 years ago

Hi, thank you for this plugin, it looks nice even though and struggling to quickly integrate it in my code.

Would it be possible to please give a real list example as could be used in an app?

Forexample below is my code using another sticky header plugin, how can I quickly implement your plugin in this flow.

Widget buildListMessage() {
    print('conversationId in the list wig is: $hasConversationId');
    print('private chat it: $privateChatId');
    return Flexible(
      child: privateChatId == ''
          ? Center(
              child: CircularProgressIndicator(
                  valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF180018))))
          : StreamBuilder(
              ///STREAM CONVARSATION WITH THIS USER
              stream: _messages,
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  //If there is no conversation of this is set hasConversation
                  hasConversationId = false;
                  return Center(
                      child: CircularProgressIndicator(
                          valueColor: AlwaysStoppedAnimation<Color>(
                              Color(0xFF180018))));
                } else {
                  listMessage = snapshot.data.documents;
                  if (snapshot.data.documents.length > 0)
                    //If there is no conversation of this is set hasConversation
                    hasConversationId = true;
                  print(
                      'conversationId inside list return: $hasConversationId');
                  var newMap = groupBy(
                      listMessage,
                      (obj) => DateFormat('y-MM-dd').format(
                          DateTime.fromMillisecondsSinceEpoch(
                              int.parse(obj['timestamp']))));

                  return CustomScrollView(
                      reverse: true,
                    slivers: newMap.entries
                        .map<Widget>((item) => buildDateItem(context, item))
                        .toList(),
                  );
                }
              },
            ),
    );
  }

  Widget buildDateItem(BuildContext context, item) {
    print('Map in listview: ${item.value}');
    return SliverStickyHeader(
      header: Center(
        child: Container(
          padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0),
          margin: EdgeInsets.only(top: 5.0, bottom: 5.0),
          decoration: BoxDecoration(
              color: Colors.black54,
              borderRadius: BorderRadius.all(Radius.circular(30))),
          child: Text(
            dateFx(item.key),
            style: const TextStyle(
              color: Colors.white,
            ),
            textAlign: TextAlign.center,
          ),
        ),
      ),
      sliver: SliverList(
        delegate: SliverChildBuilderDelegate(
          (BuildContext context, int index) => buildItem(context, index, item.value[index]),
          childCount: item.value.length,
        ),
      ),
    );
  }

Thanks.

TatsuUkraine commented 4 years ago

Hi) do you have visualization of what you want to achieve? Just wants to make sure that I understood your code sample in a right way

pkitatta commented 4 years ago

Yes I do

Intex_Aqua power M_Screenshot_20200214-154633

This one I actually did with sticky_header by slighfoot but as I read your comment in the issues there, the header is wobbly - not very cool. And also I need to implement scroll to position so I chose to look for another plugin. flutter_sticky_header the plugin I use for the above code example has its issues - the header is at the bottom of the list.

Widget buildListMessage() {
    print('conversationId in the list wig is: $hasConversationId');
    print('private chat it: $privateChatId');
    return Flexible(
      child: privateChatId == ''
          ? Center(
              child: CircularProgressIndicator(
                  valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF180018))))
          : StreamBuilder(
              ///STREAM CONVARSATION WITH THIS USER
              stream: _messages,
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  //If there is no conversation of this is set hasConversation
                  hasConversationId = false;
                  return Center(
                      child: CircularProgressIndicator(
                          valueColor: AlwaysStoppedAnimation<Color>(
                              Color(0xFF180018))));
                } else {
                  listMessage = snapshot.data.documents;
                  if (snapshot.data.documents.length > 0)
                    //If there is no conversation of this is set hasConversation
                    hasConversationId = true;
                  print(
                      'conversationId inside list return: $hasConversationId');
                  var newMap = groupBy(
                      listMessage,
                      (obj) => DateFormat('y-MM-dd').format(
                          DateTime.fromMillisecondsSinceEpoch(
                              int.parse(obj['timestamp']))));
//                  print(newMap);
//                  for (var obj in newMap.entries) {
//                    print(obj.key);
//                    for (var value in obj.value) print(value.toString());
//                  }

                  return ListView(
                    padding: EdgeInsets.only(top: 0),
                    children: newMap.entries
                        .map<Widget>((item) => buildDateItem(context, item))
                        .toList(),
                    reverse: true,
                    controller: listScrollController,
                  );
              },
            ),
    );
  }

  Widget buildDateItem(BuildContext context, item) {
    print('Map in listview: ${item.value}');
    return StickyHeader(
      header: Center(
        child: Container(
          padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0),
          margin: EdgeInsets.only(top: 5.0, bottom: 5.0),
          decoration: BoxDecoration(
              color: Colors.black54,
              borderRadius: BorderRadius.all(Radius.circular(30))),
          child: Text(
            dateFx(item.key),
            style: const TextStyle(
              color: Colors.white,
            ),
            textAlign: TextAlign.center,
          ),
        ),
      ),
      content: Column(
        children: item.value.reversed
            .map<Widget>((item) => buildItem(context, item))
            .toList(),
      ),
    );
  }

This is the implementation for sticky_header

TatsuUkraine commented 4 years ago

as I understood - only days should be sticked to the bottom? First thought was that you also need client photo to be sticked too with dates)

TatsuUkraine commented 4 years ago

I will provide some code samples for your case in a few hours if it's ok?

pkitatta commented 4 years ago

as I understood - only days should be sticked to the bottom? First thought was that you also need client photo to be sticked too with dates)

No date sticks at the top. In the photo I didn't scroll to the bottom. I know the design is confusing but it's because am using a Stack widget for the body with AppBar positioned at the top. Here is a better photo.

Intex_Aqua power M_Screenshot_20200214-163715

pkitatta commented 4 years ago

I will provide some code samples for your case in a few hours if it's ok?

That very OK thanks.

TatsuUkraine commented 4 years ago

ok, so here is a very raw example

Widget _buildChat(data) {
  return InfiniteList(
    anchor: 1,
    direction: InfiniteListDirection.multi,
    minChildCount: data.length * -1, /// your total days count with minus,
    maxChildCount: 0,
    builder: (context, index) {
      /// consider that index will be negative only, starting from -1
      final messageIndex = (index + 1) * -1; /// if index == -1 - result will be 0, -2 -> 1 and etc.
      /// get needed data from `data` by `messageIndex`

      return InfiniteListItem(
        headerAlignment: HeaderAlignment.topCenter,
        headerBuilder: (context) => /// you header,
        contentBuilder: (context) => Container(
          margin: const EdgeInsets.top( /// put your header height here ),
          child: Column(
            children: [
              /// your messages goes here
            ],
          ),
        ),
        minOffsetProvider: (state) => /// your header height,
      );
    },
  );
}

But take into account that for your particular case probably none of the sticky header packages won't help you to solve the probable performance issue (among packages that I'm aware at least). Day block will be rendered on-demand, but the case here that you quite likely will have tons and tons of messages for one particular day, which means that all packages will require to render all of them, that regular ListView is trying to avoid by rendering each scroll item on demand

TatsuUkraine commented 4 years ago

btw, header size is needed to define offset for content (same as padding around content). For now headers always overlay content (#19)

TatsuUkraine commented 4 years ago

like an option to solve day item data (with huge amount of data), use headerStateBuilder. It will be invoked each time when sticky header changes it's position (https://github.com/TatsuUkraine/flutter_sticky_infinite_list#state). So when it's about to get to the end edge - you can rebuild content with adding there more item for render. But again, it's more like a workaround, I would probably build own solution for your particular case)

pkitatta commented 4 years ago

@TatsuUkraine thanks a for the honesty, insight, and example code. The sample code might do another person good as well so I think it would not hurt to put it in your documentation.

You are probably right about the performance, I might need to build my own and use this plugin in another part of the app. Thanks for the help.

TatsuUkraine commented 4 years ago

What problem do you have with SliverStickyHeader? Also I curious if it renders each perticular message on demand or whole day block at once?

TatsuUkraine commented 4 years ago

@pkitatta if it renders each message on demand, and problem just in header positioning, I think I know one workaround how you can fix it

TatsuUkraine commented 4 years ago

The sample code might do another person good as well so I think it would not hurt to put it in your documentation.

I have not exact same, but similar code example here https://github.com/TatsuUkraine/flutter_sticky_infinite_list/blob/master/README.md#reverse-infinite-scroll

pkitatta commented 4 years ago

@pkitatta if it renders each message on demand, and problem just in header positioning, I think I know one workaround how you can fix it

@TatsuUkraine SliverStickyHeader is great. With its lack of support for reversed list as its main problem. It renders on demand as it uses SliverList and SliverChildBuilderDelegate for the list content. If you can do something about the header at the bottom then please please please you are most welcome because will save me some time to try and build my own animations for the header.

TatsuUkraine commented 4 years ago

@pkitatta I honestly tried the workaround, that I was thinking could solve an issue, but it still pinned header to the bottom(

and now I recall why I decided to build my own package, I had similar problems in addition to the fact that it can't be used within multi-directional infinite list)

I looked at the source code of the sliver sticky header package and found that it always refers to the scroll starting position. So like an option, you can fork it and just change the way how it calculates position for the header, which I believe happens here https://github.com/letsar/flutter_sticky_header/blob/master/lib/src/rendering/sliver_sticky_header.dart#L216

pkitatta commented 4 years ago

Yeah, am not yet advanced to fork and change things. I don't even know where to start. I have used flutter for three months but while migrating my project from ionic with a deadline. So it's been a crash course for me.

For now and doing things the simple way. This is my starting point: https://flutter.dev/docs/cookbook/lists/mixed-list

I have change my code as you can see

Widget buildListMessage() {
    print('conversationId in the list wig is: $hasConversationId');
    print('private chat it: $privateChatId');
    return Flexible(
      child: privateChatId == ''
          ? Center(
              child: CircularProgressIndicator(
                  valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF180018))))
          : StreamBuilder(
              ///STREAM CONVARSATION WITH THIS USER
              stream: _messages,
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  //If there is no conversation of this is set hasConversation
                  hasConversationId = false;
                  return Center(
                      child: CircularProgressIndicator(
                          valueColor: AlwaysStoppedAnimation<Color>(
                              Color(0xFF180018))));
                } else {
//                  listMessage = snapshot.data.documents;
                  if (snapshot.data.documents.length > 0)
                    //If there is no conversation of this is set hasConversation
                    hasConversationId = true;
                  print(
                      'conversationId inside list return: $hasConversationId');

                  //Group the messages from the database by date
                  var newMap = groupBy(
                      snapshot.data.documents,
                      (obj) => obj['date']);
                  print(newMap);

                  listMessage = [];

                  //Make the grouped data into one flat list
                  for (var obj in newMap.entries){

                    //Load the list with a group of messages
                    for(var value in obj.value){
                      listMessage.add(value);
                    }

                    //Add date header after adding child messages above
                    listMessage.add({'messageHeading': obj.key});
                  }

                  return ListView.builder(
                    padding: EdgeInsets.all(10.0),
                    itemBuilder: (context, index) =>
                        buildDateItem(context, listMessage[index]),
                    itemCount: snapshot.data.documents.length,
                    reverse: true,
                    controller: listScrollController,
                  );
                }
              },
            ),
    );
  }

  Widget buildDateItem(BuildContext context, item) {
    print('date: ${item}');
    if(item['messageHeading'] != null){
      print('date: ${item['messageHeading']}');
      return Center(
        child: Container(
          padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0),
          margin: EdgeInsets.only(top: 5.0, bottom: 5.0),
          decoration: BoxDecoration(
              color: Colors.black54,
              borderRadius: BorderRadius.all(Radius.circular(30))),
          child: Text(
            dateFx(item['messageHeading'].toString()),
            style: const TextStyle(
              color: Colors.white,
            ),
            textAlign: TextAlign.center,
          ),
        ),
      );
    }else{
      return InkWell(
        onTap: longPressed
            ? () {
          print('in onTap fx');
          //Check if already selected if true remove and if last item reset longPress param
          if (_selectedMessages.contains(item)) {
            print('in onTap fx, if statement');
            setState(() {
              _selectedMessages.remove(item);
            });
            print('selectedList lenght ${_selectedMessages.length}');
            if (_selectedMessages.length == 0) {
              setState(() {
                longPressed = false;
                isSelected = false;
              });
            }
          } else {
            setState(() {
              _selectedMessages.add(item);
            });
            print('in onTap fx, else statement');
            print('selectedList lenght ${_selectedMessages.length}');
          }
        }
            : () {
          print('mere tap');
        },
        onLongPress: !longPressed
            ? () {
          print('in onLongTap fx');
          Feedback.forLongPress(context);
          setState(() {
            longPressed = true;
            isSelected = true;
            _selectedMessages.add(item);
          });
          print('selectedList lenght ${_selectedMessages.length}');
        }
            : null,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Container(
              color: _selectedMessages.contains(item) ? Colors.white24 : null,
              child: item['type'] == 0
                  ? bubble(
                item,
                TextSpan(text: item['content']),
                DateFormat('kk:mm').format(
                    DateTime.fromMillisecondsSinceEpoch(
                        int.parse(item['timestamp']))),
                true,
                item['idFrom'] == id ? true : false,
              )
                  : item['type'] == 1
                  ? replyBubble(
                item,
                TextSpan(text: item['content']),
                DateFormat('kk:mm').format(
                    DateTime.fromMillisecondsSinceEpoch(
                        int.parse(item['timestamp']))),
                true,
                item['idFrom'] == id ? true : false,
              )
                  : item['type'] == 3
                  ? billBubble(
                item,
                TextSpan(text: item['content']),
                DateFormat('kk:mm').format(
                    DateTime.fromMillisecondsSinceEpoch(
                        int.parse(item['timestamp']))),
                true,
                item['idFrom'] == id ? true : false,
              )
                  : Container(),
            )
          ],
        ),
      );
    }
  }

Instead for looping through a grouped list of lists I create one flat list and then use the flutter example.

Now all I need is to get a container to animate as a sticky header - this is where my challenge is going to be.

TatsuUkraine commented 4 years ago

Ok. As I said before, you PROBABLY will have performance issue if you have tons and tons of messages per day. So you can try to use code sample that I put before and measure fps in dev and prod builds with big amount of data. Flutter is quite fast with rendering, so quite possibly that you won't have any problems, but I would double check) if so, you can simply use my package and if you get any performance problems, you always can get back to your latest variant)

TatsuUkraine commented 4 years ago

Also I would recommend you to split your code into smaller widgets, in that way flutter will try to optimize any rebuild process to avoid building widgets that wasn't changed during rebuild https://flutter.dev/docs/perf/rendering/best-practices#controlling-build-cost

pkitatta commented 4 years ago

@pkitatta I honestly tried the workaround, that I was thinking could solve an issue, but it still pinned header to the bottom(

and now I recall why I decided to build my own package, I had similar problems in addition to the fact that it can't be used within multi-directional infinite list)

I looked at the source code of the sliver sticky header package and found that it always refers to the scroll starting position. So like an option, you can fork it and just change the way how it calculates position for the header, which I believe happens here https://github.com/letsar/flutter_sticky_header/blob/master/lib/src/rendering/sliver_sticky_header.dart#L216

@TatsuUkraine I finally got the solution. I reversed the list at the point I was grouping the data

var newMap = groupBy(
                      listMessage.reversed, <------------------------------ at this point
                      (obj) => DateFormat('y-MM-dd').format(
                          DateTime.fromMillisecondsSinceEpoch(
                              int.parse(obj['timestamp']))));

Now the header is no longer at the bottom.

TatsuUkraine commented 4 years ago

@pkitatta I can assume you using forward direction scroll list also?

pkitatta commented 4 years ago

@pkitatta I can assume you using forward direction scroll list also?

Everything is OK. I can scroll up and down the list. I have just run into one problem through: Because the headers and its messages are grouped in slivers, each sliver has its own message indexing that starts from 0. This is something I didn't face with sticky header and am assuming your plugin doesn't have this problem. Unified indexing for all the children or messages in the list is needed so that I can use it to say, when I click on the reply message the list should be a to scroll to the position to the original message.

TatsuUkraine commented 4 years ago

@pkitatta I can assume you using forward direction scroll list also?

Everything is OK. I can scroll up and down the list. I have just run into one problem through: Because the headers and its messages are grouped in slivers, each sliver has its own message indexing that starts from 0. This is something I didn't face with sticky header and am assuming your plugin doesn't have this problem. Unified indexing for all the children or messages in the list is needed so that I can use it to say, when I click on the reply message the list should be a to scroll to the position to the original message.

I was asking about scroll type, because if you using NOT reversed scroll you can run into an issue with it's position during dynamic data fetch, because starting point of such scroll is placed at the top rather than at the bottom. Any new messages, that will be placed at the end of your list, won't scroll your content to the bottom edge. Any old massages placed at the top during scroll up and lazy data load will shift your content on new messages size

pkitatta commented 4 years ago

I get what you are saying.

TatsuUkraine commented 4 years ago

if any help is needed, feel free to open new issue