skip to Main Content

I am building an app where you can browse various products. Each product also has a product detail page. On that product detail page, I want to display the product image, the price, a product description and also a comment section.

Because the product description can be quite long, I don’t want to show the entire description right away to the user, but rather enable the user to be able to expand the description with a click on read more.

Below my product description is a comments section. The comments can expand to the top as well as to the bottom. The reason behind this is it is basically like a paginated site where you can load older comments and newer comments. I have solved this as described here.

However, because I have center key set, the product description does expand to the top when I click read more. That’s the behavior I want to change.

  1. I want my product description to expand to the bottom and push all widgets below down
  2. Older comments to expand to the top and push the all widgets above up
  3. Newer comments to expand to the bottom and push all widgets below down

At all time, the user should stay where he is on the screen. I do not want to use functions like jumpTo to jump to the top when the product description expands up. I want to solve this problem properly and have the product description expanding down. How can I archive that?

Secondly, I also want to archive that when a user opens the page it starts at the top, with the product image, like every other page. What do I need to do in order to archive that goal too?

Here you can find an example of my code. I have added two floating buttons which simulate comments being added to the top and to the bottom: https://dartpad.dev/?id=486578f48833dd3d53b1f76080ac6f23

I am grateful for any kind of help!

2

Answers


  1. To achieve the desired behavior where the product description expands downwards and pushes the content below it, and where the comments expand both upwards and downwards without jumping the user to the top of the page, you can follow these steps:

    Wrap your product description and comments section in a
    CustomScrollView with multiple SliverList widgets.

    Ensure that the product description is expanded using an
    AnimatedContainer widget or similar, and adjust its height accordingly
    to show the desired number of lines initially.

    Implement a state management solution to control the expansion state
    of the product description.

    Ensure that when expanding the product description downwards or the
    comments upwards/downwards, the scroll position of the
    CustomScrollView is maintained.

    Here’s how you can modify your code to achieve these goals:

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(MaterialApp(
        home: TestPage(),
      ));
    }
    
    class TestPage extends StatefulWidget {
      const TestPage({super.key});
    
      @override
      State<TestPage> createState() => _TestPageState();
    }
    
    class _TestPageState extends State<TestPage> {
      List<Widget> newList = List.generate(
        20,
        (index) => Text('Upper ${index.toString()}'),
      );
      List<Widget> myList = List.generate(
        20,
        (index) => Text('Lower ${index.toString()}'),
      );
    
      final scrollController = ScrollController();
      final keyTop = GlobalKey();
    
      @override
      void dispose() {
        scrollController.dispose();
        super.dispose();
      }
    
      bool _isExpanded = false;
    
      @override
      Widget build(BuildContext context) {
        String productDescription =
            "Peanut butter, a creamy concoction crafted from roasted peanuts, is a timeless culinary delight cherished by millions worldwide. Its rich, nutty flavor and smooth texture make it a versatile ingredient that transcends traditional boundaries, finding its way into sandwiches, desserts, sauces, and even savory dishes. This delectable spread has captured the hearts and palates of food enthusiasts for generations, evolving from a humble pantry staple to a beloved icon of gastronomy. In this exploration, we delve deep into the captivating world of peanut butter, uncovering its history, culinary uses, nutritional benefits, and enduring appeal. Peanut butter, a creamy concoction crafted from roasted peanuts, is a timeless culinary delight cherished by millions worldwide. Its rich, nutty flavor and smooth texture make it a versatile ingredient that transcends traditional boundaries, finding its way into sandwiches, desserts, sauces, and even savory dishes. This delectable spread has captured the hearts and palates of food enthusiasts for generations, evolving from a humble pantry staple to a beloved icon of gastronomy. In this exploration, we delve deep into the captivating world of peanut butter, uncovering its history, culinary uses, nutritional benefits, and enduring appeal.";
    
        return Scaffold(
          floatingActionButton: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              FloatingActionButton.extended(
                onPressed: () {
                  setState(() {
                    newList.add(Text('Upper ${newList.length}'));
                  });
                },
                label: const Text('Add to Upper'),
              ),
              const SizedBox(height: 10),
              FloatingActionButton.extended(
                onPressed: () {
                  setState(() {
                    myList.add(Text('Lower ${newList.length}'));
                  });
                },
                label: const Text('Add to Lower'),
              ),
            ],
          ),
          appBar: AppBar(),
          body: CustomScrollView(
            controller: scrollController,
            slivers: [
              SliverToBoxAdapter(
                key: keyTop,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Image.network(
                        "https://m.media-amazon.com/images/I/719NQBiqh9L.jpg"),
                    SizedBox(height: 16.0),
                    Text(
                      'Product Name',
                      style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                    ),
                    SizedBox(height: 8.0),
                    Text(
                      '$100', // Replace with actual price
                      style: TextStyle(fontSize: 16),
                    ),
                    SizedBox(height: 16.0),
                    AnimatedContainer(
                      duration: Duration(milliseconds: 500),
                      curve: Curves.easeInOut,
                      height: _isExpanded ? null : 85.0, // Initial height
                      child: Text(
                        productDescription,
                        style: TextStyle(fontSize: 16),
                        overflow: TextOverflow.fade,
                      ),
                    ),
                    SizedBox(height: 16.0),
                    GestureDetector(
                      onTap: () {
                        setState(() {
                          _isExpanded = !_isExpanded;
                        });
                      },
                      child: Text(
                        _isExpanded ? 'Read less' : 'Read more',
                        style: TextStyle(color: Colors.blue),
                      ),
                    ),
                    SizedBox(height: 16.0),
                  ],
                ),
              ),
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return ListTile(
                      title: newList[index],
                    );
                  },
                  childCount: newList.length,
                ),
              ),
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return ListTile(title: myList[index]);
                  },
                  childCount: myList.length,
                ),
              ),
            ],
          ),
        );
      }
    }
    
    Login or Signup to reply.
  2. I have completed the necessary work on your case and have devised a final solution.
    The current code utilizes the center key for the CustomScrollView, causing the upper half to move upwards and the lower half to move downwards. This conflicts with the requirement for the product description to expand downwards.
    Therefore, I propose removing the center key and implementing a custom ScrollPhysics class to maintain the position of the CustomScrollView.

    You must manually determine when to retain the scroll and when not to. The system cannot automatically recognize what is appropriate for your specific case. Please refer to the _shouldRetainScroll usage for more information.

    Also, with the removal of the center key, the page will now start at the top when a user opens it.

    Finally, the solution may not be perfect, but give it a go. I look forward to your response.

    Below is the implemented code:

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(MaterialApp(
        home: TestPage(),
      ));
    }
    
    class TestPage extends StatefulWidget {
      const TestPage({super.key});
    
      @override
      State<TestPage> createState() => _TestPageState();
    }
    
    class _TestPageState extends State<TestPage> {
      List<Widget> newList = List.generate(
        20,
        (index) => Text('Upper ${index.toString()}'),
      );
      List<Widget> myList = List.generate(
        20,
        (index) => Text('Lower ${index.toString()}'),
      );
    
      final Key centerKey = const ValueKey('second-sliver-list');
      final scrollController = ScrollController();
      final keyTop = GlobalKey();
      final ValueNotifier<bool> _shouldRetainScroll = ValueNotifier(false);
      @override
      void dispose() {
        scrollController.dispose();
        _shouldRetainScroll.dispose();
        super.dispose();
      }
    
      @override
      void initState() {
        super.initState();
      }
    
      bool _isExpanded = false;
    
      @override
      Widget build(BuildContext context) {
        String productDescription =
            "Peanut butter, a creamy concoction crafted from roasted peanuts, is a timeless culinary delight cherished by millions worldwide. Its rich, nutty flavor and smooth texture make it a versatile ingredient that transcends traditional boundaries, finding its way into sandwiches, desserts, sauces, and even savory dishes. This delectable spread has captured the hearts and palates of food enthusiasts for generations, evolving from a humble pantry staple to a beloved icon of gastronomy. In this exploration, we delve deep into the captivating world of peanut butter, uncovering its history, culinary uses, nutritional benefits, and enduring appeal. Peanut butter, a creamy concoction crafted from roasted peanuts, is a timeless culinary delight cherished by millions worldwide. Its rich, nutty flavor and smooth texture make it a versatile ingredient that transcends traditional boundaries, finding its way into sandwiches, desserts, sauces, and even savory dishes. This delectable spread has captured the hearts and palates of food enthusiasts for generations, evolving from a humble pantry staple to a beloved icon of gastronomy. In this exploration, we delve deep into the captivating world of peanut butter, uncovering its history, culinary uses, nutritional benefits, and enduring appeal.";
    
        return Scaffold(
          floatingActionButton: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              FloatingActionButton.extended(
                onPressed: () {
                  _shouldRetainScroll.value = true;
                  setState(() {
                    newList.add(Text('Upper ${newList.length}'));
                  });
                },
                label: const Text('Add to Upper'),
              ),
              const SizedBox(height: 10),
              FloatingActionButton.extended(
                onPressed: () {
                  _shouldRetainScroll.value = false;
                  scrollController.jumpTo(scrollController.position.pixels + 0.000000001); //Quite tricky here. Purpose is force the PositionRetainedScrollPhysics recalculate the position;
                  setState(() {
                    myList.add(Text('Lower ${myList.length}'));
                  });
                },
                label: const Text('Add to Lower'),
              ),
            ],
          ),
          appBar: AppBar(),
          body: CustomScrollView(
            //center: centerKey,
            physics: PositionRetainedScrollPhysics(shouldRetainScroll: _shouldRetainScroll),
            controller: scrollController,
            slivers: [
              SliverToBoxAdapter(
                key: keyTop,
                child: Column(
                  children: [
                    Image.network(
                        "https://m.media-amazon.com/images/I/719NQBiqh9L.jpg"),
                    SizedBox(height: 16.0),
                    Text(
                      'Product Name',
                      style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                    ),
                    SizedBox(height: 8.0),
                    Text(
                      '$100', // Replace with actual price
                      style: TextStyle(fontSize: 16),
                    ),
                    SizedBox(height: 16.0),
                    AnimatedContainer(
                      duration: Duration(milliseconds: 500),
                      curve: Curves.easeInOut,
                      height: _isExpanded
                          ? null
                          : 85.0, // Adjust height to show around 3-4 lines
                      child: Text(
                        productDescription,
                        style: TextStyle(fontSize: 16),
                        overflow: TextOverflow.fade,
                      ),
                    ),
                    SizedBox(height: 16.0),
                    GestureDetector(
                      onTap: () {
                        _shouldRetainScroll.value = false;
                        setState(() {
                          _isExpanded = !_isExpanded;
                        });
                      },
                      child: Text(
                        _isExpanded ? 'Read less' : 'Read more',
                        style: TextStyle(color: Colors.blue),
                      ),
                    ),
                  ],
                ),
              ),
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return ListTile(
                      title: newList.reversed.toList()[index],
                    );
                  },
                  childCount: newList.length,
                ),
              ),
              SliverList(
                //key: centerKey,
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return ListTile(title: myList[index]);
                  },
                  childCount: myList.length,
                ),
              ),
            ],
          ),
        );
      }
    }
    
    class PositionRetainedScrollPhysics extends ScrollPhysics {
      final ValueNotifier<bool> shouldRetainScroll;
      const PositionRetainedScrollPhysics({super.parent, required this.shouldRetainScroll});
    
      @override
      PositionRetainedScrollPhysics applyTo(ScrollPhysics? ancestor) {
        return PositionRetainedScrollPhysics(
          parent: buildParent(ancestor),
            shouldRetainScroll: shouldRetainScroll
        );
      }
    
      @override
      double adjustPositionForNewDimensions({
        required ScrollMetrics oldPosition,
        required ScrollMetrics newPosition,
        required bool isScrolling,
        required double velocity,
      }) {
        final position = super.adjustPositionForNewDimensions(
          oldPosition: oldPosition,
          newPosition: newPosition,
          isScrolling: isScrolling,
          velocity: velocity,
        );
    
        final diff = newPosition.maxScrollExtent - oldPosition.maxScrollExtent;
    
        if (oldPosition.pixels > oldPosition.minScrollExtent && diff > 0 && shouldRetainScroll.value == true) {
          return position + diff;
        } else {
          return position;
        }
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search