Flutter can't draw Scrollbar with nested Columns and ListViews - flutter

I have a bunch of nested Columns and ListViews, and I get this exception, telling me that the app can't paint a Scrollbar. The app works fine, but of course I want to fix this issue.
This is part of the output in the console:
======== Exception caught by animation library =====================================================
The following assertion was thrown while notifying status listeners for AnimationController:
The provided ScrollController is currently attached to more than one ScrollPosition.
The Scrollbar requires a single ScrollPosition in order to be painted.
When the scrollbar is interactive, the associated Scrollable widgets must have unique ScrollControllers. The provided ScrollController must be unique to a Scrollable widget.
Below are two alternatives of my code (once with a ListView at the top level, once with a Column). I left out the parts I believe to be irrelevant to this problem:
#override
Widget build(BuildContext context) {
return Column(
children: [
//I originally used ListView.builder, this didn't work
ListView.builder(
shrinkWrap: true,
itemCount: _altControllers.length,
itemBuilder: (context, index) {
return Column(
key: ValueKey('alt $index'), //no widget.targetLangCode and ws here (see a few lines below)
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _writingSystems.map((String ws) {
return Row(
key: ValueKey('alt $index: $widget.targetLangCode $ws'),
// children: ...,
);
}).toList(),
),
// ...
],
);
},
),
//then I tried this alternative with a Column, it still didn't work
Column(
children: _altControllers
.asMap()
.map((int index, Map<String, TextEditingController> controllers) {
return MapEntry(
index,
Column(
key: ValueKey('alt $index'),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _writingSystems.map((String ws) {
return Row(
key: ValueKey('alt $index: $widget.targetLangCode $ws'),
// children: ...,
);
}).toList(),
),
// ...
],
),
);
})
.values
.toList(),
),
],
);
}
I've also tried using ScrollController in some places, but didn't manage to solve my problem with that.

Related

In Flutter, how to put a horizontal sliverlist inside a vertical SingleChildScrollView without static height?

Problem
When I tried to used horizontal SliverList inside a vertical SingleChildScrollView, this error was occured.
RenderBox was not laid out: RenderShrinkWrappingViewport#7c402 relayoutBoundary=up13 NEEDS-PAINT
'package:flutter/src/rendering/box.dart':
Failed assertion: line 1979 pos 12: 'hasSize'
Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
Code
This is my code's widget tree.
NestedScrollView
TabBarView
SingleChildView(vertical)
Column
Column
Row
SliverList(horizontal)
And, this is at the bottom stateless widget.
class MyHorizontalWidget extends StatelessWidget {
final List<num> maxList;
final List<num> minList;
const MyHorizontalWidget({
super.key,
required this.maxList,
required this.minList,
});
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomScrollView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Text('$index');
},
childCount: maxAreaTypes.length,
),
),
],
),
KoreanText(
'front one is an index',
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomScrollView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Text('$index 2');
},
childCount: minAreaTypes.length,
),
),
],
),
KoreanText(
'front one is an index, too',
),
],
),
],
);
}
}
Try
I tried giving the mainAxisSize: MainAxisSize.min option to all columns at the top of MyHorizontalWidget. => fail
I tried wrapping MyHorizontalWidget's row children with Flexible(or Expanded) and SizedBox. => fail
Question
Like this question's title, how could I put a horizontal sliverlist inside a vertical SingleChildScrollView without static height??
Edit
I want to set a auto line change with row widget inside MyHorizontalWidget.

How to wrap a List of Widgets without changing the layout when unwrapped and spread in a Row?

Currently, the following structure gives me the intended layout:
Consumer<State>(
builder(context, state, child) => Row(
children: [
Widget1(...), // fixed width
...List.generate(state.length, (index) => Expanded(child: Widget2(index, state))),
]
)
)
Widget1 does not need to be hooked up to state, and so I don't want it to be rerendered when it changes. It would be great if List.generate could be hooked to a Consumer, but builder needs to return a single Widget, not a list of Widgets.
I've tried using the Wrap widget around the list, and hooking that to a Consumer. This accomplishes my goal of not rerendering Widget1, but it changes the layout - the Widget2's don't expand to fill the remaining space any longer.
Here's an example:
Row(
children: [
Widget1(...), // fixed width
Consumer<State>(
builder: (context, state, child) {
return Expanded(
child: Row(
children: List.generate(
state.length,
(index) {
return Expanded(
child: Widget2(index, state),
);
},
),
),
);
},
),
],
),

Widget flickers and disappears when scrolling

I'm already losing sleep over this.
I'm trying to display a chart inside a ListView (for scrolling). For some reason the contents of the Card flickers when scrolling and randomly completely disappears (the Card itself stays visible though).
Any idea why would that happen?
(...) ListView (...)
children: [Row ( children: [buildChartBox()] )] (...)
Expanded buildChartBox() {
return Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
chartTitles(
title: 'Items',
subtitle: 'by value'),
SizedBox(
height: 300,
child: ValuesChart(data: calculateValues(items)))
],
),
],
),
),
),
);
}
Row chartTitles({String title = '', String subtitle = ''}) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: text_charttitle),
Text(subtitle, style: text_chartsubtitle),
],
)
],
);
}
Things tried:
Both of these were originally Stateless Widgets; I changed to simple
methods to simplify but it didn't change the weird behaviour.
Replacing the chartTitles return with an empty Container (i.e. removing the titles) does mitigate the issue. The chart then stays displayed but also flickers slightly.
Replacing the ListView with a SingleChildScrollView doesn't change anything.
EDIT: Code for the ValuesChart:
import 'package:fl_chart/fl_chart.dart';
class ValuesChart extends StatelessWidget {
final Map<String, int> data;
const ValuesChart({required this.data});
#override
Widget build(BuildContext context) {
return Container(
child: PieChart(
_theData(data),
));
}
}
Note I'm using a package called 'fl_chart'. _theData just returns various parameters for the chart, I don't think it's relevant.
Try to replace ListView with SingleChildScrollView
ListViews in flutter by default using what it is called in Android RecyclerView to efficiently use render resources.
If you are interested here an article
https://medium.com/1mgofficial/how-recyclerview-works-internally-71290de5d2c4

How to make a Widget to be as small as it can (wrap_content in Android)?

I have a rendering issue with render tree similar to Column>PageView>Column, where the last Column is inside a page of the PageView.
The PageView isn't being rendered correctly, so I get an exception (Horizontal viewport was given unbounded height.) as the framework can't calculate its sizes, I can fix it by wrapping it into a Flexible or an Expanded, but I don't want the PageView to take the whole screen, I want it to be as small as possible and on the center on the screen.
Here's a representation of my problem:
// This code throws an exception:
class Widget_1_Has_Problem extends StatelessWidget {
final PageController pageController = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.done,size: 112),
SizedBox(height: 32),
PageView(
physics: NeverScrollableScrollPhysics(),
controller: pageController,
children: <Widget>[
// Many widgets go here, I am just simplifying with a Column.
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('TEXT TEXT TEXT TEXT TEXT TEXT'),
SizedBox(height: 16),
Text('TEXT2 TEXT2 TEXT2 TEXT2 TEXT2 TEXT2'),
],
),
],
),
],
),
);
}
}
This is what I wanted to achieve (I removed the PageView in order to show it):
class Widget_2_No_PageView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.done,size: 112),
SizedBox(height: 32),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('TEXT TEXT TEXT TEXT TEXT TEXT'),
SizedBox(height: 16),
Text('TEXT2 TEXT2 TEXT2 TEXT2 TEXT2 TEXT2'),
],
),
],
),
);
}
}
And this is how I can fix it, but it's not perfect, I will show later:
class Widget_3_With_Flex_Not_Perfect_Solution extends StatelessWidget {
final PageController pageController = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Spacer(flex: 2,),
Icon(Icons.done,size: 112),
SizedBox(height: 32),
Flexible(
flex: 1,
child: PageView(
physics: NeverScrollableScrollPhysics(),
controller: pageController,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('TEXT TEXT TEXT TEXT TEXT TEXT'),
SizedBox(height: 16),
Text('TEXT2 TEXT2 TEXT2 TEXT2 TEXT2 TEXT2'),
],
),
],
),
),
Spacer(flex: 2,),
],
),
);
}
}
This solution isn't perfect because Spacer and Flexible or Expanded always try to maintain it's proportionality of the flex weight, which means that for smaller displays the Spacer won't vanish, it will remain there, whereas the first and desired image won't have such a void space. As we can see in the below image, the Spacer is always there. Also I have to calculate the flex factor for every change in this design, whereas in my first code example the widget will size itself to the center of the screen automatically.
How can I instruct PageView then to be as small as it can be, instead of expand as much as it wants, as that's the only solution I can find online?
The PageView uses SliverFillViewport to wrap around given children.
This SliverFillViewport doesn't care about how small the children are. It only cares how much space it can occupy from its parent. So the Flex and Align widget are useless.
Currently with PageView, it most likely not possible unless we do some calculation and use SizedBox or ConstrainedBox.
I also found this discussion
This can be done, but not with PageView. I had to use SingleChildScrollView with a Row. The reason for that is: if you want to size your PageView properly, you need to build and layout every child it has. PageView does not do that, but Row does, you can even decide how smaller elements should be positioned. PageController also works if you properly set widths of children in the row.
Note that this will only work nicely if you have small amount of pages.
class SizingPageView extends StatelessWidget {
final ScrollPhysics physics;
final PageController controller;
final List<Widget> children;
const SizingPageView({Key key, this.physics, this.controller, this.children})
: super(key: key);
#override
Widget build(BuildContext context) {
Widget transform(Widget input) {
return SizedBox(
width: MediaQuery.of(context).size.width,
child: input,
);
}
return SingleChildScrollView(
physics: physics ?? PageScrollPhysics(),
controller: controller,
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: children.map(transform).toList(),
),
);
}
}
class Solution extends StatelessWidget {
final PageController pageController = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Spacer(
flex: 2,
),
Icon(Icons.done, size: 112),
SizedBox(height: 32),
Text('BEFORE SCROLL'),
SizingPageView(
physics: NeverScrollableScrollPhysics(),
controller: pageController,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('TEXT TEXT TEXT TEXT TEXT TEXT'),
SizedBox(height: 16),
Text('TEXT2 TEXT2 TEXT2 TEXT2 TEXT2 TEXT2'),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('TEXT TEXT TEXT TEXT TEXT TEXT'),
],
),
Text('Third'),
],
),
Text('AFTER SCROLL'),
Spacer(
flex: 2,
),
FlatButton(
child: Text('Next'),
onPressed: () {
var next = pageController.page.round() + 1;
if (next >= 3) {
next = 0;
}
pageController.animateToPage(
next,
duration: Duration(seconds: 1),
curve: Curves.easeInOut,
);
},
)
],
),
);
}
}
You are asking for items inside of the PageView to set the size of PageView itself. This is not going to work with the standard class. If you look at its source, you'll find that uses Viewport inside Scrollable. Viewport grabs maximum size and ignores the size of the children (Slivers). You'll need to use a different approach or maybe create a custom PageView, perhaps one that uses ShrinkWrappingViewport.
https://api.flutter.dev/flutter/widgets/ShrinkWrappingViewport-class.html
By wrapping PageView inside SizedBox, we can get rid of the exception you are getting. (Horizontal viewport was given unbounded height.).
SizedBox(height: 32, child:
PageView(
physics: NeverScrollableScrollPhysics(),
controller: pageController,
children: <Widget>[
// Many widgets go here, I am just simplifying with a Column.
Column(
...
But that will give you a bottom overflow exception of 16 pixels since you have another SizedBox with fixed height inside Column in PageView.
In order to resolve this issue, let's just for time being replace top SizedBox with Container and add color property to it to see how much space and height is available to us.
So, instead of hardcoding the sizedbox height to 32, we can make use of MediaQuery to calculate height for us.
If you print height of the top SizedBox, it will come out to be 597
SizedBox(height: MediaQuery.of(context).size.height,
I for demo, divided the height with 5 to get 119.4 height of the SizedBox widget that contains PageView, which resolves the renderflow exception as well as maintains pageview's location at center of the screen.
If we again use Container to see now how much space and height is available to us, it will look like:
So with this approach, you will not be required to use Expanded or Flexible widgets as you mentioned and won't need to change much of your code.
The working code is as below:
h = MediaQuery.of(context).size.height / 5;
print(h); // 119.4
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.done,size: 112),
SizedBox(height: h, child:
PageView(
physics: NeverScrollableScrollPhysics(),
controller: pageController,
children: <Widget>[
// Many widgets go here, I am just simplifying with a Column.
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('TEXT TEXT TEXT TEXT TEXT TEXT'),
SizedBox(height: 16),
Text('TEXT2 TEXT2 TEXT2 TEXT2 TEXT2 TEXT2'),
],
),
],
),
),
],
),
);
You might need to adjust height per your need, but I hope this resolves your issue and answers your question.
You can simply wrap your widget with Flexible widget https://api.flutter.dev/flutter/widgets/Flexible-class.html

Vertical ListView inside horizontal SingleChildScrollView

I need to build a page with lists of objects that are grouped in a single line. This needs to be scrolled both vertically (to show more groups) and horizontally (to show groups children).
The vertical and horizontal scroll needs to move the items altogether.
Something like this: https://gph.is/g/46gjW0q
I'm doing this with this code:
Widget _buildGroups() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Container(
width: 2000,
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: boardData.length,
itemBuilder: (context, index) {
return Row(children: _buildSingleGroup(index));
},
),
),
);
}
List<Widget> _buildSingleGroup(int groupIndex) {
return List.generate(...);
}
I need this to be horizontally dynamic but here, as you can see, I need to set a width cause otherwise an error occurs:
══════════════════ Exception caught by rendering library ═════════════════════
The following assertion was thrown during performResize()
Vertical viewport was given unbounded width.
Viewports expand in the cross axis to fill their container and constrain their
children to match their extent in the cross axis. In this case, a vertical
viewport was given an unlimited amount of horizontal space in which to expand.
I tried to set "shrinkWrap: true" in the ListView but the result gives another error:
Failed assertion: line 1629 pos 16: 'constraints.hasBoundedWidth': is not true.
So how do you do this with a dynamical ListView width?
The error happens because when ListView tries to get height from its parent to fill the entire height, the parent returns infinity height because SingleChildScrollView doesn't have fixed container height value. To fix that issue you need to limit the height value. For example, I used SizedBox to specify height for ListView.
The following code works for me.
SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(
height: 120, // <-- you should put some value here
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: items.length,
itemBuilder: (context, index) {
return Text('it works');
},
),
),
],
),
);
Please can you explain your guess about the objects size?
I have a solution, but in my solution , flutter will build all objects (In both axes) for first frame. And build again any setState in the three. So its not good solition.
Last scroll offset horizontally
and
But I'll explain anyway.
///
class BothAxisScroll extends StatefulWidget {
#override
_BothAxisScrollState createState() => _BothAxisScrollState();
}
class _BothAxisScrollState extends State<BothAxisScroll> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(30),
child: Container(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: _build(),
),
),
),
),
),
);
}
}
List<Widget> _build() {
return List.generate(
20,
(index) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildSingleGroup(index),
));
}
List<Widget> _buildSingleGroup(int _index) {
return List.generate(
Random().nextInt(20),
(index) {
print('BUILD: $_index');
return Container(
padding: const EdgeInsets.all(15), child: Text("$_index-$index"));
},
);
}
and result in first frame :