Flutter异常:ScrollController附加到多个滚动视图

31
我的Flutter应用程序在从带有 ScrollController 控制 NestedScrollView 的页面导航时抛出异常(附加到多个滚动视图的ScrollController),我不确定我做错了什么。
以下是我用简单的示例重现异常。我可以从 FirstPage 正常地导航到 SecondPage (并可选择返回),但是当我从 SecondPage 导航到 ThirdPage 时,会抛出异常。 SecondPage 是包含我的自定义 CollapsingAppBarPage 部件的页面,我认为它有问题。CollapsingAppBarPage 在此示例中已简化,但在我的真实应用程序中,它根据滚动位置更改组件的颜色/大小。在此示例中,当导航开始到 ThirdPage 时调用 _scrollController.offset 时会发生异常。此外,我知道FirstPage和ThirdPage可以是Stateless而不是StatefulWidgets,但我想使其尽量像我的应用程序。这是我的完全工作示例。
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: FirstPage(),
    );
  }
}

class FirstPage extends StatefulWidget {
  @override
  _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("First Page"),
      ),
      body: Center(
        child: RaisedButton(
          child: Text("Navigate Next"),
          onPressed: () async {
            await Navigator.push(context,
                MaterialPageRoute(builder: (context) => SecondPage())
            );
          },
        ),
      ),
    );
  }
}

class SecondPage extends StatefulWidget {
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: CollapsingAppBarPage(
        titleText: "Second Page",
        bodyCreator: (context) {
          return ListView(
            children: <Widget>[
              Center(
                child: RaisedButton(
                  child: Text("Navigate Next"),
                  onPressed: () async {
                    await Navigator.push(context,
                        MaterialPageRoute(builder: (context) => ThirdPage())
                    );
                  },
                ),
              )
            ],
          );
        },
      ),
    );
  }
}

class ThirdPage extends StatefulWidget {
  @override
  _ThirdPageState createState() => _ThirdPageState();
}

class _ThirdPageState extends State<ThirdPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text("Third Page")
        ),
        body: Container()
    );
  }
}

typedef CollapsingAppBarBodyCreator = Widget Function(BuildContext context);

class CollapsingAppBarPage extends StatefulWidget {

  final String titleText;
  final CollapsingAppBarBodyCreator bodyCreator;

  CollapsingAppBarPage({
    Key key,
    this.titleText,
    @required this.bodyCreator,
  }) : super(key: key);

  @override
  _CollapsingAppBarPageState createState() => _CollapsingAppBarPageState();
}

class _CollapsingAppBarPageState extends State<CollapsingAppBarPage> {

  static const _kExpandedHeight = 200.0;

  ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController()
      ..addListener(() {
        setState(() {
          // force a refresh so the app bar can be updated
        });
      });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return NestedScrollView(
      controller: _scrollController,
      headerSliverBuilder: _createSliverAppBar,
      body: widget.bodyCreator(context),
    );
  }

  List<Widget> _createSliverAppBar(BuildContext context, bool innerBoxIsScrolled) {
    // change the icon color as the page scrolls
    var collapsePercent = _getAppBarCollapsePercent();
    int rgb = ((1.0 - collapsePercent) * 255).round();
    var color = Color.fromARGB(255, rgb, rgb, rgb);

    return <Widget>[
      SliverAppBar(
        expandedHeight: _kExpandedHeight,
        pinned: true,
        iconTheme: IconThemeData(color: color),
        title: Text(widget.titleText),
      )
    ];
  }

  double _getAppBarCollapsePercent() {
    if (!_scrollController.hasClients)
      return 0.0;

    return (_scrollController.offset / (_kExpandedHeight - kToolbarHeight)).clamp(0.0, 1.0);
  }
}

以下是异常的截断跟踪:

flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building Builder(dirty):
flutter: ScrollController attached to multiple scroll views.
flutter: 'package:flutter/src/widgets/scroll_controller.dart': Failed assertion: line 111 pos 12:
flutter: '_positions.length == 1'
flutter:
flutter: Either the assertion indicates an error in the framework itself, or we should provide substantially
flutter: more information in this error message to help you determine and fix the underlying cause.
flutter: In either case, please report this assertion by filing a bug on GitHub:
flutter:   https://github.com/flutter/flutter/issues/new?template=BUG.md
flutter:
flutter: When the exception was thrown, this was the stack:
flutter: #2      ScrollController.position (package:flutter/src/widgets/scroll_controller.dart:111:12)
flutter: #3      ScrollController.offset (package:flutter/src/widgets/scroll_controller.dart:118:24)
flutter: #4      _CollapsingAppBarPageState._getAppBarCollapsePercent (package:flutter_scroll_test/main.dart:160:31)
flutter: #5      _CollapsingAppBarPageState._createSliverAppBar (package:flutter_scroll_test/main.dart:142:27)
flutter: #6      NestedScrollView._buildSlivers (package:flutter/src/widgets/nested_scroll_view.dart:271:20)
flutter: #7      _NestedScrollViewState.build.<anonymous closure> (package:flutter/src/widgets/nested_scroll_view.dart:347:29)
flutter: #8      Builder.build (package:flutter/src/widgets/basic.dart:5736:41)
flutter: #9      StatelessElement.build (package:flutter/src/widgets/framework.dart:3774:28)
flutter: #10     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3721:15)
7个回答

51

如果您的情况是在同一视图中有多个 ListViews,您也会遇到此错误。为了解决它,您只需要将以下属性添加到每个 listView builder 中:

controller: ScrollController(),

完成以下步骤后:

ListView.builder(
        controller: ScrollController(),//just add this line
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index]),
          );

源代码位于此链接:点击这里


小心不要随意为每个列表视图添加新控制器;例如,滚动条可能需要共享同一个控制器才能正确交互。 - qix
此操作为每个视图创建一个新的控制器,因此避免了多个视图争夺同一控制器的冲突。 - elgsylvain85

30

在推入路由时,不要直接使用_scrollController.offset,因为似乎会附加一个滚动位置以在弹出路由时检索。因此,我们需要将其转换为一段状态。

这是完整的修订代码。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: FirstPage(),
    );
  }
}

class FirstPage extends StatefulWidget {
  @override
  _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("First Page"),
      ),
      body: Center(
        child: RaisedButton(
          child: Text("Navigate Next"),
          onPressed: () async {
            await Navigator.push(context,
                MaterialPageRoute(builder: (context) => SecondPage())
            );
          },
        ),
      ),
    );
  }
}

class SecondPage extends StatefulWidget {
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: CollapsingAppBarPage(
        titleText: "Second Page",
        bodyCreator: (context) {
          return ListView(

            children: <Widget>[
              Center(
                child: RaisedButton(
                  child: Text("Navigate Next"),
                  onPressed: () async {
                    await Navigator.push(context,
                        MaterialPageRoute(builder: (context) => ThirdPage())
                    );
                  },
                ),
              )
            ],
          );
        },
      ),
    );
  }
}

class ThirdPage extends StatefulWidget {
  @override
  _ThirdPageState createState() => _ThirdPageState();
}

class _ThirdPageState extends State<ThirdPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text("Third Page")
        ),
        body: Container()
    );
  }
}

typedef CollapsingAppBarBodyCreator = Widget Function(BuildContext context);

class CollapsingAppBarPage extends StatefulWidget {

  final String titleText;
  final CollapsingAppBarBodyCreator bodyCreator;

  CollapsingAppBarPage({
    Key key,
    this.titleText,
    @required this.bodyCreator,
  }) : super(key: key);

  @override
  _CollapsingAppBarPageState createState() => _CollapsingAppBarPageState();
}

class _CollapsingAppBarPageState extends State<CollapsingAppBarPage> {

  static const _kExpandedHeight = 200.0;

  ScrollController _scrollController;
  //Offset state <-------------------------------------
  double offset = 0.0 ;



  @override
  void initState() {
    super.initState();
    //print("init state is called");

    _scrollController = ScrollController() //keepScrollOffset: false removed
      ..addListener(() {
        setState(() {
          //<-----------------------------
          offset = _scrollController.offset;
          // force a refresh so the app bar can be updated
        });
      });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return NestedScrollView(
      controller: _scrollController,
      headerSliverBuilder: _createSliverAppBar,
      body: widget.bodyCreator(context),
    );
  }

  List<Widget> _createSliverAppBar(BuildContext context, bool innerBoxIsScrolled) {
    // change the icon color as the page scrolls
    print("_createSliverAppBar is called");
    var collapsePercent = _getAppBarCollapsePercent();
    int rgb = ((1.0 - collapsePercent) * 255).round();
    var color = Color.fromARGB(255, rgb, rgb, rgb);

    return <Widget>[
      SliverAppBar(
        expandedHeight: _kExpandedHeight,
        pinned: true,
        iconTheme: IconThemeData(color: color),
        title: Text(widget.titleText),
      )
    ];
  }

  double _getAppBarCollapsePercent() {
    if (!_scrollController.hasClients ){
      print("positions is ${_scrollController.positions.length}");
      return 0.0;

    }


    //print("offset is${_scrollController.offset} and positions is ${_scrollController.positions.length}");
    return (offset / (_kExpandedHeight - kToolbarHeight)).clamp(0.0, 1.0);
  }
}

还要注意嵌套滚动控制器。一个滚动控制器只能在嵌套中拥有一个滚动视图。 - Golden Lion

14
问题出在你调用 _scrollController.position_scrollController.offset 的地方,应该将其替换为 _scrollController.positions.last,并且无论何时调用 _scrollController.offset,都应该替换为 _scrollController.positions.last.pixels,这样就不会出现错误了。
实际上,问题是因为在 _scrollControllerpositions 列表中添加了新元素,在 scroll_controller.dart 中检查后发现 _scrollController.position 返回 scrollcontroller.positions.single,而_scrollController.offset 返回 _scrollController.position.pixels, 所以我们会遇到这个问题。
但是,如果使用 _scrollController.positions.last,它将返回最后保存的位置元素,因此您的应用程序将顺利运行,也不会引发任何错误。

11
解决方法是标记适当的 ScrollViews,即(ListView、SingleChildScrollView、CustomScrollView等),使用属性primary: false,这样您就只剩下一个主要的 ScrollView 了。

2

你需要在_getAppBarCollapsePercent()中添加另一个条件,即

 double _getAppBarCollapsePercent() {
    if (!_scrollController.hasClients || _scrollController.positions.length > 1)
      return 0.0;

    return (_scrollController.offset / (_kExpandedHeight - kToolbarHeight)).clamp(0.0, 1.0);
  }

如果控制器控制多个可滚动区域,则无法指定偏移量。


4
是的,我之前尝试过这个方法,但是positionsScrollController的受保护成员,如果尝试访问它,编译器会发出警告。虽然这可以防止异常,但在从第三页返回第二页时,它仍无法正确地维护滚动偏移(计算出的RGB颜色)。是否有我的代码有问题,导致多个滚动位置/客户端被注册? - dewald

0

对于使用SingleScrollView的人

final ScrollController _scrollController = ScrollController();

Expanded(
            child: Scrollbar(
                    controller: _scrollController,
                    thumbVisibility: true,
                    child: SingleChildScrollView(
                      controller: _scrollController,
                      child: _mainView(context),
                    )
          )

0
我不知道是什么原因导致了这个问题,但我通过清除我的导航堆栈历史记录来暂时解决了它,通过使用以下方式导航。
Navigator.pushAndRemoveUntil(
              context,
              MaterialPageRoute(builder: (_) => const HomeScreen()),
              (route) => false)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接