DraggableScrollableSheet rebuilds all children every frame when dragged
Asked Answered
D

3

6

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);
  }
}
Dedra answered 28/9, 2020 at 12:0 Comment(5)
same problem here. found any solutions yet?Vesica
Unfortunately not :/ If it's sufficient for your needs, just use NestedScrollview instead of CustomScrollview. The latter doesn't produce any unnecessary rebuilds.Dedra
I lodged an issue with Flutter and it turned out to be a bug. github.com/flutter/flutter/issues/67219Vesica
Cool. Let's see what comes around there..Dedra
Anyone found a solution in the meantime?Equivalency
J
3

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;
  }
);
Jigging answered 6/5, 2021 at 15:37 Comment(1)
what if child widget contains async operations? I tried the solution above but the variable child stores a widget during loading.Saltpeter
P
0

This is how I solved my problem.

I set the enableDrag property of showBottomSheet to false.

 return showBottomSheet(
      enableDrag: false,
      builder: (context) {
        return DraggableSheetWidget()
      });
Percussion answered 26/7, 2023 at 0:17 Comment(0)
M
-1

I didn't found a good solution so I used sliding up panel package.

Mathilda answered 7/1, 2021 at 15:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.