Limbou / expandable_page_view

MIT License
88 stars 36 forks source link

Strange, Slow Behavior with AnimatedContainer/SizeTransition After Changing a Page #55

Closed MeatCheatDev closed 1 year ago

MeatCheatDev commented 1 year ago

I've noticed some unusual behavior with your package. This can be reproduced by trying out my code here:


import 'package:expandable_page_view/expandable_page_view.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late PageController _pageController;
  late int _previousPage;
  bool _expanded = false;

  double _containerHeight = 100;

  @override
  void initState() {
    super.initState();
    final today = DateTime.now();
    _pageController = PageController(
        initialPage: 400 - today.difference(DateTime(today.year, 1, 1)).inDays);
    _previousPage = _pageController.initialPage;
  }

  @override
  void dispose() {
    super.dispose();
    _pageController.dispose();
  }

  void _toggleExpanded() {
    setState(() {
      _expanded = !_expanded;
      _containerHeight = _expanded ? 300 : 100;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(toolbarHeight: 0,),
      body: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            ExpandablePageView.builder(
              controller: _pageController,
              itemCount: 400,
              onPageChanged: (value) {
              },
              itemBuilder: (context, index) {
                return Padding(
                  padding: EdgeInsets.all(16),
                  child: Container(
                    decoration: BoxDecoration(
                      color: Theme.of(context).colorScheme.secondary,
                      borderRadius: BorderRadius.circular(22),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withAlpha(50),
                          spreadRadius: 5,
                          blurRadius: 7,
                          offset: const Offset(0, 3),
                        ),
                      ],
                    ),
                    child: Column(
                      children: [
                        AnimatedContainer(
                          height: _containerHeight,
                          duration: Duration(milliseconds: 500),
                          child: Column(
                            children: [
                              Row(
                                children: [
                                  IconButton(
                                      onPressed: () {}, icon: Icon(Icons.more_horiz)),
                                  Spacer(),
                                  Align(
                                      alignment: Alignment.centerRight,
                                      child: Text('Date')),
                                ],
                              ),
                              Text("Frühstück:"),
                              Text("Mittagessen:"),
                              Text("Abendessen:"),
                            ],
                          ),
                        ),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            IconButton(
                              onPressed: _toggleExpanded,
                              icon: Icon(
                                !_expanded ? Icons.arrow_drop_down : Icons.arrow_drop_up,
                              ),
                              padding: EdgeInsets.zero,
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

, or simply by using an ExpandablePageView and then applying a SizeTransition or AnimatedContainer. The behavior can be seen in the video, but can also be described as laggy or not smooth. Interestingly, this behavior only occurs after I have switched a page, and it disappears again after the dispose() method has been called.

https://github.com/Limbou/expandable_page_view/assets/24623409/4c09a652-dae0-46d5-9672-aabd47157d0d

JohnF17 commented 1 year ago

Hi @MeatCheatDev, I solved this issue by separating the ExpandedPageView into its own separate Stateful widget, cheers👍

MeatCheatDev commented 1 year ago

@JohnF17

You mean like this?:

Doesn't work for me, same issue

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(toolbarHeight: 0,),
      body: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [ExpandedStfl(),
            Text("data")
          ],
        ),
      ),
    );
  }
}

class ExpandedStfl extends StatefulWidget {
  const ExpandedStfl({super.key});

  @override
  State<ExpandedStfl> createState() => _ExpandedStflState();
}

class _ExpandedStflState extends State<ExpandedStfl> {
  late PageController _pageController;
  late int _previousPage;
  bool _expanded = false;

  double _containerHeight = 100;

  @override
  void initState() {
    super.initState();
    final today = DateTime.now();
    _pageController = PageController(
        initialPage: 400 - today.difference(DateTime(today.year, 1, 1)).inDays);
    _previousPage = _pageController.initialPage;
  }

  @override
  void dispose() {
    super.dispose();
    _pageController.dispose();
  }

  void _toggleExpanded() {
    setState(() {
      _expanded = !_expanded;
      _containerHeight = _expanded ? 300 : 100;
    });
  }

  @override
  Widget build(BuildContext context) {
    return             ExpandablePageView.builder(
      controller: _pageController,
      itemCount: 400,
      onPageChanged: (value) {
      },
      itemBuilder: (context, index) {
        return Padding(
          padding: EdgeInsets.all(16),
          child: Container(
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.secondary,
              borderRadius: BorderRadius.circular(22),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withAlpha(50),
                  spreadRadius: 5,
                  blurRadius: 7,
                  offset: const Offset(0, 3),
                ),
              ],
            ),
            child: Column(
              children: [
                AnimatedContainer(
                  height: _containerHeight,
                  duration: Duration(milliseconds: 500),
                  child: Column(
                    children: [
                      Row(
                        children: [
                          IconButton(
                              onPressed: () {}, icon: Icon(Icons.more_horiz)),
                          Spacer(),
                          Align(
                              alignment: Alignment.centerRight,
                              child: Text('Date')),
                        ],
                      ),
                      Text("Frühstück:"),
                      Text("Mittagessen:"),
                      Text("Abendessen:"),
                    ],
                  ),
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    IconButton(
                      onPressed: _toggleExpanded,
                      icon: Icon(
                        !_expanded ? Icons.arrow_drop_down : Icons.arrow_drop_up,
                      ),
                      padding: EdgeInsets.zero,
                    ),
                  ],
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}
JohnF17 commented 1 year ago

@MeatCheatDev, hmm should've had a major improvement there, but regardless if your itemCount is lot higher, always use a separate stateful widget for it.

I can see the main issue is right here; calling setState per button click that causes the entire widget tree to rebuild

  void _toggleExpanded() {
    setState(() {
      _expanded = !_expanded;
      _containerHeight = _expanded ? 300 : 100;
    });
  }

This is very performance heavy, so here's a solution that'll guarantee a smooth result for ya, all solutions step by step

  1. Put the ExpandablePageView.builder into its own stateful widget so that it can handle its own updates (page changes only)
  2. Set the animationDuration parameter of the builder to something like 400.ms (that's 4 times the default value, it'll help immensely and guarantees a huge leap in performance)
  3. Take the content of the ExpandablePageView.builder out of the builder and put it into a separate stateful Widget to maintain its own state like the toggleExpanded method.

I can promise you, you will see great performance bumps after these changes. 👍 Also please keep in mind that the app functions quite fast when on profile mode so try to see if there are improvements there.

MeatCheatDev commented 1 year ago

@JohnF17 Tried everything. I mean, when you copy past the hole code in your project, does it work? New Code:

import 'package:expandable_page_view/expandable_page_view.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(toolbarHeight: 0,),
      body: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [ExpandedStfl(),
            Text("data")
          ],
        ),
      ),
    );
  }
}

class ExpandedStfl extends StatefulWidget {
  const ExpandedStfl({super.key});

  @override
  State<ExpandedStfl> createState() => _ExpandedStflState();
}

class _ExpandedStflState extends State<ExpandedStfl> {
  late PageController _pageController;
  late int _previousPage;
  bool _expanded = false;

  double _containerHeight = 100;

  @override
  void initState() {
    super.initState();
    final today = DateTime.now();
    _pageController = PageController(
        initialPage: 400 - today.difference(DateTime(today.year, 1, 1)).inDays);
    _previousPage = _pageController.initialPage;
  }

  @override
  void dispose() {
    super.dispose();
    _pageController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ExpandablePageView.builder(
      animationDuration: Duration(milliseconds: 400),
      controller: _pageController,
      itemCount: 50,
      onPageChanged: (value) {
      },
      itemBuilder: (context, index) {
        return ContentExpandable();
      },
    );
  }
}

class ContentExpandable extends StatefulWidget {
  const ContentExpandable({super.key});

  @override
  State<ContentExpandable> createState() => _ContentExpandableState();
}

class _ContentExpandableState extends State<ContentExpandable> {
  bool _expanded = false;
  double _containerHeight = 100;

  void _toggleExpanded() {
    setState(() {
      _expanded = !_expanded;
      _containerHeight = _expanded ? 300 : 100;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(16),
      child: Container(
        decoration: BoxDecoration(
          color: Theme.of(context).colorScheme.secondary,
          borderRadius: BorderRadius.circular(22),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withAlpha(50),
              spreadRadius: 5,
              blurRadius: 7,
              offset: const Offset(0, 3),
            ),
          ],
        ),
        child: Column(
          children: [
            AnimatedContainer(
              height: _containerHeight,
              duration: Duration(milliseconds: 500),
              child: Column(
                children: [
                  Row(
                    children: [
                      IconButton(
                          onPressed: () {}, icon: Icon(Icons.more_horiz)),
                      Spacer(),
                      Align(
                          alignment: Alignment.centerRight,
                          child: Text('Date')),
                    ],
                  ),
                  Text("Frühstück:"),
                  Text("Mittagessen:"),
                  Text("Abendessen:"),
                ],
              ),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                IconButton(
                  onPressed: _toggleExpanded,
                  icon: Icon(
                    !_expanded ? Icons.arrow_drop_down : Icons.arrow_drop_up,
                  ),
                  padding: EdgeInsets.zero,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
JohnF17 commented 1 year ago

Fixed your code for you @MeatCheatDev, It's not the package's issue nor the AnimatedContainer but rather the padding widget that was above the container holding it, because it has to push the padding as well when animating from the child. (Edit: it was just the animationDuration), its all fixed now, but remember keep these changes and keep using the methods I mentioned for you.

Additional tip: If at some point, you do decide to put these inside a listview or a listview.builder (recommended), remember to give it a higher cacheExtent like the (number of items * 5 or more) to improve performance but don't multiply it by a much higher value as that will take up much memory. Another tip: If you are using VsCode, add the extension ErrorLens this will help you with performance recommendations like where to use const, shows you unused variables etc

Here's your code:


import 'package:expandable_page_view/expandable_page_view.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        toolbarHeight: 0,
      ),
      body: const SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [ExpandedStfl(), Text("data")],
        ),
      ),
    );
  }
}

class ExpandedStfl extends StatefulWidget {
  const ExpandedStfl({super.key});

  @override
  State<ExpandedStfl> createState() => _ExpandedStflState();
}

class _ExpandedStflState extends State<ExpandedStfl> {
  late PageController _pageController;
  late int _previousPage;

  @override
  void initState() {
    super.initState();
    final today = DateTime.now();
    _pageController = PageController(
        initialPage: 400 - today.difference(DateTime(today.year, 1, 1)).inDays);
    _previousPage = _pageController.initialPage;
  }

  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ExpandablePageView.builder(
      animationDuration: const Duration(milliseconds: 400),
      controller: _pageController,
      itemCount: 400,
      onPageChanged: (value) {},
      itemBuilder: (context, index) {
        return const ContentExpandable();
      },
    );
  }
}

class ContentExpandable extends StatefulWidget {
  const ContentExpandable({super.key});

  @override
  State<ContentExpandable> createState() => _ContentExpandableState();
}

class _ContentExpandableState extends State<ContentExpandable> {
  bool _expanded = false;
  double _containerHeight = 100;

  void _toggleExpanded() {
    setState(() {
      _expanded = !_expanded;
      _containerHeight = _expanded ? 300 : 100;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      margin: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.secondary,
        borderRadius: BorderRadius.circular(22),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withAlpha(50),
            spreadRadius: 5,
            blurRadius: 7,
            offset: const Offset(0, 3),
          ),
        ],
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          AnimatedContainer(
            height: _containerHeight,
            duration: const Duration(milliseconds: 200),
            child: Column(
              children: [
                Row(
                  children: [
                    IconButton(
                        onPressed: () {}, icon: const Icon(Icons.more_horiz)),
                    const Spacer(),
                    const Align(
                        alignment: Alignment.centerRight, child: Text('Date')),
                  ],
                ),
                const Text("Frühstück:"),
                const Text("Mittagessen:"),
                const Text("Abendessen:"),
              ],
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              IconButton(
                onPressed: _toggleExpanded,
                icon: Icon(
                  !_expanded ? Icons.arrow_drop_down : Icons.arrow_drop_up,
                ),
                padding: EdgeInsets.zero,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

I hope this is the result you wanted, cheers 👍

MeatCheatDev commented 1 year ago

https://github.com/Limbou/expandable_page_view/assets/24623409/ee650ed0-4bcc-42a0-a48c-77370dc17894

@JohnF17 Still same issue. Perhaps we have talked past each other. Its when you switch the Page and expanded it directly after. You see the "data" text? Without changing the page its smooth and synced. After switching pages its async, too slow, not smooth.

JohnF17 commented 1 year ago

Oh i see, So the issue is when you switch pages, let me have a look.

JohnF17 commented 1 year ago

Ha lol, @MeatCheatDev , i solved your issue, now i truly mean it 😅 Set the animation duration of the expandable page view to Duration.zero.

You see since the animatFirstPage is false by default, the duration is set to Duration.Zero according to the implementation.

  Duration _getDuration() {
    if (_firstPageLoaded) {
      return widget.animationDuration;
    }
    return widget.animateFirstPage ? widget.animationDuration : Duration.zero;
  }

That's why the first page was smooth because it had no duration, the others however receive the provided animationDuration. Took us a minute to align our ideas lol, but at the end of the day, its fixed, cheers 👍

MeatCheatDev commented 1 year ago

FINALLY, Thank you my man! <3 Appreciate your work.

JohnF17 commented 1 year ago

I'm glad it all worked out great for ya 👍🙂

Tip: If you want the content widget to keep its entire state like opened/closed or other variables inside it even after it has been swiped, just add the automaticKeepAliveClientMixin to its state (but not recommended for many pages).

class _ContentExpandableState extends State<ContentExpandable>
    with AutomaticKeepAliveClientMixin{

    // Needs to be overriden
      @override
       bool get wantKeepAlive => true;  // you can also return true if .... or false if  ....

    @override
  Widget build(BuildContext context) {
    super.build(context);  // Has to be the first line

    ......
    }

Seems like a closed issue, cheers 🎊

p.s What screen recording software do you use?

MeatCheatDev commented 1 year ago

@JohnF17 Open broadcast Software (OBS) ;-) Yes, I'll close it now as solved. Thanks!