Fixed column and row header for DataTable on Flutter Dart - flutter

I've build a table on Flutter Dart using DataTable. This table is very large, and I'm using both Vertical and Horizontal scrolling.
When scrolling I lose reference to columns, I need to know what is the column.
As example. On the screenshot i don't know what the numbers 20.0 and 25.0 on the means, unless I scroll to the top.
I've added a GIF example of what i want to achieve. (Using LibreOffice). I need fixed column name (first row).
Example of the table, while scrolling around the middle of the table:
Example of what i want to do:
Code sample for my table:
return SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: MyDataSet.getColumns(),
rows: widget._data.map<DataRow>((row) => DataRow(
onSelectChanged: (d) {
setState(() {
selectedRow = d ? row.hashCode : null;
});
},
selected: row.hashCode == selectedRow,
cells: MyDataSet.toDataCells(row)
)).toList()
)
),
);
Missing code sample:
return columns.map<DataColumn>((name) => DataColumn(
label: Text(name, style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black),)
)).toList();
Update (24/10/2019)
Current code works well if header name is the same size as cell content. Otherwise both sizes will be different.
Update (21/02/2020)
People created a package to do that. :D
https://pub.dev/packages/table_sticky_headers
Image from pub.dev!

I could come up with a workaround using scroll controllers, looks like this: Video
Basically it's an horizontal scroll for the first row, a vertical scroll for the first column and a mixed horizontal and vertical scroll for the subtable. Then when you move the subtable, its controllers move the column and the row.
Here is a custom widget with an example of how to use it:
final _rowsCells = [
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8],
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8],
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8],
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8]
];
final _fixedColCells = [
"Pablo",
"Gustavo",
"John",
"Jack",
"Pablo",
"Gustavo",
"John",
"Jack",
"Pablo",
"Gustavo",
"John",
"Jack",
"Pablo",
"Gustavo",
"John",
"Jack",
];
final _fixedRowCells = [
"Math",
"Informatics",
"Geography",
"Physics",
"Biology"
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: CustomDataTable(
rowsCells: _rowsCells,
fixedColCells: _fixedColCells,
fixedRowCells: _fixedRowCells,
cellBuilder: (data) {
return Text('$data', style: TextStyle(color: Colors.red));
},
),
);
}
class CustomDataTable<T> extends StatefulWidget {
final T fixedCornerCell;
final List<T> fixedColCells;
final List<T> fixedRowCells;
final List<List<T>> rowsCells;
final Widget Function(T data) cellBuilder;
final double fixedColWidth;
final double cellWidth;
final double cellHeight;
final double cellMargin;
final double cellSpacing;
CustomDataTable({
this.fixedCornerCell,
this.fixedColCells,
this.fixedRowCells,
#required this.rowsCells,
this.cellBuilder,
this.fixedColWidth = 60.0,
this.cellHeight = 56.0,
this.cellWidth = 120.0,
this.cellMargin = 10.0,
this.cellSpacing = 10.0,
});
#override
State<StatefulWidget> createState() => CustomDataTableState();
}
class CustomDataTableState<T> extends State<CustomDataTable<T>> {
final _columnController = ScrollController();
final _rowController = ScrollController();
final _subTableYController = ScrollController();
final _subTableXController = ScrollController();
Widget _buildChild(double width, T data) => SizedBox(
width: width, child: widget.cellBuilder?.call(data) ?? Text('$data'));
Widget _buildFixedCol() => widget.fixedColCells == null
? SizedBox.shrink()
: Material(
color: Colors.lightBlueAccent,
child: DataTable(
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: [
DataColumn(
label: _buildChild(
widget.fixedColWidth, widget.fixedColCells.first))
],
rows: widget.fixedColCells
.sublist(widget.fixedRowCells == null ? 1 : 0)
.map((c) => DataRow(
cells: [DataCell(_buildChild(widget.fixedColWidth, c))]))
.toList()),
);
Widget _buildFixedRow() => widget.fixedRowCells == null
? SizedBox.shrink()
: Material(
color: Colors.greenAccent,
child: DataTable(
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: widget.fixedRowCells
.map((c) =>
DataColumn(label: _buildChild(widget.cellWidth, c)))
.toList(),
rows: []),
);
Widget _buildSubTable() => Material(
color: Colors.lightGreenAccent,
child: DataTable(
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: widget.rowsCells.first
.map((c) => DataColumn(label: _buildChild(widget.cellWidth, c)))
.toList(),
rows: widget.rowsCells
.sublist(widget.fixedRowCells == null ? 1 : 0)
.map((row) => DataRow(
cells: row
.map((c) => DataCell(_buildChild(widget.cellWidth, c)))
.toList()))
.toList()));
Widget _buildCornerCell() =>
widget.fixedColCells == null || widget.fixedRowCells == null
? SizedBox.shrink()
: Material(
color: Colors.amberAccent,
child: DataTable(
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: [
DataColumn(
label: _buildChild(
widget.fixedColWidth, widget.fixedCornerCell))
],
rows: []),
);
#override
void initState() {
super.initState();
_subTableXController.addListener(() {
_rowController.jumpTo(_subTableXController.position.pixels);
});
_subTableYController.addListener(() {
_columnController.jumpTo(_subTableYController.position.pixels);
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Row(
children: <Widget>[
SingleChildScrollView(
controller: _columnController,
scrollDirection: Axis.vertical,
physics: NeverScrollableScrollPhysics(),
child: _buildFixedCol(),
),
Flexible(
child: SingleChildScrollView(
controller: _subTableXController,
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
controller: _subTableYController,
scrollDirection: Axis.vertical,
child: _buildSubTable(),
),
),
),
],
),
Row(
children: <Widget>[
_buildCornerCell(),
Flexible(
child: SingleChildScrollView(
controller: _rowController,
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
child: _buildFixedRow(),
),
),
],
),
],
);
}
}
Since the first column, the first row and the subtable are independent, I had to create a DataTable for each one. And since DataTable has headers that can't be removed, the headers of the first column and the subtable are hidden by the first row.
Also, I had to make the first column and first row not manually scrollable because if you scroll them the subtable won't scroll.
This might not be the best solution, but at the moment doesn't seem to be another way to do it. You could try to improve this approach, maybe using Table or other widgets instead of DataTable at least you could avoid hiding the headers of the subtable and first column.

A few months back I had similar issue with limmited capabilities of stock DataTable and PaginatedDataTable2 widgets which didn't allow to fix the header. Eventually I took those widgets appart and created my own versions but with blackjack and fixed header row. Here's the plug-in on pub.dev:
https://pub.dev/packages/data_table_2
The classes DataTable2 and PaginatedDataTable2 provide exactly the same APIs as the original versions.
NOTE: these one only implement sticky top rows, leftmost columns are not fixed/sticky

Try the flutter package horizontal_data_table
A Flutter Widget that create a horizontal table with fixed column on left hand side.
dependencies:
horizontal_data_table: ^2.5.0

Below Given Pablo Barrera answer is quite interesting, I have corrected and modified this answer with an enhanced feature, also it is easy to handle DataTable with fixed rows and columns. This DataTable you can customize as per your requirements.
import 'package:flutter/material.dart';
class DataTablePage extends StatelessWidget {
const DataTablePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: CustomDataTable(
fixedCornerCell: '',
borderColor: Colors.grey.shade300,
rowsCells: _rowsCells,
fixedColCells: _fixedColCells,
fixedRowCells: _fixedRowCells,
),
),
),
),
);
}
}
final _rowsCells = [
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8],
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8],
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8],
[7, 8, 10, 8, 7],
[10, 10, 9, 6, 6],
[5, 4, 5, 7, 5],
[9, 4, 1, 7, 8]
];
final _fixedColCells = [
"Pablo",
"Gustavo",
"John",
"Jack",
"Pablo",
"Gustavo",
"John",
"Jack",
"Pablo",
"Gustavo",
"John",
"Jack",
"Pablo",
"Gustavo",
"John",
"Jack",
];
final _fixedRowCells = [
"Math",
"Informatics",
"Geography",
"Physics",
"Biology"
];
class CustomDataTable<T> extends StatefulWidget {
final T fixedCornerCell;
final List<T> fixedColCells;
final List<T> fixedRowCells;
final List<List<T>> rowsCells;
final double fixedColWidth;
final double cellWidth;
final double cellHeight;
final double cellMargin;
final double cellSpacing;
final Color borderColor;
const CustomDataTable({
super.key,
required this.fixedCornerCell,
required this.fixedColCells,
required this.fixedRowCells,
required this.rowsCells,
this.fixedColWidth = 60.0,
this.cellHeight = 56.0,
this.cellWidth = 120.0,
this.cellMargin = 10.0,
this.cellSpacing = 10.0,
required this.borderColor,
});
#override
State<StatefulWidget> createState() => CustomDataTableState();
}
class CustomDataTableState<T> extends State<CustomDataTable<T>> {
final _columnController = ScrollController();
final _rowController = ScrollController();
final _subTableYController = ScrollController();
final _subTableXController = ScrollController();
Widget _buildChild(double width, T data) => SizedBox(
width: width,
child: Text(
'$data',
textAlign: TextAlign.center,
),
);
TableBorder _buildBorder({
bool top = false,
bool left = false,
bool right = false,
bool bottom = false,
bool verticalInside = false,
}) {
return TableBorder(
top: top ? BorderSide(color: widget.borderColor) : BorderSide.none,
left: left ? BorderSide(color: widget.borderColor) : BorderSide.none,
right: right ? BorderSide(color: widget.borderColor) : BorderSide.none,
bottom: bottom ? BorderSide(color: widget.borderColor) : BorderSide.none,
verticalInside: verticalInside
? BorderSide(color: widget.borderColor)
: BorderSide.none,
);
}
Widget _buildFixedCol() => DataTable(
border: _buildBorder(right: true),
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: [
DataColumn(
label:
_buildChild(widget.fixedColWidth, widget.fixedColCells.first))
],
rows: widget.fixedColCells
.map((c) =>
DataRow(cells: [DataCell(_buildChild(widget.fixedColWidth, c))]))
.toList());
Widget _buildFixedRow() => DataTable(
border: _buildBorder(verticalInside: true, bottom: true),
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: widget.fixedRowCells
.map(
(c) => DataColumn(
label: _buildChild(widget.cellWidth, c),
),
)
.toList(),
rows: const [],
);
Widget _buildSubTable() => Material(
color: Colors.white,
child: DataTable(
border: _buildBorder(verticalInside: true),
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: widget.rowsCells.first
.map((c) => DataColumn(label: _buildChild(widget.cellWidth, c)))
.toList(),
rows: widget.rowsCells
.map(
(row) => DataRow(
cells: row
.map((c) => DataCell(_buildChild(widget.cellWidth, c)))
.toList()),
)
.toList()));
Widget _buildCornerCell() => DataTable(
border: _buildBorder(bottom: true, right: true),
horizontalMargin: widget.cellMargin,
columnSpacing: widget.cellSpacing,
headingRowHeight: widget.cellHeight,
dataRowHeight: widget.cellHeight,
columns: [
DataColumn(
label: _buildChild(
widget.fixedColWidth,
widget.fixedCornerCell,
),
)
],
rows: const [],
);
#override
void initState() {
super.initState();
_subTableXController.addListener(() {
_rowController.jumpTo(_subTableXController.position.pixels);
});
_subTableYController.addListener(() {
_columnController.jumpTo(_subTableYController.position.pixels);
});
}
#override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: widget.borderColor),
),
child: Column(
children: [
Row(
children: [
_buildCornerCell(),
Flexible(
child: SingleChildScrollView(
controller: _rowController,
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
child: _buildFixedRow(),
),
),
],
),
Expanded(
child: Row(
children: [
SingleChildScrollView(
controller: _columnController,
scrollDirection: Axis.vertical,
physics: const NeverScrollableScrollPhysics(),
child: _buildFixedCol(),
),
Flexible(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
controller: _subTableXController,
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
controller: _subTableYController,
scrollDirection: Axis.vertical,
child: _buildSubTable(),
),
),
),
],
),
),
],
),
);
}
}
Output:

Related

First index doesnt show highlighted colour

The bounty expires in 6 days. Answers to this question are eligible for a +50 reputation bounty.
wuuyungwuu is looking for an answer from a reputable source.
I am trying to select multiple components in this Wrap.toList() but every first index I select doesn't change its colour indicating that it is selected. It is selected in the list but it doesn't show.
See the 4 components I have selected.
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
shrinkWrap: true,
children: cC.allCommodityList.map((order) {
return InkWell(
onTap: () {
setState(() {
selectedItems.contains(order)
? selectedItems.remove(order)
: selectedItems.add(order);
commodityName = order.commodityName;
commodityid = order.commodityID;
// }
});
},
child: Card(
child: Column(
children: [
Expanded(
child: selectedItems.contains(order)
? SvgPicture.asset(
'assets/toiletpaper.svg',
color: Color.fromRGBO(0, 76, 32, 1),
)
: SvgPicture.asset(
'assets/toiletpaper.svg',
)),
selectedItems.contains(order)
? TopBorderNoTap(
listColor: [
Color.fromRGBO(229, 229, 229, 1),
Color.fromRGBO(0, 76, 32, 1),
],
text: order.commodityName.toString(),
color: Colors.white,
textColor: Colors.white)
: TopBorderNoTap(
listColor: [
Color.fromRGBO(229, 229, 229, 1),
Colors.white
],
text: order.commodityName.toString(),
textColor: Colors.black,
)
],
)),
);
}).toList(),
))),
This is my model class, not the full code but it just returns from json and to json
CommodityModel({
this.commodityID,
this.commodityName,
this.commodityImage,
});
CommodityModel.fromJson(Map<String, dynamic> json) {
commodityID = json['commodityID'];
commodityName = json['commodityName'];
commodityImage = json['commodityImage'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['commodityID'] = commodityID;
data['commodityName'] = commodityName;
data['commodityImage'] = commodityImage;
You can try this approach to select & d-select model list item.
class MyNewWidget extends StatefulWidget {
const MyNewWidget({super.key});
#override
State<MyNewWidget> createState() => _MyNewWidgetState();
}
class _MyNewWidgetState extends State<MyNewWidget> {
final List<CommodityModel> allCommodityList = [
CommodityModel(
commodityID: 1,
commodityName: "Toilet Paper",
commodityImage: "commodityImage"),
CommodityModel(
commodityID: 2,
commodityName: "Paper Towels",
commodityImage: "commodityImage"),
CommodityModel(
commodityID: 3,
commodityName: "Hand shop",
commodityImage: "commodityImage"),
CommodityModel(
commodityID: 4,
commodityName: "Air freshner",
commodityImage: "commodityImage")
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
childAspectRatio: 3 / 2,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
itemCount: allCommodityList.length,
itemBuilder: (BuildContext ctx, index) {
final order = allCommodityList[index];
return Container(
alignment: Alignment.center,
child: InkWell(
onTap: () {
setState(() {
order.isSelected = !order.isSelected;
});
},
child: Card(
child: Column(
children: [
Expanded(
child: SvgPicture.asset(
'assets/toiletpaper.svg',
color: order.isSelected
? const Color.fromRGBO(0, 76, 32, 1)
: null,
)),
Row(
children: [
Expanded(
child: Container(
color: order.isSelected
? const Color.fromRGBO(0, 76, 32, 1)
: null,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(
order.commodityName ?? "",
style: TextStyle(
color: order.isSelected
? Colors.white
: Colors.black),
)),
),
),
),
],
)
],
)),
),
);
}),
),
);
}
}
class CommodityModel {
int? commodityID;
String? commodityName;
String? commodityImage;
bool isSelected =
false; // Add key for selection handle. You can also handle with single orderID Array
CommodityModel({this.commodityID, this.commodityName, this.commodityImage});
CommodityModel.fromJson(Map<String, dynamic> json) {
commodityID = json['commodityID'];
commodityName = json['commodityName'];
commodityImage = json['commodityImage'];
isSelected = json['isSelected'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['commodityID'] = commodityID;
data['commodityName'] = commodityName;
data['commodityImage'] = commodityImage;
data['isSelected'] = isSelected;
return data;
}
}

Is there time series chart on fl_chart or is there any way to make line_chart a time series one?

Using line charts i cannot display time series data correctly. Is there a way to make it happen using any packages other than syncfusion_flutter_charts and charts_flutter.
Here is the code I tried. But it results in incorrect graph.
Here is the expected output
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
class FlChartExample extends StatelessWidget {
FlChartExample({Key? key}) : super(key: key);
List<SalesDataPair> sales = [
SalesDataPair( DateTime(2017, 9, 20), 25),
SalesDataPair( DateTime(2017, 9, 24), 50),
SalesDataPair( DateTime(2017, 10, 3), 100),
SalesDataPair( DateTime(2017, 10, 11), 75),
];
#override
Widget build(BuildContext context) {
final List<FlSpot> dummyData1 = List.generate(4, (index) {
return FlSpot(sales[index].date.day.toDouble(), sales[index].amount);
});
return Scaffold(
body: SafeArea(
child: Container(
padding: const EdgeInsets.all(20),
width: double.infinity,
child: LineChart(
LineChartData(
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: dummyData1,
isCurved: false,
barWidth: 3,
color: Colors.red,
),
],
),
),
),
),
);
}
}
class SalesDataPair {
SalesDataPair(this.date, this.amount);
final DateTime date;
final double amount;
}
I understood why the graph is wrongly displayed.

Merged two lists and apply search filter on it using flutter

I have two lists one is static that has month_names and another list is dynamic have data is comes from API.
Months Lists is static:
List monthName= ["july", "august","september", "october","november", "december","january","febuary","march","april","may","june"];
Data list is dynamic comes from API:
var updatedList= [
{
"transGroup": 1,
"transType": 0,
"serviceTypeId": 0,
"serviceDescription": "Opening Balance",
"financialYear": "2022/2023",
"july": 54818.34,
"august": 54818.34,
"september": 0,
"october": 0,
"november": 0,
"december": 0,
"january": 0,
"febuary": 0,
"march": 0,
"april": 0,
"may": 0,
"june": 0
},
{
"transGroup": 990,
"transType": 0,
"serviceTypeId": 0,
"serviceDescription": "Closing Balance",
"financialYear": "2022/2023",
"july": 54818.34,
"august": 54818.34,
"september": 0,
"october": 0,
"november": 0,
"december": 0,
"january": 0,
"febuary": 0,
"march": 0,
"april": 0,
"may": 0,
"june": 0
}
];
Note: in above list serviceDescription sometimes has been change like Opening Balance, Closing Balance, Total,Interval,Intrest...etc.
I have create myData list model from json_to_dart
Then I apply the logic below
List data = [];
int? monthIndex;
for (var i = 0; i < updatedList.length; i++) {
if (monthName[monthIndex] == "July") {
data.add(updatedList[i].july.toString());
} else if (monthName[monthIndex] == "August") {
data.add(updatedList[i].august.toString());
} //upto all 12 months
}
It gives the result like july data in july card widget , august data in august card widget and so on.....
Widget:
return ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: updatedList.length,
itemBuilder: (context, childIndex) {
return ListTile(
visualDensity: const VisualDensity(
vertical: -4,
),
contentPadding: EdgeInsets.zero,
title: Text(
updatedList[childIndex].serviceDescription.toString(),
style: caption,
),
trailing: Text(
data[childIndex],
style: dataTitle.copyWith(fontSize: 14),
),
);
},
);
My result display correctly in my widgets but I want to add search filter on both lists
I have try to merged above two lists
var mergedLists = List.from(monthName)..addAll(updatedList);
print(mergedLists);
he gives me below like result:
[July, August, September, October, November, December, January, February, March, April, May, June, Instance of 'UpdatedListModel ',Instance of 'UpdatedListModel ',Instance of 'UpdatedListModel ',....]
Search filter
List results = [];
List displayList = [];
//search function
void runFilter(String enteredKeyword) {
if (enteredKeyword.isEmpty) {
results = monthName;
displayList = monthName;
setState(() {});
} else {
setState(() {
results = monthName.where((data) {
return data.toLowerCase().contains(
enteredKeyword.toLowerCase(),
);
}).toList();
displayList = results;
setState(() {});
});
}
}
Main Listview Widget:
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: displayList.length,
itemBuilder: (context, index) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 5,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
displayList[index],
style: dataTitle.copyWith(fontSize: 16),
),
apiData(index, transactionLists)
],
),
),
);
},
),
You need to format your data,First define new variable like this:
List<Map<String, dynamic>> finalData = [];
then define a function like this:
formatData() {
List<Map<String, dynamic>> result = List<Map<String, dynamic>>.generate(
monthName.length,
(index) => {"month": monthName[index], "service": []});
for (var item in updatedList) {
for (var month in monthName) {
int index = result.indexWhere((element) => element["month"] == month);
(result[index]["service"] as List).add({
"serviceDescription": item["serviceDescription"],
"value": item[month]
});
}
}
setState(() {
finalData = result;
});
}
and call it in initState like this:
#override
void initState() {
super.initState();
formatData();
}
then use finalData to create your main listview that contain the cards and use finalData's service to create child listview. Also use it to do your search function like this:
void runFilter(String enteredKeyword) {
if (enteredKeyword.isEmpty) {
results = finalData;
displayList = finalData;
setState(() {});
} else {
setState(() {
results = finalData.where((data) {
return data["month"].toLowerCase().contains(
enteredKeyword.toLowerCase(),
);
}).toList();
displayList = results;
setState(() {});
});
}
}
and change your main list to this:
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: displayList.length,
itemBuilder: (context, index) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 5,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
displayList[index]["month"],
style: dataTitle.copyWith(fontSize: 16),
),
//apiData(index, displayList[index]["service"])
],
),
),
);
},
),
then in your inner listview try this:
return ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: displayList[index]["service"].length,// index here is the main listview index
itemBuilder: (context, childIndex) {
var services = displayList[index]["service"];// index here is the main listview index
return ListTile(
visualDensity: const VisualDensity(
vertical: -4,
),
contentPadding: EdgeInsets.zero,
title: Text(
services[childIndex]["serviceDescription"].toString(),
style: caption,
),
trailing: Text(
services[childIndex]["value"].toString(),
style: dataTitle.copyWith(fontSize: 14),
),
);
},
);

Rendering Filtered List in Flutter

I'm having trouble displaying a filtered list in my widget. It works up to the point of printing the filtered data in the list as per the query that is passed but not when that exact data needs to be displayed. I believe I will have to update the list with the filtered data every time I type in a query but I just cannot figure out how and where I need to use setState to update that. My code and the outputs are as follows:
Initially, the entire list gets rendered but the moment I type in a query string, the list is supposed to get modified with only the data that matched the query. This is not something that's happening at the moment. The list tends to remain as it is.
However, when I print the filtered data, it seems to work just fine(_searchResult printed in the searchData method below).
[
{product_id: 8, restaurant_name: Mocambo, restaurant_id: 6, product_name: Kaju Paneer, product_description: Tasty yummy paneer gravy dish, product_image: /public/assets/product/lgml5L03-19-41.jpg, product_selling_price: 320},
{product_id: 5, restaurant_name: City Club, restaurant_id: 1, product_name: Palak Paneer, product_description: Tasty silky gravy with goodness of palak, product_image: /public/assets/product/C6pGz101-42-17.jpg, product_selling_price: 180},
{product_id: 4, restaurant_name: City Club, restaurant_id: 1, product_name: Shahi Paneer, product_description: Tasty Paneer main course dish, product_image: /public/assets/product/vgI1dR01-29-18.jpg, product_selling_price: 240}
]
The code:
The method that filters. (Please note that the filtering is performed after the data is fetched from the server. For my convenience, I decided to convert it into a list)
class PopularDishesProvider with ChangeNotifier {
Map<String, dynamic> _dishes = {};
final List<dynamic> _searchDish = [];
List<dynamic> _searchResult = [];
List<dynamic> get searchDish {
return [..._searchDish];
}
List<dynamic> get searchResult {
return [..._searchResult];
}
Future<void> searchData(String query) async {
final url = Uri.parse(baseUrl + 'api/all_products');
final response = await http.get(url);
PopularDishes popularDishes = popularDishesFromJson(response.body); //This method converts the response into Dart model
_dishes = popularDishes.toJson();
_dishes['data'].forEach((value) => _searchDish.add(value));
_searchResult = _searchDish.where((element) {
final name = element['product_name'].toLowerCase();
final searchQuery = query.toLowerCase();
return name.contains(searchQuery);
}).toList();
print(_searchResult);
notifyListeners();
}
}
The widget where this is supposed to be rendered:
class SearchState extends State<Search> {
final _controller = TextEditingController();
bool value = true;
String query = '';
List<dynamic> search = [];
PopularDishesProvider popular = PopularDishesProvider();
#override
void initState() { //This is to make the API Call for the first time
// TODO: implement initState
Provider.of<PopularDishesProvider>(context, listen: false)
.searchData('');
});
super.initState();
}
#override
Widget build(BuildContext context) {
final height = MediaQuery.of(context).size.height;
final width = MediaQuery.of(context).size.width;
final textScale = MediaQuery.of(context).textScaleFactor * 1.2;
final searchProvider = Provider.of<PopularDishesProvider>(context).searchResult;
PopularDishesProvider popular = PopularDishesProvider();
// TODO: implement build
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
elevation: 5,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
// backgroundColor: Colors.green,
titleSpacing: 0,
toolbarHeight: 100,
title: Column(
children: [
Container(
width: double.infinity,
height: 40,
.......
.......
.......
),
Stack(
children: [
Container(
height: 60,
width: double.infinity,
// color: Colors.red,
padding: const EdgeInsets.only(top: 8, left: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(
Icons.search,
size: 30,
color: Colors.grey,
),
Expanded(
child: Center(
child: Container(
margin:
const EdgeInsets.only(bottom: 6, right: 4),
padding: const EdgeInsets.only(left: 6),
height: 45,
width: width * 0.7,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(14)),
border:
Border.all(color: Colors.grey, width: 2)),
child: Row(
children: [
Flexible(
flex: 9,
fit: FlexFit.tight,
child: Center(
child: TextField(
controller: _controller,
onChanged: (value) async {
setState(() {
query = value;
});
await popular.searchData(value);
},
autofocus: true,
cursorColor: Colors.grey,
style: const TextStyle(
color: Colors.grey, fontSize: 18),
decoration: const InputDecoration(
border: InputBorder.none,
hintText:
'Search By Restaurant or Food',
hintStyle:
TextStyle(color: Colors.grey),
),
),
)),
Flexible(
flex: 1,
fit: FlexFit.tight,
child: InkWell(
onTap: () => Navigator.of(context).pop(),
child: const Icon(Icons.close,
color: Colors.grey),
),
)
],
),
),
),
),
],
),
),
],
)
],
)),
body: Column(
children: [
Expanded(
child: Container(
width: double.infinity,
color: Colors.red,
child: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(searchProvider [index]['product_name'])),
itemCount: searchProvider.length,
),
)
)
],
),
);
}
}
Can someone please help out?

How do I ensure that the url of my image in flutter doesn't show up on the screen instead of the image?

I am new to flutter. I was trying to get the pictures of some plants to show up beside the name of the specific plant however I was getting the url instead, is there any possible way that i can fix this issue in my code down below?
import 'package:flutter/material.dart';
class ProfilePage extends StatefulWidget {
const ProfilePage({Key? key}) : super(key: key);
#override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
// This holds a list of fiction users
// You can use data fetched from a database or a server as well
final List<Map<String, dynamic>> _allHerbs = [
{
"id": 1,
"name": "plant1",
"urlImage":
'https://www.southernexposure.com/media/products/originals/sweet-genovese-basil-809aaf7e3d9a3f3fa7ce2f0eb4480e95.jpg'
},
{"id": 2, "name": "plant2", "urlImage": ''},
{"id": 3, "name": "plant3", "urlImage": ''},
{"id": 4, "name": "plant4", "urlImage": ''},
{"id": 5, "name": "plant5", "urlImage": ''},
{"id": 6, "name": "plant6", "urlImage": ''},
{"id": 7, "name": "plant7", "urlImage": ''},
{"id": 8, "name": "plant8", "urlImage": ''},
{"id": 9, "name": "plant9", "urlImage": ''},
{"id": 10, "name": "plant10", "urlImage": ''},
];
// This list holds the data for the list view
List<Map<String, dynamic>> _foundHerbs = [];
#override
initState() {
// at the beginning, all users are shown
_foundHerbs = _allHerbs;
super.initState();
}
// This function is called whenever the text field changes
void _runFilter(String enteredKeyword) {
List<Map<String, dynamic>> results = [];
if (enteredKeyword.isEmpty) {
// if the search field is empty or only contains white-space, we'll display all users
results = _allHerbs;
} else {
results = _allHerbs
.where((user) =>
user["name"].toLowerCase().contains(enteredKeyword.toLowerCase()))
.toList();
// we use the toLowerCase() method to make it case-insensitive
}
// Refresh the UI
setState(() {
_foundHerbs = results;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Herb Search'),
),
body: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
const SizedBox(
height: 20,
),
TextField(
onChanged: (value) => _runFilter(value),
decoration: const InputDecoration(
labelText: 'Search', suffixIcon: Icon(Icons.search)),
),
const SizedBox(
height: 20,
),
Expanded(
child: _foundHerbs.isNotEmpty
? ListView.builder(
itemCount: _foundHerbs.length,
itemBuilder: (context, index) => Card(
key: ValueKey(_foundHerbs[index]["id"]),
color: Colors.blueAccent,
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 10),
child: ListTile(
leading: Text(
_foundHerbs[index]["id"].toString(),
style: const TextStyle(fontSize: 24),
),
title: Text(_foundHerbs[index]['name']),
subtitle: Text('${_foundHerbs[index]["urlImage"]} '),
),
),
)
: const Text(
'No results found',
style: TextStyle(fontSize: 24),
),
),
],
),
),
);
}
}
You are expecting to get image from text widget you just need to
change this
subtitle: Text('${_foundHerbs[index]["urlImage"]} '),
to this
subtitle: Image.network('${_foundHerbs[index]["urlImage"]} '),
the above will show your image under the title but if you want to show it beside use the leading or trailing instead of subtitle
like this
leading: Image.network('${_foundHerbs[index]["urlImage"]} '),