Skip to content

Creating a simple ScrollView that supports any widget #97015

@jamesblasco

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,
        ),
      ),
    ),
  ],
);

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: material designflutter/packages/flutter/material repository.f: scrollingViewports, list views, slivers, etc.frameworkflutter/packages/flutter repository. See also f: labels.r: 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