I am making custom dataTable with using row, column and SingleChildScrollView. here my requirement is header must be sticky and table rows must have bidirectional scrollable. I get vertical scrolling but while I scroll horizontally widgets move outside of screen infinitely. have any solution or suggest best plugin/Package for flutter web. (i am making admin panel with flutter web).
i need table like this.
I tried to datatable,datatable2,PaginatedDataTable but problem is this table's row elements take same width and, i need row elements width as per content have it, and row element must have take a widget so that i can add Text,button or as per need in table.
CODE
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class MyCustomDataTable extends StatelessWidget {
const MyCustomDataTable({super.key});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
appBar: AppBar(),
body: Container(
height: size.height,
width: size.width,
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey,
width: 0.45,
),
),
padding: const EdgeInsets.all(10),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
dragStartBehavior: DragStartBehavior.down,
clipBehavior: Clip.none,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: List.generate(
30,
(index) => Container(
width: 80,
height: 30,
margin: const EdgeInsets.only(right: 20),
color: Colors.purple,
child: Center(
child: Text("${1 + index}"),
),
),
).toList(),
),
Row(
children: List.generate(
30,
(index) => Container(
width: 80,
height: 30,
margin: const EdgeInsets.only(right: 20),
color: Colors.purple,
child: Center(
child: Text("${1 + index}"),
),
),
).toList(),
),
const SizedBox(height: 20),
Flexible(
child: SizedBox(
width: double.maxFinite,
child: ListView.builder(
itemCount: 50,
shrinkWrap: true,
itemBuilder: (context, index) {
return Row(
children: List.generate(
30,
(index) => Container(
width: 80,
height: 30,
margin: const EdgeInsets.only(right: 20),
color: Colors.purple.withOpacity(index / 30),
child: Center(
child: Text("${1 + index}"),
),
),
).toList(),
);
},
),
),
),
],
),
),
),
);
}
}
I am getting same output as i want. for that i was used lazy_data_table
actually I made lots of changes as per requred and then i achieve this result.And lots of changes i am doing continuously.
Here i made LazyDataTable2 Class/Component base on LazyDataTable.
And This LazyDataTable2 is giving me result as i want.
Code
class LazyDataTable2 extends StatefulWidget {
final int rows, columns;
final Widget Function(int, int) dataCellBuilder;
final double topHeaderHeight, leftHeaderWidth, cellHeight, cellWidth;
final double flexFactor;
final Map<int, double> customCellWidth;
final List<DataTableHeader> header;
final Widget Function(int)? leftHeaderBuilder;
final Widget? topLeftCornerWidget;
LazyDataTable2({
super.key,
required this.rows,
required this.columns,
required this.dataCellBuilder,
this.topHeaderHeight = 38,
this.cellHeight = 33,
this.cellWidth = 80,
this.leftHeaderWidth = 00, // 100 is best value
this.flexFactor = 0, // 0.625,
this.customCellWidth = const <int, double>{},
required this.header,
this.leftHeaderBuilder,
this.topLeftCornerWidget,
}) : assert(
(flexFactor == 0 && leftHeaderWidth == 0) ||
flexFactor != 0 && leftHeaderWidth != 0,
"flexFactor & leftHeaderWidth both have zero value on not zero value: ErrorMsg",
),
assert(
columns != 0,
"number of columns can't be zero so, columns!=0 this is not true here : ErrorMsg ",
),
assert(
header.isNotEmpty && header.length == columns,
"header must not empty and header have same length as number of column : ErrorMsg",
),
assert(
header.every(
(element) {
final bool res =
(element.header != null && element.headerText == null) ||
(element.header == null && element.headerText != null);
return res;
},
),
"for each and every element of header you can used either header as widget or headerText as String,only one of them : ErrorMsg",
);
@override
State<LazyDataTable2> createState() => _LazyDataTable2State();
}
class _LazyDataTable2State extends State<LazyDataTable2> {
final List<double> _flex = <double>[];
final List<double> _minRowWidth = <double>[];
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
List.generate(
widget.header.length,
(index) {
_flex.add(widget.header[index].flex); // add a flex
_minRowWidth.add(
widget.header[index].minWidth,
); // add a min rowWidth
},
);
if (mounted) setState(() {});
printingWork();
},
);
super.initState();
}
void printingWork() {
debugPrint('flex => $_flex');
_minRowWidth.forEach((element) {
final i = _minRowWidth.indexOf(element);
debugPrint('__minRowWidth[$i] => $element');
});
}
final BoxBorder _boxBorder = Border.all(
color: Colors.transparent,
width: 0.225,
);
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: grey400,
width: 0.45,
),
),
child: LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
final equal = maxWidth / (widget.columns);
/// sum of total flex => sigmaFlex
final sigmaFlex =
_flex.reduce((previous, next) => previous + next);
final List<int> keys =
List.generate(_flex.length, (index) => index);
final List<double> values = List.generate(
_flex.length,
(index) => max(
(equal * widget.columns) * (_flex[index] / sigmaFlex),
_minRowWidth[index],
),
);
final customCellWidth =
Map<int, double>.fromIterables(keys, values);
debugPrint('sigmaFlex => $sigmaFlex');
debugPrint('equal => $equal');
debugPrint('values => $values');
debugPrint('customCellWidth => $customCellWidth');
debugPrint('maxWidth => $maxWidth : Size => $size');
// assigned width
_flex.forEach((element) {
final w = (equal * widget.columns) * (element / sigmaFlex);
final int index = _flex.indexOf(element);
debugPrint(
'rowWidth => calculated : $w => Used[$index] : ${max(w, _minRowWidth[index])}');
});
debugPrint(
'calculatedWidth : ${values.reduce((value, element) => value + element)}',
);
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: (widget.cellHeight) * (widget.rows) +
widget.topHeaderHeight +
1.2,
maxWidth: maxWidth,
minHeight: 0.0,
minWidth: 0.0,
),
child: LazyDataTable(
tableTheme: LazyDataTableTheme(
columnHeaderColor: primaryPallet5,
columnHeaderBorder: const Border(
left: BorderSide(
color: grey400,
width: 0.45,
),
bottom: BorderSide(
color: grey400,
width: 0.45,
),
),
cellBorder: const Border(
left: BorderSide(
color: grey400,
width: 0.45,
),
),
rowHeaderBorder: _boxBorder,
cornerColor: primaryPallet5,
cornerBorder: const Border(
bottom: BorderSide(
color: grey400,
width: 0.45,
),
),
),
rows: widget.rows,
columns: widget.columns,
tableDimensions: LazyDataTableDimensions(
cellHeight: widget.cellHeight,
cellWidth: widget.cellWidth,
customCellHeight: {},
customCellWidth: widget.customCellWidth.isNotEmpty
? widget.customCellWidth
: customCellWidth,
topHeaderHeight: widget.topHeaderHeight,
leftHeaderWidth: widget.leftHeaderWidth,
bottomHeaderHeight: 0.0,
rightHeaderWidth: 0.0,
),
dataCellBuilder: widget.dataCellBuilder,
topLeftCornerWidget: widget.topLeftCornerWidget,
topHeaderBuilder: (i) {
if (widget.header[i].header != null) {
return SizedBox(
child: widget.header[i].header,
);
} else {
return Padding(
padding: const EdgeInsets.only(
left: 15,
top: 8,
right: 15,
),
child: Text(
"${widget.header[i].headerText}",
style: MyTextStyles.medium.copyWith(
color: appPrimary,
fontSize: 14.5,
),
),
);
}
},
leftHeaderBuilder: widget.leftHeaderBuilder,
),
);
},
),
),
/// *page controller
/// !Future Reference
],
);
}
}
class DataTableHeader {
final String? headerText;
final Widget? header;
final double minWidth, flex;
DataTableHeader({
this.headerText,
this.header,
this.minWidth = 100, // set default value
this.flex = 1, // set default value
});
}
Sample Example
class CustomerNotAgree extends StatelessWidget {
const CustomerNotAgree({super.key});
final List<Map<String, dynamic>> rows = const [
{
'title': 'Battery',
'date': '29/04/2023',
},
{
'title': 'Tire',
'date': '29/04/2023',
},
{
'title': 'Head Lights',
'date': '29/04/2023',
},
{
'title': 'Water Albo Upper',
'date': '29/04/2023',
},
{
'title': 'Air Filter ',
'date': '29/04/2023',
},
{
'title': 'Oil Filter',
'date': '29/04/2023',
},
{
'title': 'Engine Oil',
'date': '29/04/2023',
},
];
@override
Widget build(BuildContext context) {
return ScreenLayout(
isSecondaryAppBar: true,
child: Padding(
padding: Responsive.isDesktop(context)
? desktopPadding
: Responsive.isTablet(context)
? tabletPadding
: mobilePadding,
child: LazyDataTable2(
rows: rows.length,
columns: 3,
header: [
DataTableHeader(
flex: 0.5,
minWidth: 60,
headerText: 'No.',
),
DataTableHeader(
flex: 4.5,
minWidth: 180,
headerText: 'Parts / Service Name',
),
DataTableHeader(
flex: 4,
minWidth: 100,
headerText: 'Date',
),
],
dataCellBuilder: (r, c) {
final srNo = r;
if (c == 0) {
return Padding(
padding: tableCellPadding,
child: Text(
'${srNo + 1}',
style: tableFirstColumnStyle,
),
);
} else if (c == 1) {
return Padding(
padding: tableCellPadding,
child: Text(
rows[srNo]['title'],
style: tableColumnStyle,
),
);
} else {
return Padding(
padding: tableCellPadding,
child: Text(
rows[srNo]['date'],
style: tableColumnStyle,
),
);
}
},
),
),
);
}
}