-
Notifications
You must be signed in to change notification settings - Fork 29k
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
ScrollView
to something likeScrollableView
,RawScrollView
. And use the familiar word 'ScrollView' to be used in every project, just like Text usesRichText
or 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,
),
),
),
],
);