When lots of data need to be shown on a screen, it requires a large view area that definitely doesn’t fit the restricted screen size, especially for mobile. It might be a design issue but there are some cases that we really want to place a data table there to show them. How can we make the view scrollable for both vertical and horizontal directions?
Go to this repository if you need the complete code.
Conclusion
It is actually simpler than I expected. The following two are the solutions.
- Use SingleChildScrollView with Column or Row widget
- Use two SingleChildScrollView
Let’s check the actual code.
Impossible to make a ListView cross axis scroll?
We sometimes need to handle long texts like this below.
final _data = const [
"1 long long word assdaffadfadafdafdsfasfdafdsaffdazfdasfdafdfafafdfadfafds",
"2 long long word assdaffadfadafdafdsfasfdafdsaffdazfdasfdafdfafafdfadfafds",
"3 long long word assdaffadfadafdafdsfasfdafdsaffdazfdasfdafdfafafdfadfafds",
"4 long long word assdaffadfadafdafdsfasfdafdsaffdazfdasfdafdfafafdfadfafds",
"5 long long word assdaffadfadafdafdsfasfdafdsaffdazfdasfdafdfafafdfadfafds",
"6 long long word assdaffadfadafdafdsfasfdafdsaffdazfdasfdafdfafafdfadfafds",
"7 long long word assdaffadfadafdafdsfasfdafdsaffdazfdasfdafdfafafdfadfafds",
"8 long long word assdaffadfadafdafdsfasfdafdsaffdazfdasfdafdfafafdfadfafds",
"9 long long word assdaffadfadafdafdsfasfdafdsaffdazfdasfdafdfafafdfadfafds",
];
URL is I think a good example in the real world. It can easily be a long text. If we need to show them with ListView it looks like this.
The text is wrapped and it looks not nice.
If Axishorizontal
is set to scrollDirection, it looks like this.
This is also not nice.
The first attempt to solve this problem is wrapping ListView by SingleChildScrollView
. ListView is scrollable in the vertical direction and SingleChildScrollView in the horizontal direction.
Widget _createCrossAxis2() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: _createListView(Axis.vertical),
);
}
Widget _createListView(Axis direction) {
return ListView.builder(
scrollDirection: direction,
itemCount: _data.length,
itemBuilder: (context, index) => Card(child: Text(_data[index])),
);
}
It doesn’t go well. The following error happens.
════════ 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.
The relevant error-causing widget was
ListView
lib\cross_axis_scroll.dart:61
When the exception was thrown, this was the stack
The ListView widget is wrapped in the following container in the example.
Widget _wrap(BuildContext context, Widget widget) {
return Container(
child: widget,
height: 150,
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(5),
margin: EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 1),
),
);
}
Use two SingleChildScrollView with Column or Row
ListView couldn’t be used in SingleChildScrollView. The next try is use to SingleChildScrollView with Column or Row. Set a different direction to scrollDirection
property for each widget.
Widget _createCrossAxis() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: _data.map((e) => Card(child: Text(e))).toList(),
),
),
);
}
Cross axis scrollable DataTable
If the data contains column info, we might want to show it on the view. In this case, DataTable is a proper widget.
Widget _createDataTable() {
final columns = List.generate(
20,
(index) => DataColumn(
label: Text("column $index"),
),
);
final rows = List.generate(
20,
(rowIndex) => DataRow(
cells: List.generate(
20,
(cellIndex) => DataCell(
Text("data $cellIndex-$rowIndex"),
),
),
),
);
final dataTable = DataTable(
columns: columns,
rows: rows,
border: TableBorder.all(color: Colors.grey),
);
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: dataTable,
),
);
}
For web
To make it scrollable for the web version, the following code needs to be added.
class MyCustomScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
}
MaterialApp(
scrollBehavior: MyCustomScrollBehavior(),
...
The widget is scrollable when dragging it by the mouse. However, when I added a scroll bar it wasn’t scrollable by it. If I find the solution I will update.
If the scroll doesn’t work, try to add a controller respectively.
final _verticalScrollController = ScrollController();
final _horizontalScrollController = ScrollController();
...
SingleChildScrollView(
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
controller: _verticalScrollController,
scrollDirection: Axis.vertical,
child: dataTable,
),
);
Show the Scrollbar always
If you always want to show Scrollbar, you need to wrap the SingleChildScrollView
by Scrollbar
and set true to thumbVisibility
.
Scrollbar is displayed only when it’s scrolled to the end of the widget
You might implement it in the following way. Scrollbar wraps SingleChildScrollView.
// NOT WORK AS EXPECTED
final _verticalScrollController1 = ScrollController();
final _horizontalScrollController1 = ScrollController();
Widget _createDataTableCrossAxisWithBar1() {
return Scrollbar(
controller: _verticalScrollController1,
thumbVisibility: true,
trackVisibility: true, // make the scrollbar easy to see
child: SingleChildScrollView(
controller: _verticalScrollController1,
scrollDirection: Axis.vertical,
child: Scrollbar(
controller: _horizontalScrollController1,
thumbVisibility: true,
trackVisibility: true, // make the scrollbar easy to see
child: SingleChildScrollView(
controller: _horizontalScrollController1,
scrollDirection: Axis.horizontal,
child: _generateColorfulDataTable(), // DataTable here
),
),
),
);
}
The result looks like the following video.
The only vertical scrollbar is displayed. The horizontal scrollbar is displayed only when the bottom of the widget is shown. Why?
Because the Scrollbar
widget for the horizontal bar is wrapped by SingleChildScrollView
which is used for the vertical scrollbar. The scrollbar is shown at the bottom of the widget in this case.
Make the both scrollbars always visible
The problem was that Scrollbar
widget is wrapped by SingleChildScrollView
. Then, let’s swap the widgets. A Scrollbar
wraps a Scrollbar
.
final _verticalScrollController2 = ScrollController();
final _horizontalScrollController2 = ScrollController();
Widget _createDataTableCrossAxisWithBar2() {
return Scrollbar(
controller: _verticalScrollController2,
thumbVisibility: true,
trackVisibility: true, // make the scrollbar easy to see
child: Scrollbar(
controller: _horizontalScrollController2,
thumbVisibility: true,
trackVisibility: true, // make the scrollbar easy to see
notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView(
controller: _verticalScrollController2,
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
controller: _horizontalScrollController2,
scrollDirection: Axis.horizontal,
child: _generateColorfulDataTable(),
),
),
),
);
}
Watch the following video. Both scrollbars are visible in this way.
Don’t forget to add the following line to the wrapped Scrollbar
.
notificationPredicate: (notif) => notif.depth == 1,
Comments
Have you found any solution to the web horizontal scrolling issue?
Hmm… I don’t remember it well but a controller is set in my private project. I added the code in the article to the bottom.
Can you try it?