Repository navigation
Creating a simple ScrollView that supports any widget #97015
Description
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?
-
First we should remain the current abstract
ScrollViewto something likeScrollableView,RawScrollView. And use the familiar word 'ScrollView' to be used in every project, just like Text usesRichTextor TextButton usesTextStyleButton. But the simple and familiar name is exposed to the developers. -
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
YazeedAlKhalaf commentedon Jan 21, 2022
Do you know that you can just wrap non sliver widgets with a
SliverToBoxAdapter?What would this solve exactly?
jamesblasco commentedon Jan 21, 2022
Exactly that, no need to use SliverToBoxAdapter 😆
darshankawar commentedon Jan 24, 2022
Labeling it as a proposal / feature request and for further input from the team.
/cc @HansMuller @Piinks
Piinks commentedon Jan 24, 2022
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.
jamesblasco commentedon Jan 24, 2022
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.
Piinks commentedon Jan 24, 2022
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
NestedScrollViewand theSliverOverlap*widgets are a good example of this. It does not check to seeYou 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
NestedScrollViewdoes not do this. It treats everyheadSliverBuilderandbodythe exact same way, regardless of the contents.In the case of a pinned
SliverAppBar, theSliverOverlap*widgets handle padding thebodyin order to have everything layout properly - and even theSliverOverlap*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.htmlI think there is still a lot of opportunity for education regarding slivers. @craiglabenz has been helping out with this a lot lately.
9 remaining items
Piinks commentedon Mar 17, 2023
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.
Documentation improvements
Kavantix commentedon Mar 17, 2023
@Piinks I’ve had several requests for a
MultiSliver.builderwhich 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.
Kavantix commentedon Mar 17, 2023
@Hixie could you make sure your documentation PR does not close this ticket when the PR is merged
Piinks commentedon Mar 17, 2023
This would be a pretty big foot gun for developers to discern for themselves.
@Kavantix can you help understand what the action item here is after that PR?
Kavantix commentedon Mar 17, 2023
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
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.
Piinks commentedon Mar 17, 2023
Thank you for sharing more insight on this!
This still boils down to a SliverList, which lays out lazily.
.builderis better since widgets are further created lazily, but it is still optimized for the use case. :)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
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. :)
Piinks commentedon Mar 17, 2023
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. :)
Kavantix commentedon Mar 22, 2023
@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
github-actions commentedon Apr 6, 2023
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 -vand a minimal reproduction of the issue.