Flutter DataTable Cross axis scroll

eye-catch Dart and Flutter

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.

https://github.com/yuto-yuto/flutter_samples/blob/main/lib/cross_axis_scroll.dart
Sponsored links

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.

Sponsored links

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.

listview-vertical

The text is wrapped and it looks not nice.

If Axishorizontal is set to scrollDirection, it looks like this.

listview-horizontal

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

  1. Biruk says:

    Have you found any solution to the web horizontal scrolling issue?

    • Yuto Yuto says:

      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?

Copied title and URL