Skip to content

Creating a simple ScrollView that supports any widget #97015

Closed

Description

@jamesblasco

Quick example:

ScrollView(
  children: [
    SliverAppBar(title: Text('Hello')),
    Container(color: Colors.grey[400], height: 100),
    Gap(20),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(
          title: Text('Item $index'),
        ),
        childCount: 4,
      ),
    ),
    Divider(),
    Align(
      alignment: Alignment.center,
      child: Text('This is a text'),
    ),
    Divider(),
 ],
);

Proposal

I personally love slivers, and when building scrollable experiences, CustomScrollView has always been my way to go. But still, I consider that this widget is a great barrier for a lot of devs starting in Flutter. They don't understand the word sliver (as it is not present in other similar frameworks) and they tend to go for what they already are familiar with: Column and ListViews to then find that it does not fit their needs.

I think there is quite a big space of improvement here where we could simplify the way to build these interfaces and make them more friendly and familiar to the Flutter community.

My proposal is to create a widget called ScrollView that would allow adding any widget as its children. This is would be ideal and the big idea. From the developer perspective, makes sense right? A scrollable widget (vertical/horizontal) where we can add widgets one after other and it can be scrolled. Notice this component is available in most of the UI SDKs (Android, JetpackCompose, UIKit, SwiftUI..) and would work as people expect it to work

How can we achieve that with all the powerfulness we already have in the flutter framework?

  1. First we should remain the current abstract ScrollView to something like ScrollableView, RawScrollView. And use the familiar word 'ScrollView' to be used in every project, just like Text uses RichTextor TextButton uses TextStyleButton. But the simple and familiar name is exposed to the developers.

  2. Then we should inject a RenderSliverToBoxAdapter before any render object that is a RenderBox type. (We should only support children of the type RenderSliver, and RenderSliverToBoxAdapter(child: RenderBox). Any other custom render object is out of the scope here).

This can be done in different parts of the framework. I would suggest it to do in the insertRenderObjectChild/moveRenderObjectChild/removeRenderObjectChild of the _ViewportElement.

Example implementation

This showcase an example of injecting a RenderSliverToBoxAdapter by using an extra widget

import 'package:flutter/material.dart' hide ScrollView;
import 'package:flutter/rendering.dart';

class ScrollView extends CustomScrollView {
  const ScrollView({Key? key, required List<Widget> slivers})
      : super(key: key, slivers: slivers);

  @override
  List<Widget> buildSlivers(BuildContext context) {
    return super
        .buildSlivers(context)
        .map((child) => Sliver(child: child))
        .toList();
  }
}

class Sliver extends SingleChildRenderObjectWidget {
  Sliver({Key? key, required this.child}) : super(key: key);

  final Widget child;

  @override
  _RenderProxySliver createRenderObject(BuildContext context) {
    return _RenderProxySliver();
  }

  @override
  SingleChildRenderObjectElement createElement() {
    return _SingleChildRenderObjectElement(this);
  }
}

class _RenderProxySliver extends RenderProxySliver {}

class _SingleChildRenderObjectElement extends SingleChildRenderObjectElement {
  _SingleChildRenderObjectElement(SingleChildRenderObjectWidget widget)
      : super(widget);

  RenderSliverToBoxAdapter? _adapter;

  @override
  void insertRenderObjectChild(RenderObject child, Object? slot) {
    final RenderObjectWithChildMixin<RenderObject> renderObject =
        this.renderObject as RenderObjectWithChildMixin<RenderObject>;
    assert(slot == null);
    final RenderObject proxyChild;
    if (child is RenderBox) {
      _adapter ??= RenderSliverToBoxAdapter();
      _adapter!.child = child;
      proxyChild = _adapter!;
    } else {
      proxyChild = child;
      assert(renderObject.debugValidateChild(child));
    }
    super.insertRenderObjectChild(proxyChild, slot);
  }

  @override
  void removeRenderObjectChild(RenderObject child, Object? slot) {
    final RenderObjectWithChildMixin<RenderObject> renderObject =
        this.renderObject as RenderObjectWithChildMixin<RenderObject>;
    assert(slot == null);
    assert(() {
      final RenderObject proxyChild;
      if (child is RenderBox) {
        assert(_adapter != null);
        proxyChild = _adapter!;
      } else {
        proxyChild = child;
      }
      return renderObject.child == proxyChild;
    }());
    renderObject.child = null;
    assert(renderObject == this.renderObject);
  }

  @override
  void unmount() {
    _adapter?.dispose();
    _adapter = null;
    super.unmount();
  }
}

This would allow mixing Sliver widgets with the rest of the widgets breaking a big barrier that was limiting to use slivers before.

Example

This is how it looks the code with the above implementation:

ScrollView(
  children: [
    SliverAppBar(title: Text('Hello')),
    Container(color: Colors.grey[400], height: 100),
    Gap(20),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(
          title: Text('Item $index'),
        ),
        childCount: 4,
      ),
    ),
    Divider(),
    Align(
      alignment: Alignment.center,
      child: Text('This is a text'),
    ),
    Divider(),
    SliverContainer(
      margin: EdgeInsets.all(20),
      padding: EdgeInsets.all(10),
      decoration: BoxDecoration(
        color: Colors.grey[200],
        borderRadius: BorderRadius.circular(12),
      ),
      sliver: SliverList(
        delegate: SliverChildBuilderDelegate(
          (context, index) => ListTile(
            title: Text('Item ${index + 4}'),
          ),
          childCount: 4,
        ),
      ),
    ),
  ],
);

Activity

  1. YazeedAlKhalaf commented on Jan 21, 2022

    @YazeedAlKhalaf
    Contributor

    Do you know that you can just wrap non sliver widgets with a SliverToBoxAdapter?

    What would this solve exactly?

  2. jamesblasco commented on Jan 21, 2022

    @jamesblasco
    ContributorAuthor

    Exactly that, no need to use SliverToBoxAdapter 😆

  3. darshankawar commented on Jan 24, 2022

    @darshankawar
    Member

    Labeling it as a proposal / feature request and for further input from the team.

    /cc @HansMuller @Piinks

  4. added
    p: material_uimaterial_ui package in flutter/packages
    f: scrollingViewports, list views, slivers, etc.
    frameworkflutter/packages/flutter repository. See also f: labels.
    c: proposalA detailed proposal for a change to Flutter
    c: new featureNothing broken; request for a new capability
    and removed
    in triagePresently being triaged by the triage team
    on Jan 24, 2022
  5. Piinks commented on Jan 24, 2022

    @Piinks
    Contributor

    Box widgets and Sliver widgets have entirely different layout protocols, so my first though would be that folks might run into obscure issues trying to compose them together. Intermingling them may make it more confusing to understand where and how they work, or don't/

    I guess you could have a ScrollView type widget that just wraps Box widgets in SliverToBoxAdapters, that might require type checking which the framework does not do. This kind of approach might be a good package though.

  6. jamesblasco commented on Jan 24, 2022

    @jamesblasco
    ContributorAuthor

    I think it is already difficult for people as it is and most devs already have problems composing them together. I think it would be something interesting to analyze in a Flutter Research Survey. If this is a real problem for devs and they would like a more usable solution for scroll views or the complexity of the current solution is making them avoid Slivers at all.

    For me, I design most of the scrollable UIs with CustomScrollView, but when talking to the community and clients they mostly struggle with them.

    @Piinks thanks for the response :). Is there anywhere I can learn why type checking is not used in the framework, I would love to learn about that a bit more if possible.

  7. Piinks commented on Jan 24, 2022

    @Piinks
    Contributor

    Is there anywhere I can learn why type checking is not used in the framework

    It's in the wiki: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#avoid-interleaving-multiple-concepts-together :)

    Widgets in the framework are agnostic. They don't have any knowledge of each other or how they work.

    The NestedScrollView and the SliverOverlap* widgets are a good example of this. It does not check to see

    Are any of the slivers in headerSliverBuilder a pinned SliverAppBar?

    You might hope it would so that it takes that overlap into account when laying out the body, but it doesn't. Trying to account for every compositional choice would add complexity, bugs, and a wide variety of behaviors that developers would need to understand and discern.

    The NestedScrollView does not do this. It treats every headSliverBuilder and body the exact same way, regardless of the contents.

    In the case of a pinned SliverAppBar, the SliverOverlap* widgets handle padding the body in order to have everything layout properly - and even the SliverOverlap* widgets have no idea what sliver they are wrapped around. This case is shown in the first example here: https://master-api.flutter.dev/flutter/widgets/NestedScrollView-class.html

    I think there is still a lot of opportunity for education regarding slivers. @craiglabenz has been helping out with this a lot lately.

  8. 9 remaining items

  9. Piinks commented on Mar 17, 2023

    @Piinks
    Contributor

    Reviewing this again, I think there is another gotcha at play when considering having boxes placed along side slivers. The RenderViewport does not handle lazy loading of children, slivers do. A SliverList is the workhorse of lazily laying out its children, the viewport just lays out all of the slivers it has - regardless of whether or not they are visible. So even if there were a wrapper to put a SliverToBoxAdapter around box children, that would not be very performant since everything would be built, whether it was in or out of view.

    So in order to do this proposal properly, with performance in mind, the RenderViewport would need to do a lot more than it currently does in its layout algorithm to be efficient.

  10. added a commit that references this issue on Mar 17, 2023
    32b37ac
  11. Kavantix commented on Mar 17, 2023

    @Kavantix
    Contributor

    @Piinks I’ve had several requests for a MultiSliver.builder which would basically be what you are describing with the optimization and would indeed not be trivial.

    However, I think it is reasonable to have normal widgets in the CustomScrollView directly. Yes if you have a lot it will become slow and you should indeed turn to a SliverList. But if you only have a few the performance is just fine, in fact that’s what happens now when people use the SliverToBoxAdapter.

  12. Kavantix commented on Mar 17, 2023

    @Kavantix
    Contributor

    @Hixie could you make sure your documentation PR does not close this ticket when the PR is merged

  13. Piinks commented on Mar 17, 2023

    @Piinks
    Contributor

    Yes if you have a lot it will become slow and you should indeed turn to a SliverList.

    This would be a pretty big foot gun for developers to discern for themselves.

    could you make sure your documentation PR does not close this ticket when the PR is merged

    @Kavantix can you help understand what the action item here is after that PR?

  14. Kavantix commented on Mar 17, 2023

    @Kavantix
    Contributor

    Yes if you have a lot it will become slow and you should indeed turn to a SliverList.

    This would be a pretty big foot gun for developers to discern for themselves.

    I can see why you would think that. But developers that go to using a CustomScrollView in the first place should be more mindful about how they are using it then when simply using a ListView.builder. Using many slivers or wrapping them manually in SliverToBoxAdapters has the exact same pitfall.

    The big difference in favor of allowing box children is that although it might become slow if used poorly it at least will never crash unlike what it does now when an error occurs in a sliver.

    Btw, the same foot gun argument could also be made for the non builder variant of ListView. Or what I’ve seen a lot, people using a column in a SingleChildScrollView

    could you make sure your documentation PR does not close this ticket when the PR is merged

    @Kavantix can you help understand what the action item here is after that PR?

    This issue is about someone proposing an improvement to CustomScrollview not a question about how slivers work or giving an indication that the documentation is not sufficient.

    So only changing the documentation cannot resolve this issue since we can still continue the discussion after.

  15. Piinks commented on Mar 17, 2023

    @Piinks
    Contributor

    Thank you for sharing more insight on this!

    the non builder variant of ListView

    This still boils down to a SliverList, which lays out lazily. .builder is better since widgets are further created lazily, but it is still optimized for the use case. :)

    using a column in a SingleChildScrollView

    Yes! This is one we are trying to better help folks avoid. Are you aware of the Scrolling Performance Pitfalls project? I'd love your input. We're working to

    1. resolve issues that lead folks to choose poor performance as a workaround
    2. help folks be more aware of and make performant decisions when composing scrolling UIs

    It is being tracked in this project:
    Scrolling Performance Pitfalls (view)

    Feel free to reach out on the discord. I'd really appreciate your expert input on this effort. :)

  16. Piinks commented on Mar 17, 2023

    @Piinks
    Contributor

    Using many slivers or wrapping them manually in SliverToBoxAdapters has the exact same pitfall.

    Also just wanted to acknowledge this. I totally get that, and it is completely true. This is why I organized this pitfall effort. What I meant by my comment was to not introduce another way for folks to fall into a performance trap. :)

  17. Kavantix commented on Mar 22, 2023

    @Kavantix
    Contributor

    @Piinks @Hixie can we reopen this issue

    @Piinks I’ll have a look at those issue and see if I have something to add to them

  18. added
    r: fixedIssue is closed as already fixed in a newer version
    on Mar 23, 2023
  19. github-actions commented on Apr 6, 2023

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  20. locked as resolved and limited conversation to collaborators on Apr 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P3Issues that are less important to the Flutter projectc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterd: api docsIssues with https://api.flutter.dev/d: examplesSample code and demosf: scrollingViewports, list views, slivers, etc.frameworkflutter/packages/flutter repository. See also f: labels.p: material_uimaterial_ui package in flutter/packagesr: fixedIssue is closed as already fixed in a newer versionwaiting for PR to land (fixed)A fix is in flightwould be a good packageSeparate Flutter package should be made for this

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions