Related
I have a list of dynamic forms where I need to add and remove form fields between two fields dynamically. I am able to add/remove form fields from the bottom of the list properly.
However, when I try to add a form field in between two form fields the data for the field does not update correctly.
How can I correctly add a field in between the two fields and populate the data correctly?
import 'package:flutter/material.dart';
class DynamicFormWidget extends StatefulWidget {
const DynamicFormWidget({Key? key}) : super(key: key);
#override
State<DynamicFormWidget> createState() => _DynamicFormWidgetState();
}
class _DynamicFormWidgetState extends State<DynamicFormWidget> {
List<String?> names = [null];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dynamic Forms'),
),
body: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
itemBuilder: (builderContext, index) => Row(
children: [
Flexible(
child: TextFormField(
initialValue: names[index],
onChanged: (name) {
names[index] = name;
debugPrint(names.toString());
},
decoration: InputDecoration(
hintText: 'Enter your name',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8))),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: () {
setState(() {
if(index + 1 == names.length){
names.add( null); debugPrint('Added: $names');
} else {
names.insert(index + 1, null); debugPrint('Added [${index+1}]: $names');
}
});
},
color: Colors.green,
iconSize: 32,
icon: const Icon(Icons.add_circle)),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: (index == 0&& names.length == 1)
? null
: () {
setState(() {
names.removeAt(index);
});
debugPrint('Removed [$index]: $names');
},
color: Colors.red,
iconSize: 32,
icon: const Icon(Icons.remove_circle)),
),
],
),
separatorBuilder: (separatorContext, index) => const SizedBox(
height: 16,
),
itemCount: names.length,
),
);
}
}
Basically the problem is that Flutter is confused about who is who in your TextFormField list.
To fix this issue simply add a key to your TextFormField, so that it can be uniquely identified by Flutter:
...
child: TextFormField(
initialValue: names[index],
key: UniqueKey(), // add this line
onChanged: (name) {
...
If you want to learn more about keys and its correct use take a look at this.
The widget AnimatedList solves this problem, it keep track of the widgets as a list would do and uses a build function so it is really easy to sync elements with another list. If you end up having a wide range of forms you can make use of the InheritedWidget to simplify the code.
In this sample i'm making use of the TextEditingController to abstract from the form code part and to initialize with value (the widget inherits from the ChangeNotifier so changing the value will update the text in the form widget), for simplicity it only adds (with the generic text) and removes at an index.
To make every CustomLineForm react the others (as in: disable remove if it only remains one) use a StreamBuilder or a ListModel to notify changes and make each entry evaluate if needs to update instead of rebuilding everything.
class App extends StatelessWidget {
final print_all = ChangeNotifier();
App({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FormList(print_notifier: print_all),
floatingActionButton: IconButton(
onPressed: print_all.notifyListeners,
icon: Icon(Icons.checklist),
),
),
);
}
}
class FormList extends StatefulWidget {
final ChangeNotifier print_notifier;
FormList({required this.print_notifier, super.key});
#override
_FormList createState() => _FormList();
}
class _FormList extends State<FormList> {
final _controllers = <TextEditingController>[];
final _list_key = GlobalKey<AnimatedListState>();
void print_all() {
for (var controller in _controllers) print(controller.text);
}
#override
void initState() {
super.initState();
widget.print_notifier.addListener(print_all);
_controllers.add(TextEditingController(text: 'Inital entrie'));
}
#override
void dispose() {
widget.print_notifier.removeListener(print_all);
for (var controller in _controllers) controller.dispose();
super.dispose();
}
void _insert(int index) {
final int at = index.clamp(0, _controllers.length - 1);
_controllers.insert(at, TextEditingController(text: 'Insert at $at'));
// AnimatedList will take what is placed in [at] so the controller
// needs to exist before adding the widget
_list_key.currentState!.insertItem(at);
}
void _remove(int index) {
final int at = index.clamp(0, _controllers.length - 1);
// The widget is replacing the original, it is used to animate the
// disposal of the widget, ex: size.y -= delta * amount
_list_key.currentState!.removeItem(at, (_, __) => Container());
_controllers[at].dispose();
_controllers.removeAt(at);
}
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _list_key,
initialItemCount: _controllers.length,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
itemBuilder: (ctx, index, _) {
return CustomLineForm(
index: index,
controler: _controllers[index],
on_insert: _insert,
on_remove: _remove,
);
},
);
}
}
class CustomLineForm extends StatelessWidget {
final int index;
final void Function(int) on_insert;
final void Function(int) on_remove;
final TextEditingController controler;
const CustomLineForm({
super.key,
required this.index,
required this.controler,
required this.on_insert,
required this.on_remove,
});
#override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: TextFormField(
controller: controler,
),
),
IconButton(
icon: Icon(Icons.add_circle),
onPressed: () => on_insert(index),
),
IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () => on_remove(index),
)
],
);
}
}
I am making an application where I can run through items in a GridView on a profile page, like on Instagram when we scroll our posts.
I want to load more items (15 per 15) when I scroll on my GridView.
I want an infinite loading.
So I added a ScrollListener to my GridView.
If I put an "initialScrollOffset" to "5.0" in attribute to my ScrollListener, it will load the 15 first items and make one loading, so it's add 15 items (work only 1 time), but if I let the default value, it loads no items.
I would like to have an infinite loading.
My GridView code :
import 'dart:developer';
import 'package:dresskip/model/item_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class ItemSection extends StatefulWidget {
const ItemSection({Key? key}) : super(key: key);
#override
_ItemSectionState createState() => _ItemSectionState();
}
class _ItemSectionState extends State<ItemSection> {
List<Item> items = [];
bool isLoading = false;
int pageCount = 1;
ScrollController _scrollController = ScrollController(initialScrollOffset: 5.0);
#override
void initState() {
super.initState();
///LOADING FIRST DATA
addItemsToList(1);
_scrollController.addListener(_scrollListener);
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Row(),
GridView.count(
controller: _scrollController,
crossAxisCount: 3,
shrinkWrap: true,
//physics: const ScrollPhysics() /*AlwaysScrollableScrollPhysics()*/,
mainAxisSpacing: 0,
children: items.map((value) {
return Image.network(value.picture);
}).toList(),
)
],
);
}
//// ADDING THE SCROLL LISTINER
_scrollListener() {
//inspect(_scrollController.offset);
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent &&
!_scrollController.position.outOfRange) {
setState(() {
print("comes to bottom $isLoading");
isLoading = true;
if (isLoading) {
print("RUNNING LOAD MORE");
pageCount = pageCount + 1;
addItemsToList(pageCount);
}
});
}
}
addItemsToList(int page) {
//if (page < 5) {}
Item myItem = Item(
name: "test",
brand: "test",
color: ["0xFF39BDC8", "0xFFdb8abc", ""],
picture:
"https://images.pexels.com/photos/9676177/pexels-photo-9676177.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
//"https://scontent.fcdg2-1.fna.fbcdn.net/v/t1.6435-9/171944671_3950113148381954_7059062044076097927_n.jpg?_nc_cat=110&ccb=1-5&_nc_sid=09cbfe&_nc_ohc=gxbPXmRQmN8AX9V5Bx5&_nc_ht=scontent.fcdg2-1.fna&oh=ac2a57c8c1d1b0b01fcf131ac42c4023&oe=6190A9BF",
solo: false,
clean: true,
type: "test");
for (int i = (pageCount * 15) - 15; i < pageCount * 15; i++) {
items.add(myItem);
isLoading = false;
}
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
Item model class
class Item {
String name;
String brand;
List<String> color;
String picture;
bool solo;
bool clean;
String type;
Item({
required this.name,
required this.brand,
required this.color,
required this.picture,
required this.solo,
required this.clean,
required this.type,
});
}
The first part (profile section) code
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '/assets/constants.dart' as constants;
import '../../assets/dresskip_icon_icons.dart' as DresskipIcons;
class ProfileSection extends StatelessWidget {
final List<String> description;
final VoidCallback onClicked;
const ProfileSection({
Key? key,
required this.description,
required this.onClicked,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(10, 10, 10, 10),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment
.spaceBetween, //pour que chaque colonnes dans la ligne ait leurs propres tailles
crossAxisAlignment: CrossAxisAlignment
.start, //pour tout coller en haut du container
children: <Widget>[
const Icon(Icons.local_laundry_service),
Stack(children: [
buildImage(),
Positioned(
bottom: 0,
right: 4,
child:
buildEditIcon(Color(constants.COLOR_BLUE_DRESSKIP)))
]),
const Icon(Icons.settings),
],
),
// Partie description
Container(
child: Text(description[0]),
margin: EdgeInsets.fromLTRB(50, 5, 50, 5)),
// Partie Instagram
Container(
child: Row(
children: <Widget>[
Container(
child: const Icon(DresskipIcons.DresskipIcon.instagram),
margin: const EdgeInsets.fromLTRB(0, 0, 20, 0),
),
Expanded(child: Text(description[1]))
],
),
margin: const EdgeInsets.fromLTRB(20, 5, 20, 5)),
// Partie Facebook
Container(
child: Row(
children: <Widget>[
Container(
child: const Icon(Icons.facebook),
margin: const EdgeInsets.fromLTRB(0, 0, 20, 0),
),
Expanded(child: Text(description[2]))
],
),
margin: const EdgeInsets.fromLTRB(20, 5, 20, 5)),
// Partie Twitter
Container(
child: Row(
children: <Widget>[
Container(
child:
const Icon(DresskipIcons.DresskipIcon.twitter_square),
margin: const EdgeInsets.fromLTRB(0, 0, 20, 0),
),
Expanded(child: Text(description[3]))
],
),
margin: const EdgeInsets.fromLTRB(20, 5, 20, 5)),
],
));
}
// Widget pour afficher l'image
Widget buildImage() {
// use if is an image on the web with the link : final image = NetworkImage(imagePath);
return ClipOval(
child: Material(
color: Colors.transparent,
child: Ink.image(
image: const AssetImage("assets/undraw_female_avatar.png"),
fit: BoxFit.cover,
width: 128,
height: 128,
child: InkWell(onTap: onClicked),
)));
}
// Widget pour l'ajout de l'icone à coté de l'image
// Ici, il y a 2 fois buildCircle pour arrondir l'icone et ensuite mettre le trait blanc arrondi entre la photo et l'icône
Widget buildEditIcon(Color color) => buildCircle(
color: Colors.white,
all: 1,
child: buildCircle(
color: color,
all: 8,
child: Icon(Icons.add_a_photo, color: Colors.white, size: 20)));
// Widget permettant d'arrondir l'image
Widget buildCircle({
required Widget child,
required double all,
required Color color,
}) =>
ClipOval(
child: Container(
padding: EdgeInsets.all(all), color: color, child: child));
}
The parent page code
import 'package:dresskip/model/user_model.dart';
import 'package:flutter/material.dart';
import 'itemSection_widget.dart';
import 'profileSection_widget.dart';
import '/assets/constants.dart' as constants;
import 'dart:convert';
class AccountPage extends StatelessWidget {
const AccountPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
ProfileSection(
description: getUserInformation().properties,
onClicked: () async {}),
const Divider(
color: Color(constants.COLOR_BLUE_DRESSKIP),
thickness: 2,
indent: 50,
endIndent: 50,
),
ItemSection()
],
)));
}
getUserInformation() {
User myUser = User(username: "test", email: "test#test.test", properties: [
"22 yo\nFlexeur/Epicurien/Philanthrope\nJ'adore la vie\nEFREI Paris",
"instagram_test",
"facebook_test",
"twitter"
]);
return myUser;
}
}
There are 2 screenshots of my App.
The problem here, it's just loading 15 + 15 items (the first 30) and I can't load more data on scrolling.
EDIT
I find a way to resolv this problem. The attribute "shrinkwrap" block the possibility to scroll more because my widget which contains the gridview is into a Column Widget.
So i removed it, but just the Gridview is scrolling, I would like to do like Instagram's profil, when you scroll on your pictures, all the page scroll and not only the GridView.
Do you have an idea ?
Endless / Infinite Scroll GridView
This example uses a GridView.builder & doesn't need a ScrollController.
When the end of the current dataset is reached, it will request more data and rebuild the GridView.
We can pad the end of the dataset with a special item. When this special item is built by GridView.builder, it will:
show a loading indicator
request more data from datasource
rebuild the GridView when data arrives
import 'package:flutter/material.dart';
class InfiniteScrollPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Infinite Scroll'),
),
body: EndlessGrid());
}
}
class EndlessGrid extends StatefulWidget {
#override
_EndlessGridState createState() => _EndlessGridState();
}
class _EndlessGridState extends State<EndlessGrid> {
NumberGenerator _numGen = NumberGenerator();
List<int> _data = [];
#override
void initState() {
super.initState();
_data = _numGen.nextPage();
}
#override
Widget build(BuildContext context) {
return GridView.builder(
itemCount: _data.length + 1, // pad data with an extra item at end
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 10, mainAxisSpacing: 10, crossAxisCount: 2),
itemBuilder: (context, i) {
if (i < _data.length) {
return gridItem(i);
}
else { // extra item will request next page & rebuild widget
getNextPage();
return CircularProgressIndicator();
}
},
);
}
Widget gridItem(int i) {
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.lightBlue
),
padding: EdgeInsets.all(5),
child: Text('$i'),
);
}
/// Request next page of data and call setState to rebuild GridView with
/// new data.
Future<void> getNextPage() async {
var _nextPage = await Future.delayed(Duration(seconds: 2), _numGen.addPage);
setState(() {
_data = _nextPage;
});
}
}
/// Mock data to fill GridView
class NumberGenerator {
static const PAGESIZE = 15;
List<int> dataset = [];
final int start;
NumberGenerator({this.start = 0});
List<int> addItem() {
dataset.add(lastItem + 1);
return dataset;
}
List<int> addPage() {
dataset.addAll(nextPage());
return dataset;
}
int get lastItem => dataset.isNotEmpty ? dataset.last : start;
List<int> nextPage({int start, int size = PAGESIZE}) {
start ??= lastItem;
return List<int>.generate(size, (i) => start + i + 1);
}
}
Firstly, I'm not aware of Instagram UI.
The problem is here with parent, while SingleChildScrollView is the parent and you want to scroll the full page and generate GridItem based on it, therefore set we don't need to ScrollController for GridView instead use it on SingleChildScrollView.
The code structure will be
parent class generate data for GridView and will be StatefulWidget.
while there are two scrollable widgets so that GridView < physics: NeverScrollableScrollPhysics(),>
AccountPage: SingleChildScrollView will have that _scrollController
ItemSection can be StatelessWidget
Full Code snippet with dummyHeaderWidget
import 'package:flutter/material.dart';
class AccountPage extends StatefulWidget {
const AccountPage({Key? key}) : super(key: key);
#override
State<AccountPage> createState() => _AccountPageState();
}
class _AccountPageState extends State<AccountPage> {
ScrollController _scrollController = ScrollController();
bool isLoading = true; //
int pageCount = 1;
List items = [];
//// ADDING THE SCROLL LISTINER
void _scrollListener() {
print(
"current ${_scrollController.offset} max: ${_scrollController.position.maxScrollExtent}");
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent &&
!_scrollController.position.outOfRange) {
setState(() {
print("comes to bottom $isLoading");
isLoading = true;
if (isLoading) {
print("RUNNING LOAD MORE");
pageCount = pageCount + 1;
addItemsToList(pageCount);
}
});
}
}
addItemsToList(int page) {
//if (page < 5) {}
Item myItem = Item(
name: "test",
brand: "test",
color: ["0xFF39BDC8", "0xFFdb8abc", ""],
picture:
"https://images.pexels.com/photos/9676177/pexels-photo-9676177.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
//"https://scontent.fcdg2-1.fna.fbcdn.net/v/t1.6435-9/171944671_3950113148381954_7059062044076097927_n.jpg?_nc_cat=110&ccb=1-5&_nc_sid=09cbfe&_nc_ohc=gxbPXmRQmN8AX9V5Bx5&_nc_ht=scontent.fcdg2-1.fna&oh=ac2a57c8c1d1b0b01fcf131ac42c4023&oe=6190A9BF",
solo: false,
clean: true,
type: "test");
for (int i = (pageCount * 15) - 15; i < pageCount * 15; i++) {
items.add(myItem);
isLoading = false;
}
}
#override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
addItemsToList(1);
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: [
Container(
height: 300,
color: Colors.green,
),
const Divider(
thickness: 2,
indent: 50,
endIndent: 50,
),
ItemSection(
items: items,
),
],
),
),
);
}
}
class ItemSection extends StatelessWidget {
final List items;
const ItemSection({
Key? key,
required this.items,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
mainAxisSpacing: 0,
children: items.map((value) {
return Image.network(value.picture);
}).toList(),
);
}
}
class Item {
String name;
String brand;
List<String> color;
String picture;
bool solo;
bool clean;
String type;
Item({
required this.name,
required this.brand,
required this.color,
required this.picture,
required this.solo,
required this.clean,
required this.type,
});
}
Read the code, hope you will get concept and make changes on your project.
Recently I have downloaded the Louis Vuitton App. I found a strange type of horizontal scroll of product items in listview. I tried card_swiper package but couldnot get through it. How can I achieve such scroll as in gif below?
the trick here is to use a stack and:
Use a page view to display every element except the first one
Use a left aligned FractionallySizedBox which displays the first item and grows with the first item offset
It took me a few tries but the result is very satisfying, I'll let you add the bags but here you go with colored boxes ;) :
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: FunList()));
}
class FunList extends StatefulWidget {
#override
State<FunList> createState() => _FunListState();
}
class _FunListState extends State<FunList> {
/// The colors of the items in the list
final _itemsColors = List.generate(
100,
(index) => Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0),
);
/// The current page of the page view
double _page = 0;
/// The index of the leftmost element of the list to be displayed
int get _firstItemIndex => _page.toInt();
/// The offset of the leftmost element of the list to be displayed
double get _firstItemOffset => _controller.hasClients ? 1 - (_page % 1) : 1;
/// Controller to get the current position of the page view
final _controller = PageController(
viewportFraction: 0.25,
);
/// The width of a single item
late final _itemWidth = MediaQuery.of(context).size.width * _controller.viewportFraction;
#override
void initState() {
super.initState();
_controller.addListener(() => setState(() {
_page = _controller.page!;
}));
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Center(
child: Stack(
children: [
Positioned.fill(
child: Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: _itemWidth,
child: FractionallySizedBox(
widthFactor: _firstItemOffset,
heightFactor: _firstItemOffset,
child: PageViewItem(color: _itemsColors[_firstItemIndex]),
),
),
),
),
SizedBox(
height: 200,
child: PageView.builder(
padEnds: false,
controller: _controller,
itemBuilder: (context, index) {
return Opacity(
opacity: index <= _firstItemIndex ? 0 : 1,
child: PageViewItem(color: _itemsColors[index]),
);
},
itemCount: _itemsColors.length,
),
),
],
),
);
}
}
class PageViewItem extends StatelessWidget {
final Color color;
const PageViewItem({
Key? key,
required this.color,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(10),
color: color,
);
}
}
In my app I am generating a ListView and items can be highlighted by tapping on them. That works fine and I also have a callback function that gives me the key for the just selected item. I can currently manually deselect the item by tapping on it again, but will ultimately take that functionality out.
My problem is that I want one and only one item to be selected at a time. In order to create the list I currently take some initial content in the form of a list, generate the tiles and add them to another list. I then use that list to create the ListView. My plan was on the callback from a new selection, run through the list of tiles and deselect them before highlighting the new chosen tile and carrying out the other functions. I have tried various methods to tell each tile to deselect itself but have not found any way to address each of the tiles. Currently I get the error:
Class 'OutlineTile' has no instance method 'deselect'.
Receiver: Instance of 'OutlineTile'
Tried calling: deselect()
I have tried to access a method within the tile class and to use a setter but neither worked so far. I am quite new to flutter so it could be something simple I am missing. My previous experience was with Actionscript where this system would have worked fine and I could access a method of an object (in this case the tile) easily as long s it is a public method.
I'd be happy to have another way to unselect the old item or to find a way to access a method within the tile. The challenge is to make the tiles show not highlighted without them being tapped themselves but when a different tile is tapped.
The code in my parent class is as follows:
class WorkingDraft extends StatefulWidget {
final String startType;
final String name;
final String currentContent;
final String currentID;
final List startContent;
WorkingDraft(
{this.startType,
this.name,
this.currentContent,
this.currentID,
this.startContent});
#override
_WorkingDraftState createState() => _WorkingDraftState();
}
class _WorkingDraftState extends State<WorkingDraft> {
final _formKey = GlobalKey<FormState>();
final myController = TextEditingController();
//String _startType;
String _currentContent = "";
String _name = "Draft";
List _startContent = [];
List _outLineTiles = [];
int _counter = 0;
#override
void dispose() {
// Clean up the controller when the widget is disposed.
myController.dispose();
super.dispose();
}
void initState() {
super.initState();
_currentContent = widget.currentContent;
_name = widget.name;
_startContent = widget.startContent;
_counter = 0;
_startContent.forEach((element) {
_outLineTiles.add(OutlineTile(
key: Key("myKey$_counter"),
outlineName: element[0],
myContent: element[1],
onTileSelected: clearHilights,
));
_counter++;
});
}
dynamic clearHilights(Key myKey) {
_outLineTiles.forEach((element) {
element.deselect(); // this throws an error Class 'OutlineTile' has no instance method 'deselect'.
Key _foundKey = element.key;
print("Element Key $_foundKey");
});
}
.......
and further down within the widget build scaffold:
child: ListView.builder(
itemCount: _startContent.length,
itemBuilder: (context, index) {
return _outLineTiles[index];
},
),
Then the tile class is as follows:
class OutlineTile extends StatefulWidget {
final Key key;
final String outlineName;
final Icon myIcon;
final String myContent;
final Function(Key) onTileSelected;
OutlineTile(
{this.key,
this.outlineName,
this.myIcon,
this.myContent,
this.onTileSelected});
#override
_OutlineTileState createState() => _OutlineTileState();
}
class _OutlineTileState extends State<OutlineTile> {
Color color;
Key _myKey;
#override
void initState() {
super.initState();
color = Colors.transparent;
}
bool _isSelected = false;
set isSelected(bool value) {
_isSelected = value;
print("set is selected to $_isSelected");
}
void changeSelection() {
setState(() {
_myKey = widget.key;
_isSelected = !_isSelected;
if (_isSelected) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
});
}
void deselect() {
setState(() {
isSelected = false;
color = Colors.transparent;
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
children: [
Card(
elevation: 10,
margin: EdgeInsets.fromLTRB(10.0, 6.0, 5.0, 0.0),
child: SizedBox(
width: 180,
child: Container(
color: color,
child: ListTile(
title: Text(widget.outlineName),
onTap: () {
if (widget.outlineName == "Heading") {
Text("Called Heading");
} else (widget.outlineName == "Paragraph") {
Text("Called Paragraph");
widget.onTileSelected(_myKey);
changeSelection();
},
),
........
Thanks for any help.
Amended Code sample and explanation, that builds to a complete project, from here:
Following the advice from phimath I have created a full buildable sample of the relevant part of my project.
The problem is that the tiles in my listview are more complex with several elements, many of which are buttons in their own right so whilst phimath's solution works for simple text tiles I have not been able to get it working inside my own project. My approach is trying to fundamentally do the same thing as phimath's but when I include these more complex tiles it fails to work.
This sample project is made up of three files. main.dart which simply calls the project and passes in some dummy data in the way my main project does. working_draft.dart which is the core of this issue. And outline_tile.dart which is the object that forms the tiles.
Within working draft I have a function that returns an updated list of the tiles which should show which tile is selected (and later any other changes from the other buttons). This gets called when first going to the screen. When the tile is tapped it uses a callback function to redraw the working_draft class but this seems to not redraw the list as I would expect it to. Any further guidance would be much appreciated.
The classes are:
first class is main.dart:
import 'package:flutter/material.dart';
import 'package:listexp/working_draft.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: WorkingDraft(
startType: "Basic",
name: "Draft",
currentID: "anID",
startContent: [
["Heading", "New Heading"],
["Paragraph", "New Text"],
["Image", "placeholder"],
["Signature", "placeholder"]
],
));
}
}
Next file is working_draft.dart:
import 'package:flutter/material.dart';
import 'package:listexp/outline_tile.dart';
class WorkingDraft extends StatefulWidget {
final String startType;
final String name;
final String currentContent;
final String currentID;
final List startContent;
final int selectedIndex;
WorkingDraft(
{this.startType,
this.name,
this.currentContent,
this.currentID,
this.startContent,
this.selectedIndex});
#override
_WorkingDraftState createState() => _WorkingDraftState();
}
class _WorkingDraftState extends State<WorkingDraft> {
int selectedIndex;
String _currentContent = "";
String _name = "Draft";
List _startContent = [];
var _outLineTiles = [];
int _counter = 0;
int _selectedIndex;
bool _isSelected;
dynamic clearHilights(int currentIndex) {
setState(() {
_selectedIndex = currentIndex;
});
}
updatedTiles() {
if (_selectedIndex == null) {
_selectedIndex = 0;
}
_currentContent = widget.currentContent;
_name = widget.name;
_startContent = widget.startContent;
_counter = 0;
_outLineTiles = [];
_startContent.forEach((element) {
_isSelected = _selectedIndex == _counter ? true : false;
_outLineTiles.add(OutlineTile(
key: Key("myKey$_counter"),
outlineName: element[0],
myContent: element[1],
myIndex: _counter,
onTileSelected: clearHilights,
isSelected: _isSelected,
));
_counter++;
});
}
#override
Widget build(BuildContext context) {
updatedTiles();
return Scaffold(
body: Center(
child: Column(children: [
SizedBox(height: 100),
Text("Outline", style: new TextStyle(fontSize: 15)),
Container(
height: 215,
width: 300,
decoration: BoxDecoration(
border: Border.all(
color: Colors.lightGreenAccent,
width: 2,
),
borderRadius: BorderRadius.circular(2),
),
child: ListView.builder(
itemCount: _startContent.length,
itemBuilder: (context, index) {
return _outLineTiles[index];
},
),
),
]),
));
}
}
and finally is outline_tile.dart
import 'package:flutter/material.dart';
class OutlineTile extends StatefulWidget {
final Key key;
final String outlineName;
final Icon myIcon;
final String myContent;
final int myIndex;
final Function(int) onTileSelected;
final bool isSelected;
OutlineTile(
{this.key,
this.outlineName,
this.myIcon,
this.myContent,
this.myIndex,
this.onTileSelected,
this.isSelected});
#override
_OutlineTileState createState() => _OutlineTileState();
}
class _OutlineTileState extends State<OutlineTile> {
Color color;
// Key _myKey;
bool _isSelected;
#override
void initState() {
super.initState();
_isSelected = widget.isSelected;
if (_isSelected == true) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
}
void deselect() {
setState(() {
_isSelected = widget.isSelected;
if (_isSelected == true) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
children: [
Card(
elevation: 10,
margin: EdgeInsets.fromLTRB(10.0, 6.0, 5.0, 0.0),
child: SizedBox(
width: 180,
child: Container(
color: color,
child: ListTile(
title: Text(widget.outlineName),
onTap: () {
if (widget.outlineName == "Heading") {
Text("Called Heading");
} else if (widget.outlineName == "Paragraph") {
Text("Called Paragraph");
} else if (widget.outlineName == "Signature") {
Text("Called Signature");
} else {
Text("Called Image");
}
var _myIndex = widget.myIndex;
widget.onTileSelected(_myIndex);
deselect();
},
),
),
),
),
SizedBox(
height: 60,
child: Column(
children: [
SizedBox(
height: 20,
child: IconButton(
iconSize: 30,
icon: Icon(Icons.arrow_drop_up),
onPressed: () {
print("Move Up");
}),
),
SizedBox(height: 5),
SizedBox(
height: 20,
child: IconButton(
iconSize: 30,
icon: Icon(Icons.arrow_drop_down),
onPressed: () {
print("Move Down");
}),
),
],
),
),
SizedBox(
height: 60,
child: Column(
children: [
SizedBox(
height: 20,
child: IconButton(
iconSize: 20,
icon: Icon(Icons.add_box),
onPressed: () {
print("Add another");
}),
),
SizedBox(
height: 10,
),
SizedBox(
height: 20,
child: IconButton(
iconSize: 20,
icon: Icon(Icons.delete),
onPressed: () {
print("Delete");
}),
),
],
),
),
],
),
);
}
}
Thanks again
Instead of manually deselecting tiles, just keep track of which tile is currently selected.
I've made a simple example for you. When we click a tile, we just set the selected index to the index we clicked, and each tile looks at that to see if its the currently selected tile.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(body: Home()),
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int selectedIndex;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item: $index'),
tileColor: selectedIndex == index ? Colors.blue : null,
onTap: () {
setState(() {
selectedIndex = index;
});
},
);
},
);
}
}
I am trying to create my own custom segment in flutter. That segment has two buttons, one for teachers and other for students. What I am trying to do, it's encapsulate the buttons in one Stateful Widget to handle the setState of both buttons, because I want the buttons to be an AnimatedContainer and if I rebuild the childrens (the buttons) from the parent the transition doesn't works.
Note that the buttons are Stack positioned and I reorder the content to get the tapped button over the other (that will has effect when I set more width in the tapped button, now this is not created yet).
Here is my code:
import 'package:flutter/cupertino.dart';
import '../../app_localizations.dart';
import '../../styles.dart';
GlobalKey<_ButtonState> teachersButtonKey = GlobalKey();
GlobalKey<_ButtonState> studentsButtonKey = GlobalKey();
String _globalTappedButtonId = 'teachersButton';
class FiltersAppBarSegment extends StatefulWidget {
#override
_FiltersAppBarSegmentState createState() => _FiltersAppBarSegmentState();
}
class _FiltersAppBarSegmentState extends State<FiltersAppBarSegment> {
List<Widget> buildStackChildren(SegmentChangedCallBack handleSegmentChanged) {
if (_globalTappedButtonId == 'teachersButton') {
return <Widget>[
Container(
key: UniqueKey(),
child: _Button(
key: studentsButtonKey,
id: 'studentsButton',
label: 'seeStudents',
rightPosition: 1,
onSegmentChanged: handleSegmentChanged,
),
),
Container(
key: UniqueKey(),
child: _Button(
key: teachersButtonKey,
id: 'teachersButton',
label: 'amTeacher',
rightPosition: null,
onSegmentChanged: handleSegmentChanged,
),
),
];
} else {
return <Widget>[
Container(
key: UniqueKey(),
child: _Button(
key: driverButtonKey,
id: 'driverButton',
label: 'amDriver',
rightPosition: null,
onSegmentChanged: handleSegmentChanged,
),
),
Container(
key: UniqueKey(),
child: _Button(
key: studentsButtonKey,
id: 'studentButton',
label: 'amStudent',
rightPosition: 1,
onSegmentChanged: handleSegmentChanged,
),
),
];
}
}
void handleSegmentChanged(String clickedButtonId) {
teachersButtonKey.currentState._handleButtonTapped();
studentsButtonKey.currentState._handleButtonTapped();
}
#override
Widget build(BuildContext context) {
return Container(
height: 42,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Stack(children: buildStackChildren(handleSegmentChanged)),
);
}
}
class _Button extends StatefulWidget {
final String id;
final String label;
final double rightPosition;
final void onSegmentChanged;
_Button({
Key key,
this.id,
this.label,
this.rightPosition,
this.onSegmentChanged,
}) : super(key: key);
#override
_ButtonState createState() => _ButtonState();
}
class _ButtonState extends State<_Button> {
bool _tapped;
double _topPosition;
double _width;
double _height;
double _getTopPosition() => _tapped ? 0 : 5;
double _getHeight() => _tapped ? 42 : 32;
Gradient _getGradient() {
if (_tapped) {
return Styles.darkAccentColorGradient;
} else {
return Styles.darkAccentColorGradientDisabled;
}
}
void _handleButtonTapped() {
setState(() {
_globalTappedButtonId = widget.id;
_tapped = (widget.id == _globalTappedButtonId);
_topPosition = _getTopPosition();
_height = _getHeight();
});
}
#override
void initState() {
super.initState();
_tapped = (widget.id == _globalTappedButtonId);
_topPosition = _getTopPosition();
_height = _getHeight();
}
#override
Widget build(BuildContext context) {
return Positioned(
top: _topPosition,
right: widget.rightPosition,
child: GestureDetector(
onTap: () {
widget.onSegmentChanged('test');
},
child: AnimatedContainer(
duration: Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
width: _width,
height: _height,
decoration: BoxDecoration(
gradient: _getGradient(),
borderRadius: BorderRadius.circular(13),
),
child: Center(
child: Text(
AppLocalizations.of(context).translate(widget.label),
style: Styles.bodyWhiteText,
textAlign: TextAlign.center,
),
),
),
),
);
}
}
I'm sure you have already found a solution to your problem by now, but this question is one of the first search results when looking at this error.
As you already know, per the Flutter doc on GlobalKey:
"You cannot simultaneously include two widgets in the tree with the
same global key. Attempting to do so will assert at runtime."
You can define your own individual keys like:
import 'package:flutter/widgets.dart';
class TestKeys{
static final testKey1 = const Key('__TESTKEY1__');
static final testKey2 = const Key('__TESTKEY2__');
...
}
And then reference them in the widget with key: TestKeys.testKey1
This was described in this question here so perhaps it can help someone with the need for a similar use case.
There are also a few solutions listed in this GitHub issue