Flutter | Use Listview.builder inside ListView - Make whole page scrollable

6,405

Solution 1

The inner ListView is not the right widget for the job at hand. What you want to do is display multiple widgets above each other, which a Column is designed to do. So, instead of using the inner ListView, consider using a Column like this:

Column(
  children: snapshot.data.map((data) {
    return ListItem(
      data: data,
      user: _user,
    );
  }).toList(),
)

Note that I updated the ListItem widget to accept the concrete data part instead of a list and an index.

Now you may wonder if the Column is less performant than the ListView.builder, but it's not. That's because the ListView.builder itself didn't scroll, so it didn't layout its children lazily. Rather, the outer ListView would decide on whether all of the inner ListView should be displayed or not.

In the long run, you'll probably want to initialize your widgets lazily (especially if you have users with thousands of pictures). Here's a StackOverflow answer on how to do that with providing multiple Futures from a BLoC.

Solution 2

I would not personally use sized components for lists in flutter, only if you want a part of your layout to be scrollable in a specific area.

But in your case, I see that you want the entire page to be scrollable, so you have a starting point, from where you can design the rest of your page. (the entire page should be scrollable, so I understand that the top widget for this page should be a scrollable widget, like SingleChildScrollView, CustomScrollView, ListView etc.)

Now your approach is not wrong, but involves you knowing the final height of the SizedBox, which you can calculate based on your list. (too hard)

To solve your problem, I used a CustomScrollView with SliverList widgets.

return SafeArea(
        child: Scaffold(
            appBar: AppBar(),
            body: CustomScrollView(
              slivers: <Widget>[
                SliverList(
                  delegate: SliverChildListDelegate(
                    [
                      Padding(
                        padding: const EdgeInsets.only(left: 25.0, top: 30.0),
                        child: Text(displayName,
                            style: TextStyle(
                                color: Colors.black, fontWeight: FontWeight.bold, fontSize: 20.0)),
                      ),
                    ],
                  ),
                ),
                FutureBuilder(
                  future: ApiProvider.getPictures(),
                  builder: ((context, AsyncSnapshot snapshot) {
                    if (snapshot.hasData) {
                      if (snapshot.connectionState == ConnectionState.done) {
                        return SliverList(
                            delegate: SliverChildBuilderDelegate(
                                (context, index) => Container(
                                      margin: EdgeInsets.symmetric(vertical: 10),
                                      child: Column(
                                        crossAxisAlignment: CrossAxisAlignment.start,
                                        children: <Widget>[
                                          Text(
                                            displayName,
                                            style: TextStyle(
                                                fontSize: 17, fontWeight: FontWeight.bold),
                                          ),
                                          Text(snapshot.data[index].location),
                                          Image(
                                            image: NetworkImage(snapshot.data[index].pictureUrl),
                                            width: MediaQuery.of(context).size.width,
                                          )
                                        ],
                                      ),
                                    ),
                                childCount: snapshot.data.length));
                      } else {
                        return SliverFillRemaining(
                          child: Center(
                            child: CircularProgressIndicator(),
                          ),
                        );
                      }
                    } else {
                      return SliverFillRemaining(
                        child: Center(
                          child: CircularProgressIndicator(),
                        ),
                      );
                    }
                  }),
                ),
              ],
            )));

There are other solutions to your problem, but I believe that this is the best solution performance wise.

Feel free to ask for help if you need some.

Share:
6,405
Martin Seubert
Author by

Martin Seubert

Updated on December 11, 2022

Comments

  • Martin Seubert
    Martin Seubert over 1 year

    I want to build a instagram-like profile page, where all the posts of the current user are listed in a feed. Above that, some information about the user (avatar, follower count, post count, bio, etc.) should be displayed. The user should be able to scroll the whole page. At the moment the page can only be scrolled to the height of the sizedBox-widget in my ListView.builder. Just like that:

    enter image description here

    The build method of ProfilePage class looks like that:

    Widget build(BuildContext context) {
    
        return SafeArea(
          child: Scaffold(    
            appBar: AppBar(),
            body: ListView(    
              children: <Widget>[
               Padding(
                        padding: const EdgeInsets.only(left: 25.0, top: 30.0),
                        child: Text(_user.displayName,
                            style: TextStyle(
                                color: Colors.black,
                                fontWeight: FontWeight.bold,
                                fontSize: 20.0)),
                      ),
                      ...(all the other information for the "header-part")
    
                      postImagesWidget(),
    
                ])));}
    

    In postImagesWidget I'm using a ListView.builder to build the feed:

    Widget postImagesWidget(){
    return FutureBuilder(
                future: _future,
                builder:
                    ((context, AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
                  if (snapshot.hasData) {
                    if (snapshot.connectionState == ConnectionState.done) {
                      return SizedBox(
                        height: 600,
                          child: ListView.builder(
                              physics: const NeverScrollableScrollPhysics(),
    
                              //shrinkWrap: true,
                              itemCount: snapshot.data.length,
                              itemBuilder: ((context, index) => ListItem(
                                  list: snapshot.data,
                                  index: index,
                                  user: _user))));
                    } else {
                      return Center(
                        child: CircularProgressIndicator(),
                      );
                    }
                  } else {
                    return Center(
                      child: CircularProgressIndicator(),
                    );
                  }
                }),
              );
    
    }
    

    I had to add a sizedBox-Widget to avoid this error:

    Another exception was thrown: RenderBox was not laid out: RenderViewport#50ccf NEEDS-PAINT

    The description of this error says that I should add shrinkwrap to my ListView.builder, but this crashes my whole app after some scrolling with this error:

    Failed assertion: line 470 pos 12: 'child.hasSize': is not true.

    How can I make the whole page scrollable and not the two parts (header & feed) separately?

    I know that i has something to do with the fact, that I am using a ListView.builder inside a ListView...

    Best Regards.