Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Suggestion] Provide a way to assert that a given Widget is a Sliver #94416

Open
navaronbracke opened this issue Nov 30, 2021 · 4 comments
Open
Labels
c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. P3 Issues that are less important to the Flutter project team-framework Owned by Framework team triaged-framework Triaged by Framework team

Comments

@navaronbracke
Copy link
Contributor

navaronbracke commented Nov 30, 2021

Use case

Currently a CustomScrollView accepts a list of slivers.
This list is currently a List<Widget>.

However this creates a bit of confusion.
If I put a Container or any other regular widget in that list,
I will get an exception since I used a Widget that is not a Sliver implementation.

Secondly, there is no easy option to assert that a given Widget is actually a Sliver implementation
(aside from checking against all known Sliver types, i.e. assert(widget is SliverList || widget is SliverGrid || ...)).
The current slivers are divided into implementations of SingleChildRenderObjectWidget and SliverMultiBoxAdaptorWidget (and others?).

There is no way to easily assert that a Widget is a Sliver, regardless of its actual runtime type.

Proposal

Make it possible to use an assert to check if a Widget is actually a Sliver.
I.e. assert(myWidget is SliverWidget, "This widget must be a Sliver.")

Perhaps the different Sliver implementations implement a common (but empty) interface?

/// All the Sliver widgets implement this interface.
/// This makes it easier to check if a given [Widget] is a Sliver.
abstract class SliverWidget {}
@danagbemava-nc danagbemava-nc added in triage Presently being triaged by the triage team f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. passed first triage c: proposal A detailed proposal for a change to Flutter c: new feature Nothing broken; request for a new capability and removed in triage Presently being triaged by the triage team labels Dec 1, 2021
@goderbauer
Copy link
Member

What is the exception that we throw if it is not a sliver? Would it be sufficient to just make that one better?

@navaronbracke
Copy link
Contributor Author

navaronbracke commented Dec 2, 2021

@goderbauer My intention was to make this kind of code a little more type safe at compile-time:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      // Enforce that the list only contains SliverXYZ widgets here.  
      home: MyWidgetWithSlivers(slivers: [Container()]),
    );
  }
}

class MyWidgetWithSlivers extends StatefulWidget {
  const MyWidgetWithSlivers({
    Key? key,
    required this.slivers,
  }) : super(key: key);

  final List<Widget> slivers;

  @override
  State<MyWidgetWithSlivers> createState() => _MyWidgetWithSliversState();
}

class _MyWidgetWithSliversState extends State<MyWidgetWithSlivers> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(slivers: widget.slivers),
    );
  }
}

The exception that we get for this code sample looks like this:

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building Container:
A RenderViewport expected a child of type RenderSliver but received a child of type
RenderLimitedBox.
RenderObjects expect specific types of children because they coordinate with their children during
layout and paint. For example, a RenderSliver cannot be the child of a RenderBox because a
RenderSliver does not understand the RenderBox layout protocol.

The RenderViewport that expected a RenderSliver child was created by:
  Viewport ← IgnorePointer-[GlobalKey#b260d] ← Semantics ← Listener ← _GestureSemantics ←
  RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#cf98a] ← Listener ← _ScrollableScope
  ← _ScrollSemantics-[GlobalKey#b1531] ← RepaintBoundary ← CustomPaint ← RepaintBoundary ← ⋯

The RenderLimitedBox that did not match the expected child type was created by:
  LimitedBox ← Container ← Viewport ← IgnorePointer-[GlobalKey#b260d] ← Semantics ← Listener ←
  _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#cf98a] ←
  Listener ← _ScrollableScope ← _ScrollSemantics-[GlobalKey#b1531] ← RepaintBoundary ← ⋯

The relevant error-causing widget was:
  Container Container:file:///Users/navaronbracke/Desktop/myapp/lib/main.dart:15:34

When the exception was thrown, this was the stack:
#0      ContainerRenderObjectMixin.debugValidateChild.<anonymous closure> (package:flutter/src/rendering/object.dart:3227:9)
#1      ContainerRenderObjectMixin.debugValidateChild (package:flutter/src/rendering/object.dart:3254:6)
#2      MultiChildRenderObjectElement.insertRenderObjectChild (package:flutter/src/widgets/framework.dart:6272:25)
#3      RenderObjectElement.attachRenderObject (package:flutter/src/widgets/framework.dart:5870:35)
#4      RenderObjectElement.mount (package:flutter/src/widgets/framework.dart:5544:5)
#5      SingleChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6194:11)
...     Normal element mounting (7 frames)
#12     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14)
#13     MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6333:36)
#14     MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6344:32)
#15     _ViewportElement.mount (package:flutter/src/widgets/viewport.dart:222:11)
...     Normal element mounting (94 frames)
#109    Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14)
#110    MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6333:36)
#111    MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6344:32)
...     Normal element mounting (261 frames)
#372    Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14)
#373    MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6333:36)
#374    MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6344:32)
...     Normal element mounting (377 frames)
#751    Element.inflateWidget (package:flutter/src/widgets/framework.dart:3673:14)
#752    Element.updateChild (package:flutter/src/widgets/framework.dart:3425:18)
#753    RenderObjectToWidgetElement._rebuild (package:flutter/src/widgets/binding.dart:1198:16)
#754    RenderObjectToWidgetElement.mount (package:flutter/src/widgets/binding.dart:1167:5)
#755    RenderObjectToWidgetAdapter.attachToRenderTree.<anonymous closure> (package:flutter/src/widgets/binding.dart:1112:18)
#756    BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2573:19)
#757    RenderObjectToWidgetAdapter.attachToRenderTree (package:flutter/src/widgets/binding.dart:1111:13)
#758    WidgetsBinding.attachRootWidget (package:flutter/src/widgets/binding.dart:944:7)
#759    WidgetsBinding.scheduleAttachRootWidget.<anonymous closure> (package:flutter/src/widgets/binding.dart:924:7)
(elided 11 frames from class _RawReceivePortImpl, class _Timer, dart:async, and dart:async-patch)

════════════════════════════════════════════════════════════════════════════════════════════════════

Another exception was thrown: A RenderViewport expected a child of type RenderSliver but received a child of type RenderErrorBox.

Another exception was thrown: 'package:flutter/src/widgets/framework.dart': Failed assertion: line 4357 pos 14: 'owner!._debugCurrentBuildTarget
== this': is not true.

Another exception was thrown: 'package:flutter/src/widgets/framework.dart': Failed assertion: line 4357 pos 14: 'owner!._debugCurrentBuildTarget
== this': is not true.

Another exception was thrown: 'package:flutter/src/widgets/framework.dart': Failed assertion: line 4357 pos 14: 'owner!._debugCurrentBuildTarget
== this': is not true.

Another exception was thrown: 'package:flutter/src/widgets/framework.dart': Failed assertion: line 4357 pos 14: 'owner!._debugCurrentBuildTarget
== this': is not true.

// The assertion failure keeps repeating itself.

The message itself is clear to me, since I understand enough of RenderObjects. Perhaps we could include a fix in the exception message, similar to how other exceptions provide us with an alternate option.

@jamesblasco
Copy link
Contributor

jamesblasco commented Jan 21, 2022

The problem is that any StatelessWidget/StatefulWidget can be used inside a CustomScrollView. So you could not only allow a list of List.

I added a proposal here to support a ScrollView that accepts any type of widget
https://twitter.com/JamesBlasco/status/1484564101598695428?s=20

@goderbauer goderbauer added the P3 Issues that are less important to the Flutter project label Feb 2, 2022
@flutter-triage-bot flutter-triage-bot bot added team-framework Owned by Framework team triaged-framework Triaged by Framework team labels Jul 8, 2023
@andynewman10
Copy link

andynewman10 commented Aug 24, 2023

Up until recently, I wasn't aware of the two "parallel worlds", slivers vs widgets, and it's been a shock, I must say.

The problem is that any StatelessWidget/StatefulWidget can be used inside a CustomScrollView. So you could not only allow a list of List.

@jamesblasco what do you mean, here? CustomScrollView does not accept box widgets, only slivers. The ScrollView widget that you propose in your twitter link is also what I have done in my code.

Converting between slivers and regular "box" widgets is easy IMHO:

  • use SliverToBoxAdapter(child: <widget>) to do box -> sliver conversions
  • use SliverList(delegate: SliverChildBuilderDelegate((context, index) { return <widget>; }, childCount: 1)) to do sliver -> box conversions

I think Flutter has a problem with redundancies between sliver and non-sliver widgets, eg. ListView vs SliverList (and even maybe some redundancies between box widgets (Column vs ListView), but this is another story)

What is the deal-breaker here, justifying the very existence of slivers? I see there is one potential issue with FutureBuilder, as these widgets somewhat behave as pass-throughs: a FutureBuilder error widget may be a box widget, whereas its content widget may be a sliver.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. P3 Issues that are less important to the Flutter project team-framework Owned by Framework team triaged-framework Triaged by Framework team
Projects
None yet
Development

No branches or pull requests

6 participants