A horizontal scrollable table layout is an effective way to display tabular data that exceeds the screen’s width. The following article provides a detailed explanation of how to build such a layout using Flutter, with an emphasis on the key properties and their roles in achieving the desired functionality.
Code Overview
The provided code demonstrates the creation of a table with:
- Rows and columns for structured data presentation.
- A horizontal scrolling mechanism to navigate content.
- Custom styling applied to headers, cells, and borders.
Each section below explains the components and properties used in the implementation.
Widget Structure
ScrollableTable
ScrollableTable is a StatefulWidget responsible for building the user interface of the table. The use of StatefulWidget facilitates state management, though the example does not include dynamic state updates.
Scaffold and SafeArea
return Scaffold(
body: SafeArea(
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ...
),
),
),
);
- Scaffold provides the basic layout structure, supporting components like navigation bars and the main content area.
- SafeArea ensures that the content avoids overlapping system UI elements such as notches and status bars.
- Container centers the table layout and applies padding for aesthetic spacing.
Enabling Horizontal Scrolling
SingleChildScrollView
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ...
)
The SingleChildScrollView widget wraps the table and enables horizontal scrolling. The scrollDirection property is set to Axis.horizontal, specifying the horizontal scroll direction.
Table Styling
Outer Container
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.withOpacity(0.5),
width: 0.5,
),
),
child: Table(...)
)
The outer container adds visual styling around the table:
- BoxDecoration configures rounded corners and borders.
- BorderRadius.circular(12) defines a uniform curve for all corners.
- Border.all(…) applies a light grey border with a thin width for a subtle effect.
Configuring the Table Layout
Table Widget
Table(
border: TableBorder(
verticalInside: BorderSide(...),
horizontalInside: BorderSide(...),
),
defaultColumnWidth: FixedColumnWidth(80.0),
children: [...],
)
The Table widget arranges content in a grid-like layout. Key properties include:
- TableBorder adds dividers between rows and columns for better readability.
- verticalInside specifies the appearance of vertical dividers.
- horizontalInside specifies the appearance of horizontal dividers.
- defaultColumnWidth sets a fixed width of 80.0 for all columns.
Adding Table Content
Header Row
TableRow(
children: [
TableCell(
child: Container(
height: 50,
decoration: const BoxDecoration(
color: Color(0xff28282B),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
),
),
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const Text(
'Score',
style: TextStyle(...),
),
),
),
for (var month in [...])
TableCell(...),
],
),
The header row consists of multiple TableCell widgets. Key points include:
- First Cell: The “Score” column features a dark background (Color(0xff28282B)) and rounded top-left corners.
- Dynamic Content: The header dynamically generates cells for each month using a for loop. The last column, corresponding to December, has rounded top-right corners applied conditionally.
Data Rows
TableRow(
children: [
TableCell(
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 5),
child: const Text(
'A',
style: TextStyle(...),
),
),
),
for (int i = 0; i < 12; i++)
TableCell(
child: Container(
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(...)),
),
),
],
),
Each data row displays:
- Row Identifiers: The first column of each row identifies the row with labels such as “A”, “B”, “C”, and “D”.
- Dynamic Data Cells: A for loop generates 12 cells for each month, displaying numerical values.
All cells are vertically and horizontally centered, with consistent styling for text alignment and appearance.
Customizing Borders
The TableBorder property defines thin dividers between cells:
TableBorder(
verticalInside: BorderSide(
color: const Color(0xff48A14D).withOpacity(0.2),
width: 0.5,
),
horizontalInside: BorderSide(
color: const Color(0xff48A14D).withOpacity(0.3),
width: 0.5,
),
),
Subtle green hues with transparency ensure that borders enhance readability without being overly distracting.
Header Styling Logic
Rounded corners are applied to the first and last header cells using conditional logic:
borderRadius: month.startsWith('Dec')
? const BorderRadius.only(
topRight: Radius.circular(12),
)
: BorderRadius.zero,
This ensures that only the last header cell has a rounded top-right corner, while other cells remain square.
Resulting Layout
The completed implementation creates a visually appealing, horizontally scrollable table layout with:
- A fixed header row styled with bold text and rounded corners.
- Rows containing dynamically generated data cells.
- Borders between rows and columns for clear separation of content.
This layout can be extended further by adding more rows, integrating real-time data, or modifying styling to match application themes. The flexibility of Flutter’s Table widget and related components makes it ideal for such use cases.
Full Code:
import 'package:flutter/material.dart';
class ScrollableTable extends StatefulWidget {
const ScrollableTable({super.key});
@override
State<ScrollableTable> createState() => _ScrollableTableState();
}
class _ScrollableTableState extends State<ScrollableTable> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.withOpacity(0.5), width: 0.5)),
child: Table(
border: TableBorder(
verticalInside: BorderSide(
color: const Color(0xff48A14D).withOpacity(0.2),
width: 0.5),
horizontalInside: BorderSide(
color: const Color(0xff48A14D).withOpacity(0.3),
width: 0.5)),
defaultColumnWidth: const FixedColumnWidth(80.0),
children: [
TableRow(
children: [
TableCell(
child: Container(
height: 50,
decoration: const BoxDecoration(
color: Color(0xff28282B),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12))),
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: const Text(
'Score',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.bold),
),
)),
for (var month in [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
])
TableCell(
child: Container(
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xff28282B),
borderRadius: month.startsWith('Dec')
? const BorderRadius.only(
topRight: Radius.circular(12))
: BorderRadius.zero,
),
child: Text(
month,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
height: 50,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 5),
child: const Text(
'A',
textAlign: TextAlign.start,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
for (int i = 0; i < 12; i++)
TableCell(
child: Container(
alignment: Alignment.center,
height: 50,
child: Text(
'$i',
textAlign: TextAlign.center,
style:
const TextStyle(fontWeight: FontWeight.bold),
),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
alignment: Alignment.center,
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 5),
child: const Text(
'B',
textAlign: TextAlign.start,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
for (int i = 0; i < 12; i++)
TableCell(
child: Container(
height: 50,
alignment: Alignment.center,
child: Text(
'$i',
textAlign: TextAlign.center,
style:
const TextStyle(fontWeight: FontWeight.bold),
),
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
height: 50,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 5),
child: const Text(
'C',
textAlign: TextAlign.start,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
for (int i = 0; i < 12; i++)
TableCell(
child: Container(
height: 50,
alignment: Alignment.center,
child: Text(
'$i',
textAlign: TextAlign.center,
style:
const TextStyle(fontWeight: FontWeight.bold),
), // Placeholder for data cells
),
),
],
),
TableRow(
children: [
TableCell(
child: Container(
height: 50,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 5),
child: const Text(
'D',
textAlign: TextAlign.start,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
for (int i = 0; i < 12; i++)
TableCell(
child: Container(
height: 50,
alignment: Alignment.center,
child: Text(
'$i',
textAlign: TextAlign.center,
style:
const TextStyle(fontWeight: FontWeight.bold),
), // Placeholder for data cells
),
),
],
),
],
),
),
),
),
),
);
}
}
DartOutput:
Conclusion:
This implementation highlights the process of creating a horizontally scrollable table layout in Flutter with a blend of dynamic content and custom styling. The approach ensures both functionality and a visually appealing design, making it well-suited for structured data presentation.