Search code examples
flutterdartflutter-sliverflutter-go-router

How to use CustomScrollView in a GoRouter ShellRoute?


I had the idea to create a CustomScrollView within a GoRouter's ShellRoute so that I could have a reusable SliverAppBar that doesn't have to be re-rendered on every route. Unfortunately, returning a SliverList from the GoRoute within a ShellRoute causes issues because GoRouter embeds the Widget returned by GoRoute#builder into some other classes...

Minimal example:

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

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

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    var title = 'Flutter Demo';
    return MaterialApp.router(
        title: title,
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
          useMaterial3: true,
        ),
        routerConfig:
            GoRouter(navigatorKey: GlobalKey<NavigatorState>(), routes: [
          ShellRoute(
              navigatorKey: GlobalKey<NavigatorState>(),
              builder:
                  (BuildContext context, GoRouterState state, Widget child) {
                return CustomScrollView(slivers: [
                  SliverAppBar(
                    expandedHeight: 300.0,
                    flexibleSpace: FlexibleSpaceBar(
                      background: Container(
                        color: Colors.red,
                        child: const Text("Shrinkable"),
                      ),
                    ),
                  ),
                  child
                ]);
              },
              routes: [
                GoRoute(
                    path: "/",
                    builder: (BuildContext context, GoRouterState state) {
                      return SliverList(
                          delegate:
                              SliverChildListDelegate([const Text("data")]));
                    })
              ])
        ]));
  }
}

Error logs:

======== Exception caught by widgets library =======================================================
The following assertion was thrown building NotificationListener<NavigationNotification>:
A RenderViewport expected a child of type RenderSliver but received a child of type RenderPointerListener.

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#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← _ScrollableScope ← _ScrollSemantics-[GlobalKey#e77c7] ← NotificationListener<ScrollMetricsNotification> ← RepaintBoundary ← CustomPaint-[GlobalKey#dedf6] ← ⋯
The RenderPointerListener that did not match the expected child type was created by: Listener ← NotificationListener<NavigationNotification> ← HeroControllerScope ← Navigator-[LabeledGlobalKey<NavigatorState>#0220d] ← HeroControllerScope ← GoRouterStateRegistryScope ← _CustomNavigator-[GlobalObjectKey int#0220d] ← Viewport ← IgnorePointer-[GlobalKey#e9fba] ← Semantics ← Listener ← _GestureSemantics ← ⋯
The relevant error-causing widget was: 
  Navigator-[LabeledGlobalKey<NavigatorState>#0220d] Navigator:file:///C:/Users/Pete/AppData/Local/Pub/Cache/hosted/pub.dev/go_router-14.2.3/lib/src/builder.dart:429:16

======== Exception caught by widgets library =======================================================
The following assertion was thrown building HeroControllerScope:
A RenderViewport expected a child of type RenderSliver but received a child of type RenderErrorBox.

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#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← _ScrollableScope ← _ScrollSemantics-[GlobalKey#e77c7] ← NotificationListener<ScrollMetricsNotification> ← RepaintBoundary ← CustomPaint-[GlobalKey#dedf6] ← ⋯
The RenderErrorBox that did not match the expected child type was created by: ErrorWidget-[#4bbd6] ← NotificationListener<NavigationNotification> ← HeroControllerScope ← Navigator-[LabeledGlobalKey<NavigatorState>#0220d] ← HeroControllerScope ← GoRouterStateRegistryScope ← _CustomNavigator-[GlobalObjectKey int#0220d] ← Viewport ← IgnorePointer-[GlobalKey#e9fba] ← Semantics ← Listener ← _GestureSemantics ← ⋯
The relevant error-causing widget was: 
  Navigator-[LabeledGlobalKey<NavigatorState>#0220d] Navigator:file:///C:/Users/Pete/AppData/Local/Pub/Cache/hosted/pub.dev/go_router-14.2.3/lib/src/builder.dart:429:16

======== Exception caught by widgets library =======================================================
The following assertion was thrown building Navigator-[LabeledGlobalKey<NavigatorState>#0220d](dependencies: [HeroControllerScope, UnmanagedRestorationScope], state: NavigatorState#e0a51(tickers: tracking 1 ticker)):
A RenderViewport expected a child of type RenderSliver but received a child of type RenderErrorBox.

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#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← _ScrollableScope ← _ScrollSemantics-[GlobalKey#e77c7] ← NotificationListener<ScrollMetricsNotification> ← RepaintBoundary ← CustomPaint-[GlobalKey#dedf6] ← ⋯
The RenderErrorBox that did not match the expected child type was created by: ErrorWidget-[#9b449] ← HeroControllerScope ← Navigator-[LabeledGlobalKey<NavigatorState>#0220d] ← HeroControllerScope ← GoRouterStateRegistryScope ← _CustomNavigator-[GlobalObjectKey int#0220d] ← Viewport ← IgnorePointer-[GlobalKey#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← ⋯
The relevant error-causing widget was: 
  Navigator-[LabeledGlobalKey<NavigatorState>#0220d] Navigator:file:///C:/Users/Pete/AppData/Local/Pub/Cache/hosted/pub.dev/go_router-14.2.3/lib/src/builder.dart:429:16

======== Exception caught by widgets library =======================================================
The following assertion was thrown building HeroControllerScope:
A RenderViewport expected a child of type RenderSliver but received a child of type RenderErrorBox.

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#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← _ScrollableScope ← _ScrollSemantics-[GlobalKey#e77c7] ← NotificationListener<ScrollMetricsNotification> ← RepaintBoundary ← CustomPaint-[GlobalKey#dedf6] ← ⋯
The RenderErrorBox that did not match the expected child type was created by: ErrorWidget-[#dbfa8] ← Navigator-[LabeledGlobalKey<NavigatorState>#0220d] ← HeroControllerScope ← GoRouterStateRegistryScope ← _CustomNavigator-[GlobalObjectKey int#0220d] ← Viewport ← IgnorePointer-[GlobalKey#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← ⋯
The relevant error-causing widget was: 
  HeroControllerScope HeroControllerScope:file:///C:/Users/Pete/AppData/Local/Pub/Cache/hosted/pub.dev/go_router-14.2.3/lib/src/builder.dart:427:14

======== Exception caught by widgets library =======================================================
The following assertion was thrown building GoRouterStateRegistryScope:
A RenderViewport expected a child of type RenderSliver but received a child of type RenderErrorBox.

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#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← _ScrollableScope ← _ScrollSemantics-[GlobalKey#e77c7] ← NotificationListener<ScrollMetricsNotification> ← RepaintBoundary ← CustomPaint-[GlobalKey#dedf6] ← ⋯
The RenderErrorBox that did not match the expected child type was created by: ErrorWidget-[#a23b2] ← HeroControllerScope ← GoRouterStateRegistryScope ← _CustomNavigator-[GlobalObjectKey int#0220d] ← Viewport ← IgnorePointer-[GlobalKey#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← _ScrollableScope ← ⋯
The relevant error-causing widget was: 
  GoRouterStateRegistryScope GoRouterStateRegistryScope:file:///C:/Users/Pete/AppData/Local/Pub/Cache/hosted/pub.dev/go_router-14.2.3/lib/src/builder.dart:425:12

======== Exception caught by widgets library =======================================================
The following assertion was thrown building _CustomNavigator-[GlobalObjectKey int#0220d](state: _CustomNavigatorState#4988d):
A RenderViewport expected a child of type RenderSliver but received a child of type RenderErrorBox.

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#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← _ScrollableScope ← _ScrollSemantics-[GlobalKey#e77c7] ← NotificationListener<ScrollMetricsNotification> ← RepaintBoundary ← CustomPaint-[GlobalKey#dedf6] ← ⋯
The RenderErrorBox that did not match the expected child type was created by: ErrorWidget-[#2da42] ← GoRouterStateRegistryScope ← _CustomNavigator-[GlobalObjectKey int#0220d] ← Viewport ← IgnorePointer-[GlobalKey#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← _ScrollableScope ← _ScrollSemantics-[GlobalKey#e77c7] ← ⋯
The relevant error-causing widget was: 
  _CustomNavigator-[GlobalObjectKey int#0220d] _CustomNavigator:file:///C:/Users/Pete/AppData/Local/Pub/Cache/hosted/pub.dev/go_router-14.2.3/lib/src/builder.dart:276:16

======== Exception caught by widgets library =======================================================
The following assertion was thrown building RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46](state: RawGestureDetectorState#cc3be(gestures: <none>, behavior: opaque)):
A RenderViewport expected a child of type RenderSliver but received a child of type RenderErrorBox.

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#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← _ScrollableScope ← _ScrollSemantics-[GlobalKey#e77c7] ← NotificationListener<ScrollMetricsNotification> ← RepaintBoundary ← CustomPaint-[GlobalKey#dedf6] ← ⋯
The RenderErrorBox that did not match the expected child type was created by: ErrorWidget-[#413d0] ← _CustomNavigator-[GlobalObjectKey int#0220d] ← Viewport ← IgnorePointer-[GlobalKey#e9fba] ← Semantics ← Listener ← _GestureSemantics ← RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#e2d46] ← Listener ← _ScrollableScope ← _ScrollSemantics-[GlobalKey#e77c7] ← NotificationListener<ScrollMetricsNotification> ← ⋯
The relevant error-causing widget was: 
  CustomScrollView CustomScrollView:file:///D:/code/playground/flutter/go_router_slivers/lib/main.dart:26:24

If I try to wrap the ShellRoute#child inside of a SliverToBoxAdapter, then I get the sort of reverse error, albeit a much smaller one:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

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

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    var title = 'Flutter Demo';
    return MaterialApp.router(
        title: title,
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
          useMaterial3: true,
        ),
        routerConfig:
            GoRouter(navigatorKey: GlobalKey<NavigatorState>(), routes: [
          ShellRoute(
              navigatorKey: GlobalKey<NavigatorState>(),
              builder:
                  (BuildContext context, GoRouterState state, Widget child) {
                return CustomScrollView(slivers: [
                  SliverAppBar(
                    expandedHeight: 300.0,
                    flexibleSpace: FlexibleSpaceBar(
                      background: Container(
                        color: Colors.red,
                        child: const Text("Shrinkable"),
                      ),
                    ),
                  ),
                  SliverToBoxAdapter(child: child) // <-- wrapped this time
                ]);
              },
              routes: [
                GoRoute(
                    path: "/",
                    builder: (BuildContext context, GoRouterState state) {
                      return SliverList(
                          delegate:
                              SliverChildListDelegate([const Text("data")]));
                    })
              ])
        ]));
  }
}

Error logs:

======== Exception caught by widgets library =======================================================
The following assertion was thrown building Builder:
A RenderSemanticsAnnotations expected a child of type RenderBox but received a child of type RenderSliverList.

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 RenderSemanticsAnnotations that expected a RenderBox child was created by: Semantics ← Builder ← RepaintBoundary-[GlobalKey#fd14b] ← IgnorePointer ← AnimatedBuilder ← SnapshotWidget ← _ZoomExitTransition ← SnapshotWidget ← _ZoomEnterTransition ← DualTransitionBuilder ← SnapshotWidget ← _ZoomExitTransition ← ⋯
The RenderSliverList that did not match the expected child type was created by: SliverList ← Builder ← Semantics ← Builder ← RepaintBoundary-[GlobalKey#fd14b] ← IgnorePointer ← AnimatedBuilder ← SnapshotWidget ← _ZoomExitTransition ← SnapshotWidget ← _ZoomEnterTransition ← DualTransitionBuilder ← ⋯
The relevant error-causing widget was: 
  Builder Builder:file:///C:/Users/Pete/AppData/Local/Pub/Cache/hosted/pub.dev/go_router-14.2.3/lib/src/builder.dart:256:9

This is my first foray into the land of Slivers, so I'm at a loss. Do I need to give up and just rebuild the CustomScrollView on each individual route, along with the SliverAppBar? Any advice is appreciated. TIA :)


Solution

  • So the problem you're facing is from the child route. Bcz, you're passing a sliver to the route builder, where it expects a widget. Here you're passing a SliverList.

    That is a sliver, not a regular widget.

    Right now, there are two simple ways to fix it. First one is -

    Do what you're already doing. And just wrap your SliverList with a CustomScrollView. Just to convert the SliverList to a regular widget. But it will overflow. If you look at the layout it was supposed to overflow anyway. Bcz you converted a regular widget to a sliver. But the regular widget doesn't define how long it will be on the scroll direction.

    But yeah the code will look something like this -

    
    
    import 'package:flutter/material.dart';
    import 'package:go_router/go_router.dart';
    
    void main() {
      runApp(const App());
    }
    
    class App extends StatelessWidget {
      const App({super.key});
    
      @override
      Widget build(BuildContext context) {
        var title = 'Flutter Demo';
        return MaterialApp.router(
          title: title,
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          routerConfig: GoRouter(
            navigatorKey: GlobalKey<NavigatorState>(),
            routes: [
              ShellRoute(
                  navigatorKey: GlobalKey<NavigatorState>(),
                  builder:
                      (BuildContext context, GoRouterState state, Widget child) {
                    return CustomScrollView(slivers: [
                      SliverAppBar(
                        expandedHeight: 300.0,
                        flexibleSpace: FlexibleSpaceBar(
                          background: Container(
                            color: Colors.red,
                            child: const Text("Shrinkable"),
                          ),
                        ),
                      ),
                      SliverToBoxAdapter(
                        child: child, // * You can define a height here too. To tell the Sliver that my regular widget will be x/y long.
                       ) // <-- wrapped this time
                    ]);
                  },
                  routes: [
                    GoRoute(
                        path: "/",
                        builder: (BuildContext context, GoRouterState state) {
                          return SizedBox(
                           height: 500; // However, I have defined the height here.
                            child: CustomScrollView(
                              slivers: [
                                SliverList(
                                  delegate: SliverChildListDelegate(
                                    [
                                      const Text("data"),
                                    ],
                                  ),
                                ),
                              ],
                            ),
                          );
                        })
                  ])
            ],
          ),
        );
      }
    }
    
    

    But the other way you can do the same thing is to pass a widget variant of the SliverList to the GoRoute builder. Which will be a regular ListView widget. And for that the code should look something like this.

    GoRoute(
         path: "/",
         builder: (BuildContext context, GoRouterState state) {
           return SizedBox(
             height: 500,
             child: ListView(
               children: const [
                 Text("data"),
               ],
             ),
           );
         },
       )
    
    

    Hope it helps! Happy coding!💙