how to make form with dynamic list in flutter - android

I am trying to build a form in flutter with the dynamic list of chips where user can select the multiple category but i am unable to build it so as i am new to flutter i am not able to get it done that how can i get the static form fields and get the dynamic list of the chips in it.
I am trying to get it using the grid view but the grid view is repeating the the whole form with every chip and if i use the grid view with only chip i am not able to get the rest of the static field of the form.
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Student form"),
),
body:ExamGrid(),
);
}
Below is the ExamGrid:
class _ExamGridState extends State<ExamGrid> {
#override
Widget build(BuildContext context) {
final loadedExams = Provider.of<StudentFormProvider>(context);
final loadedExam = loadedExams.exams;
return GridView.builder(
padding: const EdgeInsets.all(1.0),
itemCount: loadedExam.length,
itemBuilder: (ctx, i) {
String catname;
final exam = loadedExam[i];
if (i > 0) {
catname = exam.catname;
}
if (exam.catname != catname) {
//new SizedBox(height: 8);
catname = exam.catname;
return Padding(
padding: EdgeInsets.fromLTRB(16, 0, 8, 8),
child: Align(alignment: Alignment.centerLeft,
child: Text(exam.catname,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
),
);
} else {
return FilterChip(
label: Text(exam.examname),
backgroundColor: Colors.transparent,
shape: StadiumBorder(side: BorderSide()),
selected: exam.isselected,
onSelected: (bool checked) {
setState(() {
exam.isselected = checked;
});
},
);
}
},
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
//crossAxisCount: 1,
maxCrossAxisExtent: 200,
childAspectRatio: 4 / 1,
mainAxisSpacing: 10,
),
);
}
}
Can anyone please help me what to do in such scenario as anything i am trying results to unexpected result. I want to have multiple static form list with the dynamic list of chips

Have two widgets list in the class. One empty & one with all of the categories.
Show the user all of the categories and when the user tap on one category add it to the empty list and call setstate.

I have used this you need in my project there is a complete sample, you can select one chip or multiple
class CourseFilterScreen extends StatefulWidget {
static const route = "/CourseFilterScreen";
_CourseFilterScreenState createState() => _CourseFilterScreenState();
}
class _CourseFilterScreenState extends State<CourseFilterScreen> {
List title = [
{'title': "React jsx", "value": "React jsx"},
{'title': "English For Kids", "value": "English for kids"},
{'title': "IELTS", "value": "IELTS"},
{'title': "Egnlish", "value": "Egnlish"},
{'title': "Flutter", "value": "Flutter"},
];
List selectedReportList = List();
String selectedReport = '';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
child: MultiSelectChip(
reportList: title,
onSelectionChanged: (selectedList) {
setState(() {
selectedReportList = selectedList;
});
},
),
),
Text("${selectedReportList.join(",")}"),
SingleSelectChip(
reportList: title,
onSelectionChanged: (selectItem) {
setState(() {
selectedReport = selectItem;
});
},
),
Text(selectedReport),
],
),
),
);
}
}
class MultiSelectChip extends StatefulWidget {
final List reportList;
final Function onSelectionChanged;
MultiSelectChip({this.reportList, this.onSelectionChanged});
#override
_MultiSelectChipState createState() => _MultiSelectChipState();
}
class _MultiSelectChipState extends State<MultiSelectChip> {
List selectedChoices = List();
#override
Widget build(BuildContext context) {
return Wrap(
children: widget.reportList
.map((item) => (Container(
padding: const EdgeInsets.all(2.0),
child: ChoiceChip(
selectedColor: Colors.lightBlueAccent,
label: Text(item['title']),
selected: selectedChoices.contains(item['value']),
onSelected: (selected) {
setState(() {
selectedChoices.contains(item['value'])
? selectedChoices.remove(item['value'])
: selectedChoices.add(item['value']);
widget.onSelectionChanged(selectedChoices);
});
},
),
)))
.toList());
}
}
class SingleSelectChip extends StatefulWidget {
final List reportList;
final Function onSelectionChanged;
SingleSelectChip({this.reportList, this.onSelectionChanged});
#override
_SingleSelectChipState createState() => _SingleSelectChipState();
}
class _SingleSelectChipState extends State<SingleSelectChip> {
String selectedChoices = '';
#override
Widget build(BuildContext context) {
return Wrap(
children: widget.reportList
.map((item) => (Container(
padding: const EdgeInsets.all(2.0),
child: ChoiceChip(
selectedColor: Colors.lightBlueAccent,
label: Text(item['title']),
selected: selectedChoices.contains(item['value']),
onSelected: (selected) {
setState(() {
selectedChoices = item['value'];
});
},
),
)))
.toList());
}
}

Related

i can't update data, re-add products every time, i want to update

class DetailedCountingScreen extends StatefulWidget {
final String countDate;
final String standName;
const DetailedCountingScreen(
{required this.countDate, required this.standName});
#override
_DetailedCountingScreenState createState() => _DetailedCountingScreenState();
}
class _DetailedCountingScreenState extends State<DetailedCountingScreen> {
final firebaseRef = FirebaseDatabase.instance.reference();
List<String> productNames = [];
List<int> soldPieces = [];
List<int> countedPieces = [];
List<double> productPrices = [];
#override
void initState() {
firebaseRef
.child('Sayımlar')
.child(widget.standName)
.child('${widget.countDate}')
.onValue
.listen((event) {
final snapshot = event.snapshot.value ?? {};
final countedProductsMap = new Map<String, dynamic>.from(snapshot);
countedProductsMap.forEach((key, value) {
setState(() {
productNames.add(value['ürünİsmi']);
soldPieces.add(value['satılanMiktar']);
countedPieces.add(value['sayılanMiktar']);
productPrices.add(value['ürünFiyatı']);
});
});
});
super.initState();
}
Widget countsCard(
{required int soldPiece,
required double productPrice,
required int countedPiece,
required String productName,
required int thePieceBeforeCount,
required double totalPrice}) {
return Card(
color: Colors.white,
child: ListTile(
isThreeLine: true,
title: Text(
productName.toUpperCase(),
style: kBlackTS.copyWith(fontWeight: FontWeight.bold),
),
subtitle: Column(
children: [
Row(
children: [
Text('Sayılan Adet: $countedPiece', style: kBlackTS),
emptySpaceWidth(context, 0.04),
Text('Satılan Adet: $soldPiece', style: kBlackTS),
emptySpaceWidth(context, 0.04),
Text('Sayım Öncesi Adet: $thePieceBeforeCount',
style: kBlackTS),
],
),
emptySpaceHeight(context, 0.015),
Row(
children: [
Text('Satış Fiyatı: ${productPrice.toStringAsFixed(2)} ₺',
style: kBlackTS),
emptySpaceWidth(context, 0.04),
Text('Toplam Tutar: ${totalPrice.toStringAsFixed(2)} ₺',
style: kBlackTS),
],
),
],
),
),
);
}
FloatingActionButton get _invoiceFAB {
return FloatingActionButton(
backgroundColor: Colors.blue.shade700,
child: Icon(FontAwesomeIcons.fileInvoice, color: Colors.white),
onPressed: () {});
}
#override
Widget build(BuildContext context) {
CustomAppBar customAppBar = CustomAppBar(
context: context,
colour: Colors.blue.shade700,
title: '${widget.countDate} Tarihli Sayımlar');
return SafeArea(
child: Scaffold(
backgroundColor: Colors.blue.shade100,
appBar: customAppBar.customAppBar,
body: Container(
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
itemCount: productNames != [] ? productNames.length : 0,
itemBuilder: (context, index) {
double total = soldPieces[index] * productPrices[index];
int thePieceBeforeCount =
soldPieces[index] + countedPieces[index];
return countsCard(
soldPiece: soldPieces[index],
productPrice: productPrices[index],
countedPiece: countedPieces[index],
productName: productNames[index],
thePieceBeforeCount: thePieceBeforeCount,
totalPrice: total);
},
),
],
),
),
floatingActionButton: _invoiceFAB,
),
);
I don't want to update the data, instead it re-adds it, can you help me how to fix it? what should i change, how should the code be? I want to update the products I added, it is necessary for my homework and I don't know what to do. I need help on what code to write for the update thank you in advance
Avoid retrieving firebase data inside initState method
Use SteamBuilder or FutureBuilder based on your requirement
I prefer SteamBuilder for your case
Refer the doc for more reference https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
Refer the example to fetch from firebase and maintain https://stackoverflow.com/a/65960948/14950155

How to Create a List of Categories using Containers

so i wanted to create a list of categories (Day-Week-Month) and i achieved it just fine using a
listView.builder but it isn't centered on the horizontal axis so i had the idea to do it with containers.
https://im3.ezgif.com/tmp/ezgif-3-aba2cbc290ae.gif
so using only containers as the gif shows when i press it stays active even if i press on another one
which is something i dont want obviously.
https://im3.ezgif.com/tmp/ezgif-3-e1e304256aaf.gif
my code :
class DWM extends StatefulWidget {
#override
_DWMState createState() => _DWMState();
}
class _DWMState extends State<DWM> {
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextDWM(
text: "Day",
),
TextDWM(
text: "Week",
),
TextDWM(
text: "Month",
),
],
),
);
}
}
class TextDWM extends StatefulWidget {
final String text;
bool isActive;
TextDWM({this.text,this.isActive = false});
#override
_TextDWMState createState() => _TextDWMState();
}
class _TextDWMState extends State<TextDWM> {
#override
Widget build(BuildContext context) {
return Column(
children: [
GestureDetector(
onTap: (){
setState(() {
widget.isActive = true;
});
},
child: Text(
widget.text,
style: TextStyle(
color: widget.isActive ? Colors.white : Colors.grey,
),
),
),
SizedBox(height: 5,),
Container(
height: 2,
width: 40,
color: widget.isActive ? Colors.deepOrange : Colors.transparent,
)
],
);
}
}
Please check out the code below to learn one of the ways this could be done. However it is advisable to use something like Inherited widget or Provider to pass data down the widget tree.
import 'package:flutter/material.dart';
final Color darkBlue = const Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
home: MyApp()));
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Flutter Demo"),
),
body: DWM(),
);
}
}
class DWM extends StatefulWidget {
#override
_DWMState createState() => _DWMState();
}
class _DWMState extends State<DWM> {
final List<String> _items = ["Day", "Week", "Month"];
List<bool> _active = []; //[true,false,false];
#override
void initState() {
super.initState();
_active = List.filled(_items.length, false);
}
void setActive(int active) {
setState(() {
_active = List.filled(_items.length, false);
_active[active] = true;
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(
_items.length,
(index) => TextDWM(
text: _items[index],
isActive: _active[index],
setActive: () => setActive(index),
),
),
),
);
}
}
class TextDWM extends StatelessWidget {
final String text;
final bool isActive;
final Function setActive;
const TextDWM({this.text, this.isActive, this.setActive});
#override
Widget build(BuildContext context) {
return Column(
children: [
GestureDetector(
onTap: () {
setActive();
},
child: Text(
text,
style: TextStyle(
color: isActive ? Colors.white : Colors.grey,
),
),
),
const SizedBox(
height: 5,
),
Container(
height: 2,
width: 40,
color: isActive ? Colors.deepOrange : Colors.transparent,
)
],
);
}
}

Flutter update Reorderable ListView when checkbox in AlertDialog check or unchecked

I'm currently working on a Flutter mobile app which is supposed to work on Android and IOS.
The issue I'm having is about ListView and updating it.
I know I'm doing wrong with a lot of things but I'm learning and I would like to learn properly. So if you have any comments, tips about the code pls give them :)
Basically here is what it does :
The main player will choose who will play with him at the game via an AlertDialog which has a CheckboxList inside it and every time he selects a player, it will update a list called choosenPlayers which has all Player objects choosen in it.
Then what I want to do is to display a list of all selected players (a reorderable list to change the order of players) and update it everytime the choosenPlayers list is updated.
I managed to display these players but I have to reload the page by going in the drawer menu and clicking on page link to see added players.
I use a stateful widget for my players reorderable list and I pass to the parent the list of players (This is not the rigth way to do it I know) :
import 'package:flutter/material.dart';
import 'package:mollky/models/player.dart';
class ChoosenPlayers extends StatefulWidget {
_ChoosenPlayersState _choosenPlayersState = _ChoosenPlayersState();
List<Player> choosenPlayers = [];
ChoosenPlayers({Key key, this.choosenPlayers}) : super(key: key);
#override
_ChoosenPlayersState createState() => _choosenPlayersState;
}
class _ChoosenPlayersState extends State<ChoosenPlayers> {
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return ReorderableListView(
onReorder: onReorder,
children: getListItems(),
);
}
List<ListTile> getListItems() => widget.choosenPlayers
.asMap()
.map((i, item) => MapEntry(i, buildTenableListTile(item, i)))
.values
.toList();
ListTile buildTenableListTile(Player item, int index) {
return ListTile(
key: ValueKey(item.id),
title: Text(item.nickname + " " + item.name),
leading: Text("#${index + 1}"),
);
}
void onReorder(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
setState(() {
Player reOrderedPlayer = widget.choosenPlayers[oldIndex];
widget.choosenPlayers.removeAt(oldIndex);
widget.choosenPlayers.insert(newIndex, reOrderedPlayer);
});
}
}
Here is the code of the main page where reorderable list is displayed and AlertDialog showed.
Sorry, couldn't format with Dart, don't run the code snipped obviously xD
class NewGame extends StatefulWidget {
#override
State<StatefulWidget> createState() => NewGameState();
}
class NewGameState extends State<NewGame> {
List<Player> players = [];
NewGameState() {
this.players.add(
Player(id: 0, name: "Dupont", nickname: "julien", picture: "test"));
this
.players
.add(Player(id: 1, name: "Dpont", nickname: "julien", picture: "test"));
this
.players
.add(Player(id: 2, name: "Dunt", nickname: "juen", picture: "test"));
}
static List<Player> _choosenPlayers = [];
ChoosenPlayers choosenPlayersObject = ChoosenPlayers(
choosenPlayers: _choosenPlayers,
);
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: DrawerWidget(),
appBar: AppBar(title: Text("Nouvelle partie")),
body: Column(children: <Widget>[
Card(
child: ListTile(
leading: Icon(Icons.people),
title: Text("Choisissez les joueurs"),
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Les joueurs existants"),
content:
Stack(overflow: Overflow.visible, children: <
Widget>[
Positioned(
right: -40.0,
top: -40.0,
child: InkResponse(
onTap: () {
Navigator.of(context).pop();
},
child: CircleAvatar(
child: Icon(Icons.close),
backgroundColor: Colors.lightBlue,
),
),
),
Positioned(
child: StatefulBuilder(
builder: (BuildContext context,
StateSetter setState) {
return Container(
width: 350.0,
height: 150.0,
child: ListView.builder(
itemCount: players.length,
itemBuilder:
(context, playerIndex) {
return CheckboxListTile(
title: Text(players[playerIndex]
.nickname +
" " +
players[playerIndex].name),
value: _choosenPlayers.contains(
players[playerIndex]),
onChanged: (bool value) {
if (!_choosenPlayers.contains(
players[playerIndex])) {
_choosenPlayers.add(
players[playerIndex]);
setState(() {});
} else {
_choosenPlayers.remove(
players[playerIndex]);
setState(() {});
}
},
secondary: const Icon(
Icons.hourglass_empty),
);
}),
);
},
),
),
]));
});
})),
Container(
width: 350.0,
height: 150.0,
child: choosenPlayersObject,
),
]));
}
}
I've seen nothing on forums about updating list without triggering a callback like onRefresh which is not what I want.
It is a real nightmare xD. Sorry for french words btw I can translate if needed but they are not important, simple text.
Here are two screenshots of the list and alert dialog :
Thank you in advance :)
The state of parent widget is not updated. That's why, even though the payer is added to the list. But not shown to in parent widget.
The setState you called only update the state of StatefulBuilder not of the NewGame.
Check out the below code.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: NewGameScreen(),
);
}
}
class NewGameScreen extends StatefulWidget {
#override
_NewGameScreenState createState() => _NewGameScreenState();
}
class _NewGameScreenState extends State<NewGameScreen> {
List<Player> _availablePlayers = [];
List<Player> _selectedPlayers = [];
#override
void initState() {
super.initState();
_availablePlayers = [
Player(id: 0, name: "Ross", nickname: "Geller", picture: "test"),
Player(id: 1, name: "Rachel", nickname: "Green", picture: "test"),
Player(id: 2, name: "Chandler", nickname: "Bing", picture: "test"),
];
}
_selectPlayer() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Existing players"),
content: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
right: -40.0,
top: -40.0,
child: InkResponse(
onTap: () {
Navigator.of(context).pop();
},
child: CircleAvatar(
child: Icon(Icons.close),
backgroundColor: Colors.lightBlue,
),
),
),
StatefulBuilder(
builder: (BuildContext context, StateSetter alertState) {
return Container(
width: 350.0,
height: 150.0,
child: ListView.builder(
itemCount: _availablePlayers.length,
itemBuilder: (context, playerIndex) {
return CheckboxListTile(
title:
Text(_availablePlayers[playerIndex].nickname + " " + _availablePlayers[playerIndex].name),
value: _selectedPlayers.contains(_availablePlayers[playerIndex]),
onChanged: (bool value) {
if (_selectedPlayers.contains(_availablePlayers[playerIndex])) {
_selectedPlayers.remove(_availablePlayers[playerIndex]);
} else {
_selectedPlayers.add(_availablePlayers[playerIndex]);
}
setState(() {});//ALSO UPDATE THE PARENT STATE
alertState(() {});
},
secondary: const Icon(Icons.hourglass_empty),
);
},
),
);
},
),
],
),
);
},
);
}
_onReorder(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
print('oldIndex:$oldIndex');
print('newIndex:$newIndex');
setState(() {
Player player = _selectedPlayers[newIndex];
_selectedPlayers[newIndex] = _selectedPlayers[oldIndex];
_selectedPlayers[oldIndex] = player;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("New Game")),
body: Column(
children: <Widget>[
Card(
child: ListTile(
leading: Icon(Icons.people),
title: Text("Choose players"),
onTap: _selectPlayer,
),
),
Flexible(
child: ReorderableListView(
onReorder: _onReorder,
children: _selectedPlayers.map((player) {
return ListTile(
key: ValueKey(player.id),
title: Text(player.nickname + " " + player.name),
leading: Text("#${_selectedPlayers.indexOf(player) + 1}"),
);
}).toList(),
),
),
],
),
);
}
}
class Player {
int id;
String name;
String nickname;
String picture;
Player({this.id, this.name, this.nickname, this.picture});
}
Hope it helps :)

Flutter stateful widget with child not updating state

I'm working on a part of an app that will essentially just be keeping track of physical tokens that are like forms of currency. I'm trying to build a reusable Widget that will take in the state of that token quantity as a parameter, and increment/decrement that based on user interaction. For the sake of clarity, I've just included the decrement part of the Widget. My question: is the state of the token that is getting passed into the widget not updating because it's just a reference to that state? Or am I missing something else.
class RedeemTokensState extends State<RedeemTokens> {
int oneQuantity = 0;
int fiveQuantity = 0;
int tenQuantity = 0;
int total = 0;
Widget _counterWidget(int tokenQuantity) {
return Row(
children: <Widget>[
Expanded(
child: IconButton(
icon: Icon(Icons.remove),
onPressed: () {
setState(() {
tokenQuantity = tokenQuantity - 1;
print(tokenQuantity);
});
},
),
),
),
}
Widget _buildOneField() {
return ListTile(
title: Text('\$1 Token'),
trailing: Container(width: 200.0, child: _counterWidget(oneQuantity)),
);
}
Widget _buildFiveField() {
return ListTile(
title: Text('\$5 Token'),
trailing: Container(width: 200.0, child: _counterWidget(fiveQuantity)),
);
}
Widget _buildTenField() {
return ListTile(
title: Text('\$10 Token'),
trailing: Container(width: 200.0, child: _counterWidget(tenQuantity)),
);
}
}
// main scaffold with build method
... Card(
child: Container(
padding: EdgeInsets.all(10.0),
child: Column(
children: <Widget>[
_buildOneField(),
Divider(),
_buildFiveField(),
Divider(),
_buildTenField(),
Divider(),
_buildFreshConnectField(),
],
),
),
),
A generic solution could look like:
Parent widget
class RedeemTokens extends StatefulWidget {
#override
RedeemTokensState createState() => RedeemTokensState();
}
class RedeemTokensState extends State<RedeemTokens> {
final _quantities = new Map<TokenType, int>.fromIterable(TokenType.values,
key: (k) => k, value: (k) => 0);
Widget build(BuildContext context) {
final widgets = <Widget>[];
for (final type in _quantities.keys) {
widgets
..add(
new TokenQuantity(
tokenType: type,
quantity: _quantities[type],
onQuantityUpdated: (newValue) {
setState(() {
print('\$${type.value}: $newValue');
print(_quantities);
_quantities[type] = newValue;
});
}),
)
..add(Divider());
}
// widgets.add(_buildFreshConnectField());
return Card(
child: Container(
padding: EdgeInsets.all(10.0),
child: Column(
children: widgets,
),
),
);
}
}
Child widget added once per TokenType
class TokenQuantity extends StatelessWidget {
const TokenQuantity(
{#required this.tokenType,
#required this.quantity,
this.onQuantityUpdated})
: assert(quantity != null);
final TokenType tokenType;
final int quantity;
final TokenQuantityUpdatedFn onQuantityUpdated;
Widget _counterWidget() {
return Row(
children: <Widget>[
Text('$quantity'),
Expanded(
child: IconButton(
icon: Icon(Icons.remove),
onPressed: () {
if (onQuantityUpdated != null) {
onQuantityUpdated(quantity - 1);
}
},
),
),
],
);
}
#override
Widget build(BuildContext context) {
return ListTile(
title: Text('\$${tokenType.value} Token'),
trailing: Container(width: 200.0, child: _counterWidget()),
);
}
}
Typedef for the event callback
typedef TokenQuantityUpdatedFn = void Function(int newValue);
"Old-style" enum to be able to set custom values.
class TokenType {
static const one = const TokenType(1);
static const fife = const TokenType(5);
static const ten = const TokenType(10);
static const values = const <TokenType>[one, fife, ten];
final int value;
const TokenType(this.value);
#override
String toString() => 'TokenType $\$value';
}

Dynamic children for TabView in flutter

I'm trying to build a Tabbed View that has lists as children.
Both the Category labels and the lists content will be fetched from a database.
I am passing the labels from the caller page and successfully passing them as a List.
Now I'm trying to load my lists, and I have built a Widget (myList) that returns successfully a Future ListView.
The problems are two:
Every time i swipe left or right, the list rebuilds itself, while I would like to have it built only once
How can I use the code I made to have the tabs' children actually reflect the labels and are loaded dinamically according to how many categories i have?
Right now my code is this:
import 'package:flutter/material.dart';
import 'package:flutter_app/ui/menu_category_list.dart';
// Each TabBarView contains a _Page and for each _Page there is a list
// of _CardData objects. Each _CardData object is displayed by a _CardItem.
List<Tab> Tabs(List<String> l){
List<Tab> list;
for (String c in l) {
list.add(new Tab(text: c));
}
return list;
}
class TabsDemo extends StatelessWidget {
const TabsDemo({ Key key , this.categorie}) : super(key: key);
final List<Tab> categorie;
#override
Widget build(BuildContext ctxt) {
return new MaterialApp(
title: "Nice app",
home: new DefaultTabController(
length: 5,
child: new Scaffold(
appBar: new AppBar(
title: new Text("Title"),
bottom: new TabBar(
tabs:
categories,
//new Tab(text: "First Tab"),
//new Tab(text: "Second Tab"),
),
),
body: new TabBarView(
children: [
new MenuCategoryList(),
new MenuCategoryList(),
new MenuCategoryList(),
new MenuCategoryList(),
new MenuCategoryList()
]
)
),
)
);
}
}
currently result
Thanks a lot in advance
You can use List<E>.generate to achieve this.
import 'package:flutter/material.dart';
Say you have a set of categories passed from your caller page. And let's say this is your list of categories.
List<String> categories = ["a", "b", "c", "d", "e", "f", "g", "h"];
Then you can do something like this to achieve what you desire.
class TabsDemo extends StatefulWidget {
#override
_TabsDemoState createState() => _TabsDemoState();
}
class _TabsDemoState extends State<TabsDemo> {
TabController _controller;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext ctxt) {
return new MaterialApp(
home: DefaultTabController(
length: categories.length,
child: new Scaffold(
appBar: new AppBar(
title: new Text("Title"),
bottom: new TabBar(
isScrollable: true,
tabs: List<Widget>.generate(categories.length, (int index){
print(categories[0]);
return new Tab(icon: Icon(Icons.directions_car), text: "some random text");
}),
),
),
body: new TabBarView(
children: List<Widget>.generate(categories.length, (int index){
print(categories[0]);
return new Text("again some random text");
}),
)
))
);
}
You can also set different set of widgets as the Tab's view. You can create a list of pages and follow the same method.
Absolutely true List<E>.generate best solution to solve.
Problems arise if you need to modify the arrays. They consist in the fact that when modifying an array you do not have the opportunity to use the same controller.
You can use the next custom widget for this case:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<String> data = ['Page 0', 'Page 1', 'Page 2'];
int initPosition = 1;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomTabView(
initPosition: initPosition,
itemCount: data.length,
tabBuilder: (context, index) => Tab(text: data[index]),
pageBuilder: (context, index) => Center(child: Text(data[index])),
onPositionChange: (index){
print('current position: $index');
initPosition = index;
},
onScroll: (position) => print('$position'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
data.add('Page ${data.length}');
});
},
child: Icon(Icons.add),
),
);
}
}
/// Implementation
class CustomTabView extends StatefulWidget {
final int itemCount;
final IndexedWidgetBuilder tabBuilder;
final IndexedWidgetBuilder pageBuilder;
final Widget stub;
final ValueChanged<int> onPositionChange;
final ValueChanged<double> onScroll;
final int initPosition;
CustomTabView({
#required this.itemCount,
#required this.tabBuilder,
#required this.pageBuilder,
this.stub,
this.onPositionChange,
this.onScroll,
this.initPosition,
});
#override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView> with TickerProviderStateMixin {
TabController controller;
int _currentCount;
int _currentPosition;
#override
void initState() {
_currentPosition = widget.initPosition ?? 0;
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
_currentCount = widget.itemCount;
super.initState();
}
#override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition;
}
if (_currentPosition > widget.itemCount - 1) {
_currentPosition = widget.itemCount - 1;
_currentPosition = _currentPosition < 0 ? 0 :
_currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance.addPostFrameCallback((_){
if(mounted) {
widget.onPositionChange(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount;
setState(() {
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition);
}
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.itemCount < 1) return widget.stub ?? Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
alignment: Alignment.center,
child: TabBar(
isScrollable: true,
controller: controller,
labelColor: Theme.of(context).primaryColor,
unselectedLabelColor: Theme.of(context).hintColor,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
),
tabs: List.generate(
widget.itemCount,
(index) => widget.tabBuilder(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount,
(index) => widget.pageBuilder(context, index),
),
),
),
],
);
}
onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange(_currentPosition);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll(controller.animation.value);
}
}
}
You can use dynamic children using for loop within your Tabbarview Widget
List<String> categories = ["category 1" , "category 2", "category 3",];
return TabBarView(
children:[
for(var category in categories)
Text(category), // this widget will show a text with specific category. You can use any other widget
],
);
Null safety version
import 'package:flutter/material.dart';
class CustomTabView extends StatefulWidget {
final int? itemCount;
final IndexedWidgetBuilder? tabBuilder;
final IndexedWidgetBuilder? pageBuilder;
final Widget? stub;
final ValueChanged<int>? onPositionChange;
final ValueChanged<double>? onScroll;
final int? initPosition;
CustomTabView({this.itemCount, this.tabBuilder, this.pageBuilder, this.stub,
this.onPositionChange, this.onScroll, this.initPosition});
#override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView> with TickerProviderStateMixin {
late TabController controller;
late int _currentCount;
late int _currentPosition;
#override
void initState() {
_currentPosition = widget.initPosition!;
controller = TabController(
length: widget.itemCount!,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation!.addListener(onScroll);
_currentCount = widget.itemCount!;
super.initState();
}
#override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation!.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition!;
}
if (_currentPosition > widget.itemCount! - 1) {
_currentPosition = widget.itemCount! - 1;
_currentPosition = _currentPosition < 0 ? 0 :
_currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance!.addPostFrameCallback((_){
if(mounted) {
widget.onPositionChange!(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount!;
setState(() {
controller = TabController(
length: widget.itemCount!,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation!.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition!);
}
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
controller.animation!.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.itemCount! < 1) return widget.stub ?? Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
alignment: Alignment.center,
child: TabBar(
isScrollable: true,
controller: controller,
labelColor: Theme.of(context).primaryColor,
unselectedLabelColor: Theme.of(context).hintColor,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
),
tabs: List.generate(
widget.itemCount!,
(index) => widget.tabBuilder!(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount!,
(index) => widget.pageBuilder!(context, index),
),
),
),
],
);
}
onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange!(_currentPosition);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll!(controller.animation!.value);
}
}
}

Categories

Resources