flutterwidgetrebuild

DraggableScrollableSheet rebuilds all children every frame when dragged


I'm trying to implement a DraggableScrollableSheet with 2 nested scrollable lists.

I cannot use the out-of-the-box NestedScrollView class (I already tried it, but i couldn't get it to work, since it seems to be made for one preceding header sliver and only one scrollable list).

My approach with the CustomScrollView class generally seems to work, but it unfortunately brings up another problem: Everytime the user drags the sheet, all slivers get rebuild with every frame. Which is horribly slow.

I opened up a simple test case to exclude it's not the fault of any complex widget- / provider- / consumer- etc logic. Even with these simple widgets all slivers get rebuild once per frame...

Is it a bug in the framework or what am i doing wrong here?

Thank you very much...

    import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: DraggableScrollableActuator(child: MyHomePage(title: 'Flutter Demo Home Page')),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

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

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Tween<Offset> _tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: Duration(milliseconds: 300));
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Stack(children: [
      SlideTransition(
        position: _tween.animate(_controller),
        child: MyPopUpSheet(),
      ),
      Center(
        child: IconButton(
            icon: const Icon(Icons.info),
            onPressed: () {
              setState(() {
                if (_controller.isDismissed)
                  _controller.forward();
                else if (_controller.isCompleted) _controller.reverse().then((value) => _controller.reset());
              });
            }),
      ),
    ]));
  }
}

class MyPopUpSheet extends StatelessWidget {
  ScrollController _scrollController; //Needed for

  @override
  Widget build(BuildContext context) {
    print("MyPopUpSheet.build()");
    return Material(
      elevation: 4,
      shadowColor: Colors.orange,
      child: DraggableScrollableSheet(
        expand: true,
        initialChildSize: 0.4,
        minChildSize: 0.4,
        maxChildSize: 1.0,
        builder: (ctx, scrollController) {
          _scrollController = scrollController;
          return CustomScrollView(
            shrinkWrap: true,
            controller: _scrollController,
            slivers: [
              SliverToBoxAdapter(child: Test(Colors.deepOrange)),
              SliverToBoxAdapter(child: Test(Colors.red)),
              SliverToBoxAdapter(child: Test(Colors.green)),
              SliverToBoxAdapter(child: Test(Colors.purpleAccent)),
              SliverToBoxAdapter(child: Test(Colors.amberAccent))
            ],
          );
        },
      ),
    );
  }
}
    
 class Test extends StatelessWidget {
  final Color color;
  Test(this.color);
  
  @override
  Widget build(BuildContext context) {
    print("Test.build()");
    return Container(height: 100, color: color);
  }
}

Solution

  • I faced the same issue and found a workaround which doesn't stop the rebuilds but caches the child and fixes the scroll jank. What worked was, to store the widget inside the builder in a variable and return that variable from the builder.

    General Fix

    var child;
    return DraggableScrollableSheet(
      expand: true,
      initialChildSize: 0.4,
      minChildSize: 0.4,
      maxChildSize: 1.0,
      builder: (ctx, scrollController) {
        if(child == null) {
          child = SomeWidget();
        }
        return child;
      }
    );
    

    Fix specific to this problem

    var child;
    return DraggableScrollableSheet(
      expand: true,
      initialChildSize: 0.4,
      minChildSize: 0.4,
      maxChildSize: 1.0,
      builder: (ctx, scrollController) {
        if(child == null) {
          child = CustomScrollView(
            shrinkWrap: true,
            controller: _scrollController,
            slivers: [
              SliverToBoxAdapter(child: Test(Colors.deepOrange)),
              SliverToBoxAdapter(child: Test(Colors.red)),
              SliverToBoxAdapter(child: Test(Colors.green)),
              SliverToBoxAdapter(child: Test(Colors.purpleAccent)),
              SliverToBoxAdapter(child: Test(Colors.amberAccent))
            ],
          );
        }
        return child;
      }
    );