How to add footer to ReorderableListView in flutter - flutter

Trying to make a ui that contains Header and Footer with rearrangeable content items. There is a property called header from which we can add header item. But what to do if I want to add footer item as well.
import 'package:flutter/material.dart';
class MyStickyHeader extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyStickyHeaderState();
}
}
class _MyStickyHeaderState extends State<MyStickyHeader> {
List<Widget> _list = [
Text("Apple"),
Text("Ball"),
Text("Cat"),
Text("Dog"),
Text("Elephant")
];
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 30, left: 10),
color: Colors.white,
child: showData(),
);
}
Widget showData() {
return Container(
child: ReorderableListView(
header: Container(
height: 100,
color: Colors.red,
),
children: _list
.map((item) => Container(
padding: EdgeInsets.all(10),
key: Key("${(item as Text).data}"),
child: Row(
children: <Widget>[
Icon(Icons.ac_unit),
Expanded(
child: item,
)
],
),
))
.toList(),
onReorder: (int start, int current) {
// dragging from top to bottom
if (start < current) {
int end = current - 1;
Widget startItem = _list[start];
int i = 0;
int local = start;
do {
_list[local] = _list[++local];
i++;
} while (i < end - start);
_list[end] = startItem;
}
// dragging from bottom to top
if (start > current) {
Widget startItem = _list[start];
for (int i = start; i > current; i--) {
_list[i] = _list[i - 1];
}
_list[current] = startItem;
}
setState(() {});
},
),
);
}
}

Last element of your list can be a footer. It has to be a widget with onLongPress property. For example:
ReorderableListView(
onReorder: (int oldIndex, int newIndex) {},
children: List.generate(someList.items.length + 1, (index) {
if (index < someList.items.length)
return ListTile(
key: Key(someList.items[index].description),
);
else
return RaisedButton(key: Key('footer'), onPressed: () {}, onLongPress: (){}, child: Text('Button'));
})),

If you wrap your ReorderableListView with a Column and an Expanded widget, you can add a Container at the bottom to act as a footer:
Column(
children: <Widget>[
Expanded(
child: ReorderableListView(
header: Container(
height: 100,
color: Colors.red,
),
children: _list
.map((item) => Container(
padding: EdgeInsets.all(10),
key: Key("${(item as Text).data}"),
child: Row(
children: <Widget>[
Icon(Icons.ac_unit),
Expanded(
child: item,
)
],
),
)).toList(),
onReorder: (int start, int current) {
// dragging from top to bottom
if (start < current) {
int end = current - 1;
Widget startItem = _list[start];
int i = 0;
int local = start;
do {
_list[local] = _list[++local];
i++;
} while (i < end - start);
_list[end] = startItem;
}
// dragging from bottom to top
if (start > current) {
Widget startItem = _list[start];
for (int i = start; i > current; i--) {
_list[i] = _list[i - 1];
}
_list[current] = startItem;
}
setState(() {});
},
),
),
Container(
height: 40,
alignment: Alignment.center,
child: Text('Footer'),
color: Colors.orange,
),
],
),

To implement such view i recommend using Slivers.
benefits:
Sticky header/Footer.
infinity body/content scroll.
check the code below:
import 'package:flutter/material.dart';
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
Container(
width: double.infinity,
height: 50,
color: Colors.orangeAccent,
child: Center(
child: Text(
'Header',
style: TextStyle(color: Colors.white, letterSpacing:4),
),
),
),
ListView.builder(
shrinkWrap: true,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Center(child: Text('$index')),
);
},
),
],
),
),
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
height: 50,
color: Colors.blueAccent,
child: Center(
child: Text(
'Footer',
style: TextStyle(color: Colors.white, letterSpacing: 4),
),
),
),
),
)
],
),
);
}
}
For more detail take a look here:
https://itnext.io/create-a-header-footer-with-scrollable-body-view-in-flutter-5551087270de

Related

How to insert a custom tile in list view at specific interval

I am trying to insert a custom tile after every 4 entries in ListView.builder. The problem is that when the Listview.builder is scrolled up/down, the page number changes. Please see the clip. (Notice my custom tile in grey stating page numbers)
https://youtube.com/shorts/BTm7BEya62A?feature=share
My Code is as follows:
int pageCounter = 1;
int oldPageCounter = 0;
final int pageLength = 735;
int pageLengthLeft = 735;
Listview.builder...
itemBuilder: (BuildContext context, int index) {
adjustPageCounter(widget.mapOfProblems.values.elementAt(index), index);
...
child: (oldPageCounter != pageCounter)
? Column(
children: [
getPageDivider(),
MyUsualListTile()
])
: MyUsualListTile()
)}
getPageDivider() {
oldPageCounter = pageCounter;
return Container(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
width: double.infinity,
color: Colors.grey[300],
child: Align(alignment: Alignment.topRight,child: Text('Page $pageCounter'),),
);
}
void adjustPageCounter(element, int index) {
if (element is Note || element is Snag){
if (pageLengthLeft<165) resetPageLengthIncCounter();
pageLengthLeft -= 165;
}
if (element is Photos) {
if (pageLengthLeft < 250) resetPageLengthIncCounter();
pageLengthLeft -= 250;
}
}
void resetPageLengthIncCounter() { pageLengthLeft = pageLength; pageCounter++;}
The best way to do this is to use ListView.separated() like so:
ListView.separated(
cacheExtent: 20,
itemBuilder: (context, index) => ListTile(
title: Text('Item $index'),
),
separatorBuilder: (context, index) => (index + 1) % 4 == 0
? Container(
height: 10,
color: Colors.pink,
)
: const Divider(),
itemCount: 100)
See screenshot
You can try like any of bellow approach.
Using simple listView
class MyPageListView extends StatelessWidget {
const MyPageListView({super.key});
final int pageLength = 735;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: (pageLength),
itemBuilder: (context, index) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: ((index) % 4 == 0)
? Container(
height: 40,
color: Colors.yellow,
child: index == 0
? Center(child: Text('Page: ${(index + 1)}'))
: Center(
child:
Text('Page: ${((index) ~/ 4) + 1}')),
)
: Container(),
),
],
),
index == 0
? Container()
: Row(
children: [
Expanded(
child: Container(
height: 40,
color: Colors.blue,
child: Text('item index: ${index + 1}'),
),
),
],
)
],
);
},
),
);
}
}
Using separated ListView
class MyPageListView extends StatelessWidget {
const MyPageListView({super.key});
final int pageLength = 735;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.separated(
itemCount: pageLength,
itemBuilder: (context, index) => Column(
children: [
if (index == 0)
Container(
height: 40,
width: MediaQuery.of(context).size.width,
color: Colors.yellow,
child: Center(
child: Text('Page: ${(index + 1)}'),
),
),
SizedBox(
height: 40,
child: Text('Item ${index + 1}'),
),
],
),
separatorBuilder: (context, index) => (index + 1) % 4 == 0
? Container(
height: 40,
color: Colors.yellow,
child: Center(
child: Text('Page: ${((index) ~/ 4) + 1}'),
),
)
: Container(),
),
);
}
}

Expandable widget in sliver list expands to the top instead of to the bottom due to center key

I want to make expandable item in sliver list, but when the expandable widget is in sliver list above the sliver list with center key, expandable item expands to the top instead to the bottom.
CustomScrollView(
center: _centerKey,
slivers: [
// in this sliver list expandable expands to the top
SliverList(
delegate: SliverChildListDelegate(
buildItems(1),
)),
SliverList(
// in this sliver list expandable expands to the bottom
key: _centerKey,
delegate: SliverChildListDelegate(
buildItems(2),
)),
// in this sliver list expandable expands also to the bottom
SliverList(
delegate: SliverChildListDelegate(
buildItems(3),
)),
],
);
in buildItems are basic Containers and one Expandable Widget from
https://pub.dev/packages/expandable
List<Widget> buildItems(int listIndex) {
final items = <Widget>[];
for (int i = 0; i < 10; i++) {
if (i == 5) {
// Expandable widget
items.add(const ExpandedItem());
} else {
items.add(Container(
width: double.infinity,
height: 200,
color: (i % 2 == 0) ? Colors.blue : Colors.red,
));
}
}
return items;
}
After clicking on yellow item, item goes to the top instead of stay at the same place (orange and brown are expanded content)
class ExpandedItem extends StatefulWidget {
const ExpandedItem({Key? key}) : super(key: key);
#override
State<ExpandedItem> createState() => _ExpandedItemState();
}
class _ExpandedItemState extends State<ExpandedItem> {
final controller = ExpandableController();
List<Widget> buildItems() {
final items = <Widget>[];
for (int i = 0; i < 10; i++) {
items.add(GestureDetector(
onTap: i == 9
? () {
controller.toggle();
}
: null,
child: Container(
width: double.infinity,
height: 100,
color: (i % 2 == 0) ? Colors.orange : Colors.brown,
),
));
}
return items;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
ExpandableNotifier(
controller: controller,
child: Expandable(
collapsed: GestureDetector(
onTap: () {
controller.toggle();
},
child: Container(
color: Colors.yellow,
width: double.infinity,
height: 100,
),
),
expanded: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: Colors.yellow,
width: double.infinity,
height: 100,
),
...buildItems()
],
),
),
),
],
);
}
}
check this when the yellow is tapped, it expand downward
class _ShowCaseState extends State<ShowCase> {
final Key _centerKey = const ValueKey<String>('bottom-sliver-list');
List<Widget> buildItems(int listIndex) {
final items = <Widget>[];
for (int i = 0; i < 10; i++) {
if (i == 5) {
items.add(const ExpandedItem());
} else {
items.add(Container(
width: double.infinity,
height: 200,
color: (i % 2 == 0) ? Colors.blue : Colors.red,
));
}
}
return items;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
center: _centerKey,
slivers: [
// in this sliver list expandable expands to the top
SliverList(
key: _centerKey, //here
delegate: SliverChildListDelegate(
buildItems(1),
)),
SliverList(
// in this sliver list expandable expands to the bottom
delegate: SliverChildListDelegate(
buildItems(2),
)),
// in this sliver list expandable expands also to the bottom
SliverList(
delegate: SliverChildListDelegate(
buildItems(3),
)),
],
));
}
}
class ExpandedItem extends StatefulWidget {
const ExpandedItem({Key? key}) : super(key: key);
#override
State<ExpandedItem> createState() => _ExpandedItemState();
}
class _ExpandedItemState extends State<ExpandedItem> {
final controller = ExpandableController();
List<Widget> buildItems() {
final items = <Widget>[];
for (int i = 0; i < 10; i++) {
items.add(GestureDetector(
onTap: i == 9
? () {
controller.toggle();
}
: null,
child: Container(
width: double.infinity,
height: 100,
color: (i % 2 == 0) ? Colors.orange : Colors.brown,
),
));
}
return items;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
ExpandableNotifier(
controller: controller,
child: Expandable(
collapsed: GestureDetector(
onTap: () {
controller.toggle();
},
child: Container(
color: Colors.yellow,
width: double.infinity,
height: 100,
),
),
expanded: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: Colors.yellow,
width: double.infinity,
height: 100,
),
...buildItems()
],
),
),
),
],
);
}
}

Force navDrawer state update from outside state scope

I am currently developing an e-commerce mobile app.
Right now I am working on my navigation. There's a bunch of categories that can have subcategories and the subcategories can have their own subcategories. I retrieve a list of all categories via an API on app init and then I store it.
Here's an example of my dataset:
{
"id":"41490",
"name":"Electrical Equipment",
"subCategories":[
{
"id":"41492",
"name":"Breakers",
"subCategories":[
{
"id":"167542",
"name":"1 Pole",
"subCategories":[
{
"id":"167577",
"name":"15 Amp",
"subCategories":null
},
{
"id":"167585",
"name":"20 Amp",
"subCategories":null
},
{
"id":"167600",
"name":"30 Amp",
"subCategories":null
},
{
"id":"167606",
"name":"40 Amp",
"subCategories":null
}
]
},
I am using a listview and a listview builder to make my category list.
The listview builder also calls a recursive function to make the subcategories.
I've managed to get everything to generate dynamically meaning that if on the website we add a bunch of categories then the app will update itself automatically via the API.
My problem now is that when I click my categories, the navDrawer doesn't redraw. I have to close the categories and re-open them to make it redraw. I need some new concepts, been scratching my head on this one for a while.
I think there might be an issue with the structure of my code since I initialize the categories outside the state.
Here's my navDrawer class:
class navDrawer extends StatefulWidget {
bool _expandCategories = false;
bool _expandAccount = false;
List _categories;
var _categoryList;
List _tempSubCats;
void flickCategories(){
//_expandCategories = !_expandCategories;
//sleep(const Duration(microseconds: 100));
//_expandCategories = !_expandCategories;
}
void setCategories(List categories){
_categories = categories;
int catCount = categories.length;
_categoryList = new ListView.builder(
//shrinkWrap: true,
//physics: ClampingScrollPhysics(),
padding:EdgeInsets.all(0.0),
itemCount: catCount,
itemBuilder: (BuildContext context, int index) => buildCategories(context, index),
);
}
Widget buildCategories(BuildContext context, int index){
if(_categories[index]['subCategories']!=null){
if(idHandler.isIdOpen(_categories[index]['id'])){
_tempSubCats = [];
buildSubCategories(_categories[index],2);
ListView subCategories = new ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: _tempSubCats.length,
itemBuilder: (BuildContext ct, int i){
return _tempSubCats[i];
}
);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 30.0,
child: ListTile(
title: Row(
children: [
Text(" " + _categories[index]['name']),
Transform.rotate(
angle: -math.pi/2,
child:
Transform.scale(
scale: 0.75,
child:
Icon(Icons.arrow_back)
)
)
]
),
onTap: () {
flickCategories();
idHandler.toggleId(_categories[index]['id']);
}
),
padding: EdgeInsets.all(0.0),
margin: EdgeInsets.all(0.0)
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
removeLeft: true,
removeRight: true,
context: context,
child: subCategories
)
]
);
} else {
return Container(
height: 30.0,
child: ListTile(
title: Row(
children: [
Text(" " + _categories[index]['name']),
Transform.scale(
scale: 0.75,
child:
Icon(Icons.arrow_back)
)
]
),
onTap: () {
flickCategories();
idHandler.toggleId(_categories[index]['id']);
}
),
padding: EdgeInsets.all(0.0),
margin: EdgeInsets.all(0.0)
);
}
} else {
return Container(
height: 30.0,
child: ListTile(
title: Text(" "+_categories[index]['name']),
onTap: () {
//TODO: implement category navigation
}
),
padding: EdgeInsets.all(0.0),
margin: EdgeInsets.all(0.0)
);
}
}
void buildSubCategories(var parent, int depth){
if(parent['subCategories']!=null){
List subCategoryList = parent['subCategories'];
int subCategoryCount = subCategoryList.length;
//Column subCats = new Column();
if(idHandler.isIdOpen(parent['id'])) {
for (var i = 0; i < subCategoryCount; i++) {
String formattedCategory = indentCategory(parent['subCategories'][i]['name'], depth);
_tempSubCats.add(
parent['subCategories'][i]['subCategories']!=null ?
Container(
height:20.0,
child:
ListTile(
title: idHandler.isIdOpen(parent['subCategories'][i]['id']) ?
Row(
children:[
Text(formattedCategory),
Transform.rotate(
angle:-math.pi/2,
child:
Transform.scale(
scale:0.75,
child:
Icon(Icons.arrow_back)
)
)
]
)
:
Row(
children: [
Text(formattedCategory),
Transform.scale(
scale: 0.75,
child:
Icon(Icons.arrow_back)
)
]
),
onTap: (){
flickCategories();
idHandler.toggleId(parent['subCategories'][i]['id']);
}
)
)
:
Container(
height:20.0,
child:
ListTile(
title: Text(formattedCategory),
onTap: (){
//TODO: implement category navigation
}
)
)
);
buildSubCategories(parent['subCategories'][i], depth+1);
}
}
}
}
String indentCategory(String input, int amount){
String output='';
for(var i=0; i<amount; i++){
output += ' ';
}
output+=input;
return output;
}
#override
_navDrawerState createState() => _navDrawerState();
}
class _navDrawerState extends State<navDrawer>{
#override
Widget build(BuildContext Context){
return Drawer(
child:
Container(
padding:EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 5),
child:
Column(
children:[
Container(
height:80.0,
width:double.infinity,
child:
DrawerHeader(
child: Text('Menu'),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: <Color>[
Colors.grey,
Colors.red
])
)
),
margin:EdgeInsets.all(0.0),
padding:EdgeInsets.all(0.0)
),
Expanded(
child:
ListView(
padding:EdgeInsets.zero,
children: <Widget>[
widget._expandCategories?
Column(
children:[
Container(
height:40.0,
child: ListTile(
title: Row(
children: [
Text('Categories'),
Transform.rotate(
angle: -math.pi/2,
child:
Transform.scale(
scale: 0.75,
child:
Icon(Icons.arrow_back)
)
)
]
),
onTap:() {
_expandCat();
}
)
),
MediaQuery.removePadding(
removeTop:true,
context: context,
child:
SizedBox(
height:300.0,
child: widget._categoryList,
)
)
]
)
:Container(
height:40.0,
child:
ListTile(
title: Row(
children: [
Text('Categories'),
Transform.scale(
scale: 0.75,
child:
Icon(Icons.arrow_back)
)
]
),
onTap:(){
_expandCat();
//Update state of the app
}
),
margin:EdgeInsets.all(0.0),
padding:EdgeInsets.all(0.0)
),
Container(
height:40.0,
child:
ListTile(
title:Text('Your quotes'),
onTap:(){
//Update state of the app
}
),
margin:EdgeInsets.all(0.0),
padding:EdgeInsets.all(0.0)
),
widget._expandAccount?
Column(
children:[
Container(
height:40.0,
child:
ListTile(
title: Row(
children:[
Text('Your account'),
Transform.rotate(
angle:-math.pi/2,
child:
Transform.scale(
scale:0.75,
child:
Icon(Icons.arrow_back)
)
)
]
),
onTap:(){
_expandAcc();
}
),
margin:EdgeInsets.all(0.0),
padding:EdgeInsets.all(0.0)
),
Container(
height:30.0,
child:
ListTile(
title:Text(' Your Information'),
onTap:(){
}
)
),
Container(
height:30.0,
child:
ListTile(
title:Text(' Your Address'),
onTap:(){
}
)
),
Container(
height:30.0,
child:
ListTile(
title:Text(' Listed Equipment'),
onTap:(){
}
)
),
Container(
height:30.0,
child:
ListTile(
title:Text(' Add Equipment'),
onTap:(){
}
)
),
]
)
:Container(
height:40.0,
child:
ListTile(
title: Row(
children:[
Text('Your account'),
Transform.scale(
scale:0.75,
child:
Icon(Icons.arrow_back)
)
]
),
onTap:(){
_expandAcc();
}
),
margin:EdgeInsets.all(0.0),
padding:EdgeInsets.all(0.0)
)
]
),
)
]
)
)
);
}
void _expandCat(){
setState((){
widget._expandCategories=!widget._expandCategories;
});
}
void _expandAcc(){
setState((){
widget._expandAccount=!widget._expandAccount;
});
}
}
NOTE: idHandler is a public member of main.dart.
NOTE2: flickCategories() is one of my attempts at updating the state.
In the screenshot below you can see what I mean:
If I click Electrical Equipment then I have to click Categories twice to make it redraw and I have to scroll back to where it was in the list.
So, how do I make the state update when one of my categories gets clicked?
Do I need something like a stateful category widget?
I'm trying to make it look responsive with arrows and indents and etc.
I figured this out on my own.
I needed to make a productCategory stateful widget and update its state from within the widget.
Each productCategory widget has a List representing the subCategories. During my recursion I add to the subCategories for each productCategory.
The productCategory widget redraws itself properly because I call setState() which has the added bonus of keeping the scroll position where it is.
Here's my productCategory widget:
class productCategory extends StatefulWidget{
String _id = '';
String _name = '';
List<productCategory> _subCategories = [];
productCategory(String id, String name){
_id = id;
_name = name;
}
void addAllSubCategories(List<productCategory> subCats){
_subCategories.addAll(subCats);
}
void addSubCategory(productCategory cat){
_subCategories.add(cat);
}
void setName(String name){
_name = name;
}
void setId(String id){
_id = id;
}
#override
_productCategoryState createState() => _productCategoryState();
}
class _productCategoryState extends State<productCategory>{
#override
Widget build(BuildContext context) {
if(widget._subCategories.isNotEmpty){
if(idHandler.isIdOpen(widget._id)){
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 30.0,
child: ListTile(
title: Row(
children: [
Text(widget._name),
Transform.rotate(
angle: -math.pi/2,
child:
Transform.scale(
scale: 0.75,
child:
Icon(Icons.arrow_back)
)
)
]
),
onTap: () {
setState((){
idHandler.toggleId(widget._id);
});
}
),
padding: EdgeInsets.all(0.0),
margin: EdgeInsets.all(0.0)
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
removeLeft: true,
removeRight: true,
context: context,
child: ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.all(0.0),
itemCount: widget._subCategories.length,
itemBuilder: (BuildContext context, int index){
return widget._subCategories[index];
}
)
)
]
);
} else {
return Container(
height: 30.0,
child: ListTile(
title: Row(
children: [
Text(widget._name),
Transform.scale(
scale: 0.75,
child:
Icon(Icons.arrow_back)
)
]
),
onTap: () {
setState((){
idHandler.toggleId(widget._id);
});
}
),
padding: EdgeInsets.all(0.0),
margin: EdgeInsets.all(0.0)
);
}
} else {
return Container(
height: 30.0,
child: ListTile(
title: Text(widget._name),
onTap: () {
//TODO: implement category navigation
}
),
padding: EdgeInsets.all(0.0),
margin: EdgeInsets.all(0.0)
);
}
}
}

Flutter:GestureDetector does not work in ListWheelScrollView

I don't find the error why the on tab gesture is not called when you press one of the tiles. Can someone help me? What am I doing wrong?
Widget build(BuildContext context) {
final List<Widget> questionThemes = <Widget>[];
for (int i = 0; i < numberQuestionBundels; i++) {
questionThemes.add(GestureDetector(
onTap: () {
setState(() {
print('Does not work');
//... Navigation Method
});
},
child: Container(
alignment: Alignment.centerLeft,
margin: const EdgeInsets.all(2.0),
child: ListView(itemExtent: 20.0, children: <Widget>[
Text('Thema: ' + lectionBundle.taskBundle[i].nameOfTask,
style: textStyles.lightGrey20Creepy,
textAlign: TextAlign.center),
//... more Texts
]),
),
));
}
return ListWheelScrollView(
itemExtent: 110,
diameterRatio: 5,
children: questionThemes,
);}

Flutter : How to add a Header Row to a ListView

Very new to Flutter. I've been able to utilize HTTP requests for data, build a ListView, edit a Row in that List and other basics. Excellent environment.
I've managed to cobble together a badly constructed Header for a ListView but I know this isn't right. I can't get the Header text to line up properly.
I see that the Drawer Class has a DrawerHeader Class, but can't see that ListView has a ListViewHeader.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Contacts'),
actions: <Widget>[
IconButton(icon: Icon(Icons.add_circle),
onPressed: getCustData
),
],
),
//body:
body: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(child: Text('', style: TextStyle(height: 3.0, fontSize: 15.2, fontWeight: FontWeight.bold,))),
Expanded(child: Text('First Name', style: TextStyle(height: 3.0, fontSize: 15.2, fontWeight: FontWeight.bold,))),
Expanded(child: Text('Last Name', style: TextStyle(height: 3.0, fontSize: 15.2, fontWeight: FontWeight.bold,))),
Expanded(child: Text('City', style: TextStyle(height: 3.0, fontSize: 15.2, fontWeight: FontWeight.bold,))),
Expanded(child: Text('Customer Id', style: TextStyle(height: 3.0, fontSize: 15.2, fontWeight: FontWeight.bold,))),
Expanded(child: Text('', style: TextStyle(height: 3.0, fontSize: 15.2, fontWeight: FontWeight.bold,))),
]
),
Expanded(child:Container(
child: ListView.builder(
itemCount: data == null ? 0 : data.length,
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => APIDetailView(data[index])),
);
},
child: ListTile( //return new ListTile(
onTap: null,
leading: CircleAvatar(
backgroundColor: Colors.blue,
child: Text(data[index]["FirstName"][0]),
),
title: Row(
children: <Widget>[
Expanded(child: Text(data[index]["FirstName"])),
Expanded(child: Text(data[index]["LastName"])),
Expanded(child: Text(data[index]["Bill_City"])),
Expanded(child: Text(data[index]["Customer_Id"])),
]
)
),
);
}, //itemBuilder
),
),
),
]
)
);
}
}
Thanks.
Return the header as first row by itemBuilder:
ListView.builder(
itemCount: data == null ? 1 : data.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
// return the header
return new Column(...);
}
index -= 1;
// return row
var row = data[index];
return new InkWell(... with row ...);
},
);
Here's how I solved this. Thanks #najeira for getting me thinking about other solutions.
In the first body Column I used the same layout for my Header that I used for the ListTile.
Because my data ListTile, in this case, includes a CircleAvatar, all the horizontal spacing is off a bit... 5 columns where the CircleAvatar is rendered... then 4 evenly spaced columns.
So... I added a ListTile to the first body Column, a CircleAvatar with a backgroundColor of transparent, and then a Row of my 4 Headings.
ListTile(
onTap: null,
leading: CircleAvatar(
backgroundColor: Colors.transparent,
),
title: Row(
children: <Widget>[
Expanded(child: Text("First Name")),
Expanded(child: Text("Last Name")),
Expanded(child: Text("City")),
Expanded(child: Text("Id")),
]
),
),
You can add Container and ListView in Column.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text("Demo App1"),
),
body: Column(
children: <Widget>[
Container(
height: 40.0,
child: Row(
children: <Widget>[
Container(
padding: EdgeInsets.all(4.0),
width: 100.0,
child: Text(
"Name",
style: TextStyle(fontSize: 18),
)),
Container(
padding: EdgeInsets.all(4.0),
width: 100.0,
child: Text(
"Age",
style: TextStyle(fontSize: 18),
)),
],
),
),
Expanded(
child: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Row(
children: <Widget>[
Container(
padding: EdgeInsets.all(4.0),
width: 100.0,
child: Text(
"Name $index",
style: TextStyle(fontSize: 18),
)),
Container(
padding: EdgeInsets.all(4.0),
width: 100.0,
child: Text(
"Age $index",
style: TextStyle(fontSize: 18),
),
)
],
);
},
),
),
],
),
),
);
}
}
You can add a column to the first item in the item list like this
new ListView.builder(
itemCount: litems.length,
itemBuilder: (BuildContext ctxt, int index) {
if (index == 0) {
return Column(
children: <Widget>[
Header(),
rowContent(index),
],
);
} else {
return rowContent(index);
}
},
)
najeira's solution is easy and simple, but you can get the same and more flexible result without touching index.
Instead of using listView, you could use CustomScrollView & SliverList which is functionally the same as listView.
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
// you could add any widget
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.transparent,
),
title: Row(
children: <Widget>[
Expanded(child: Text("First Name")),
Expanded(child: Text("Last Name")),
Expanded(child: Text("City")),
Expanded(child: Text("Id")),
],
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => APIDetailView(data[index])),
);
},
child: ListTile(
//return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue,
child: Text(data[index]["FirstName"][0]),
),
title: Row(
children: <Widget>[
Expanded(child: Text(data[index]["FirstName"])),
Expanded(child: Text(data[index]["LastName"])),
Expanded(child: Text(data[index]["Bill_City"])),
Expanded(child: Text(data[index]["Customer_Id"])),
],
),
),
);
},
childCount: data == null ? 0 : data.length,
),
),
],
),
);
Use DataTable widget !
That widget allows you to build a table. Code : DataTable(columns: [], rows: [],)
Example :
DataTable(
columns: [
DataColumn(label: Text('Lang.')),
DataColumn(label: Text('Year')),
],
rows: [
DataRow(cells: [DataCell(Text('Dart')), DataCell(Text('2010'))]),
DataRow(cells: [DataCell(Text('Go')), DataCell(Text('2009'))]),
DataRow(cells: [DataCell(Text('PHP')), DataCell(Text('1994'))]),
DataRow(cells: [DataCell(Text('Java')), DataCell(Text('1995'))]),
],
)
Output:
You can learn more about DataTable by watching this official video or by visiting flutter.dev
It seems what you are really looking for is the DataTable widget instead of a ListView.
It has a customizable Header including sorting options.
Read the documentation including some great examples on api.flutter.dev: Data Table CLass
I have created listview_utils package to reduce boilerplate code needed to build header and footer list items. Here's an example code using the package:
import 'package:listview_utils/listview_utils.dart';
CustomListView(
header: Container(
child: Text('Header'),
),
itemCount: items.length,
itemBuilder: (BuildContext context, int index, _) {
return ListTile(
title: Text(item['title']),
);
},
);
Disclaimer: I am maintainer of the package.
Looking for dynamic section headers according to your api data. Add this class to your project.
class _FlutterSectionListViewState extends State<FlutterSectionListView> {
/// List of total number of rows and section in each group
var itemList = [];
int itemCount = 0;
int sectionCount = 0;
#override
void initState() {
/// ----#4
sectionCount = widget.numberOfSection();
/// ----#5
itemCount = listItemCount();
super.initState();
}
/// ----#6
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: itemCount,
itemBuilder: (context, index) {
return buildItemWidget(index);
},
key: widget.key,
);
}
/// Get the total count of items in list(including both row and sections)
int listItemCount() {
itemList = [];
int rowCount = 0;
for (int i = 0; i < sectionCount; i++) {
/// Get the number of rows in each section using callback
int rows = widget.numberOfRowsInSection(i);
/// Here 1 is added for each section in one group
rowCount += rows + 1;
itemList.insert(i, rowCount);
}
return rowCount;
}
/// ----#7
/// Get the widget for each item in list
Widget buildItemWidget(int index) {
/// ----#8
IndexPath indexPath = sectionModel(index);
/// ----#9
/// If the row number is -1 of any indexPath it will represent a section else row
if (indexPath.row < 0) {
/// ----#10
return widget.sectionWidget != null
? widget.sectionWidget!(indexPath.section)
: SizedBox(
height: 0,
);
} else {
return widget.rowWidget!(indexPath.section, indexPath.row);
}
}
/// Calculate/Map the indexPath for an item Index
IndexPath sectionModel(int index) {
int? row = 0;
int section = 0;
for (int i = 0; i < sectionCount; i++) {
int item = itemList[i];
if (index < item) {
row = (index - (i > 0 ? itemList[i - 1] : 0) - 1) as int?;
section = i;
break;
}
}
return IndexPath(section: section, row: row!);
}
}
/// Helper class for indexPath of each item in list
class IndexPath {
IndexPath({required this.section, required this.row});
int section = 0;
int row = 0;
}
create your list according to your api data
List<List<Operator>> ops = [];
List<String> sections = [];
if(c.operatorStatuses.value!.availableOperators.length>0){
ops.add(c.operatorStatuses.value!.availableOperators);
sections.add("Müsait Operatörler");
}
if(c.operatorStatuses.value!.busyOperators.length>0){
ops.add(c.operatorStatuses.value!.busyOperators);
sections.add("Meşgul Operatörler");
}
if(c.operatorStatuses.value!.breakOperators.length>0){
ops.add(c.operatorStatuses.value!.breakOperators);
sections.add("Moladaki Operatörler");
}
if(c.operatorStatuses.value!.closedOperators.length>0){
ops.add(c.operatorStatuses.value!.closedOperators);
sections.add("Kapalı Operatörler");
}
show it in ui;
FlutterSectionListView(
numberOfSection: () => ops.length,
numberOfRowsInSection: (section) {
return ops[section].length;
},
sectionWidget: (section) {
if(section<ops.length){
return Container(
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(sections[section]),
),
color: Colors.grey,
);
}else{
return SizedBox();
}
},
rowWidget: (section, row) {
if(row < ops[section].length){
Operator? op = ops[section][row];
return card(op);
}else{
return SizedBox();
}
},
)
thanks to [this article][1].
NOTE : code block produces error some time according to updated data..
[1]: https://medium.com/#dharmendra_yadav/ios-like-sectioned-listview-widget-in-flutter-7cf9dab2dd1a
I use this:
body: Column(
children: [
Container(
// The header will be here
),
Expanded(
// The ListView
child: ListView.builder(
itemCount: // The length,
itemBuilder: (_, index) {
return //List Item Widget Here
}),
),
],
)
Here I've created flat_list widget which has similar specifications as in React Native's FlatList.
FlatList(
+ listHeaderWidget: const Header(),
data: items.value,
buildItem: (item, index) {
var person = items.value[index];
return ListItemView(person: person);
},
),