Исключение Flutter: ScrollController прикреплен к нескольким представлениям прокрутки
Мое приложение Flutter выдает исключение (ScrollController, присоединенный к нескольким представлениям прокрутки) при переходе от страницы, которая имеет ScrollController
контролировать NestedScrollView
и я не уверен, что я делаю неправильно.
Я воссоздал исключение на простом примере ниже. Я могу ориентироваться от FirstPage
в SecondPage
(и, возможно, обратно) просто отлично, но когда я перемещаюсь из SecondPage
в ThirdPage
исключение брошено. SecondPage
это страница, которая содержит мой обычай CollapsingAppBarPage
виджет, который я предполагаю, имеет проблему. CollapsingAppBarPage
в этом примере это упрощено, но в моем реальном приложении оно меняет цвета / размеры компонентов в зависимости от положения прокрутки. В этом примере исключение происходит, когда _scrollController.offset
называется, как начинается навигация к ThirdPage
, Кроме того, я знаю, что 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 ответов
Не использовать _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);
}
}
Если в вашем случае у вас много ListViews в одном и том же представлении, вы также получите эту ошибку. И чтобы решить эту проблему, вам просто нужно добавить следующее свойство в каждый построитель listView:
controller: ScrollController(),
После того, как у вас будет:
ListView.builder(
controller: ScrollController(),//just add this line
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index]),
);
Источник - эта ссылка: Нажмите здесь
Проблема в том, что или где бы вы ни звонили, замените его на
_scrollController.positions.last
и везде, где вы звоните, замените его на
_scrollController.positions.last.pixels
вы не получите никакой ошибки.
На самом деле проблема в том, что он добавляет новый элемент в
positions
список
_scrollController
и я проверил в
scroll_controller.dart
в
_scrollController.position
возвращает
scrollcontroller.positions.single
а также
_scrollController.offset
возвращается
_scrollController.position.pixels
вот почему мы сталкиваемся с этой проблемой.
Но если вы будете использовать
_scrollController.posisions.last
он вернет последний сохраненный элемент позиции, чтобы ваше приложение работало гладко и не выдавало ошибок.
Решение состоит в том, чтобы пометить соответствующие ScrollViews, т.е. (ListView, SingleChildScrollView, CustomScrollView и т. д.) с атрибутомprimary: false
, так что у вас останется не более одного основного ScrollView
Вам нужно добавить еще одно условие в _getAppBarCollapsePercent()
быть
double _getAppBarCollapsePercent() {
if (!_scrollController.hasClients || _scrollController.positions.length > 1)
return 0.0;
return (_scrollController.offset / (_kExpandedHeight - kToolbarHeight)).clamp(0.0, 1.0);
}
Контроллер не может дать смещение, если он управляет более чем одной прокручиваемой.
Для того, кто используетSingleScrollView
final ScrollController _scrollController = ScrollController();
Expanded(
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true,
child: SingleChildScrollView(
controller: _scrollController,
child: _mainView(context),
)
)
Я не знаю, что вызвало это, но я решил это временно, очистив историю своих навигационных стеков, используя навигацию с помощью этого
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const HomeScreen()),
(route) => false)