Flutter - select only single item in list view - flutter

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;
});
},
);
},
);
}
}

Related

How to dynamically add a TextFormField with initialization data in the middle of a list of UI TextFormFields?

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),
)
],
);
}
}

How to switch BottomNavigationBar tabs Programmatically in flutter (and Navigate between pages)

I am using pretty new to flutter.So any good advice will be helpful to me.
I am creating an app which have three pages - one home page, one details page, and one Settings page. So in my code for home page, after submitting some data, the user will be taken to page two (ie. the details page)
I am using curved_navigation_bar [ https://pub.dev/packages/curved_navigation_bar ] package as my BottomNavigationBar.
HomeView.dart
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
//necessary imports
class HomeView extends StatefulWidget {
final GlobalKey globalKey;
HomeView(this.globalKey);
#override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
#override
Widget build(BuildContext context) {
return Container(
child: //SomeButton(
onPressed: () => {
final CurvedNavigationBar navigationBar = widget.globalKey.currentWidget;
navigationBar.onTap(1); //<-This is the line where user will be taken to page 2
}
),
);
}
}
Now I have made a NavBar.dart file to manage navigation in my app.
NavBar.dart
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
//necessary imports
class NavBar extends StatefulWidget {
NavBar({Key key}) : super(key: key);
#override
_NavBarState createState() => _NavBarState();
}
class _NavBarState extends State<NavBar> {
GlobalKey globalKey = new GlobalKey(debugLabel: 'btm_app_bar');
ChatSessionDetails _chatSessionDetails = ChatSessionDetails();
int _currentIndex;
String _appBarTitle;
List<Widget> pages = [];
final PageStorageBucket bucket = PageStorageBucket();
void initState() {
super.initState();
pages = [
HomeView(globalKey),
ChatView(key: PageStorageKey('ChatPage')),
SettingsView()
];
_currentIndex = 0;
_appBarTitle = 'HomePage';
}
void changeTab(int index) { //<-PageChange logic
switch (index) {
case 0:
setState(
() => {
_appBarTitle = 'HomePage',
_currentIndex = 0,
},
);
break;
case 1:
if (_sessionDetails.file != null) {
setState(
() => {
_appBarTitle = //some title,
_currentIndex = 1
},
);
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('You haven\'t opened a session yet'),
behavior: SnackBarBehavior.floating,
));
}
break;
case 2:
setState(
() => {
_appBarTitle = 'Settings',
_currentIndex = 2,
},
);
break;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_appBarTitle),
),
body: PageStorage(
child: pages[_currentIndex],
bucket: bucket,
),
extendBody: true,
bottomNavigationBar: CurvedNavigationBar(
key: globalKey, items: <Widget>[
Icon(Icons.home_rounded, size: 30, color: Colors.grey[600]),
Icon(CustomIcons.whatsapp, size: 30, color: Colors.grey[600]),
Icon(Icons.settings_rounded, size: 30, color: Colors.grey[600]),
],
onTap: (index) {
changeTab(index); //Handle button tap
},
),
);
}
}
The whole navigation thing goes pretty fine, but when I call that changeTab() function programmatically, (not by clicking on the bottom navigation tabs) things get weird.
Below is link to the gif MyApp
The tab should change as I didn't changed the state as _sessionDetails.file != null
is false. How can I avoid this ?
in changeTab you need to use the key to change page:
globalKey.currentState.setPage(1);
[UPDATED]
Official way from author:
Change page programmatically
//State class
int _page = 0;
GlobalKey _bottomNavigationKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: CurvedNavigationBar(
key: _bottomNavigationKey,
items: <Widget>[
Icon(Icons.add, size: 30),
Icon(Icons.list, size: 30),
Icon(Icons.compare_arrows, size: 30),
],
onTap: (index) {
setState(() {
_page = index;
});
},
),
body: Container(
color: Colors.blueAccent,
child: Center(
child: Column(
children: <Widget>[
Text(_page.toString(), textScaleFactor: 10.0),
RaisedButton(
child: Text('Go To Page of index 1'),
onPressed: () {
//Page change using state does the same as clicking index 1 navigation button
final CurvedNavigationBarState navBarState =
_bottomNavigationKey.currentState;
navBarState.setPage(1);
},
)
],
),
),
));
}

Flutter - How to change state from inside another widget

I'm trying to implement both a Drawer and the Bottom Navigation Bar. The below class NavigationHomeScreen is my home screen and when the user clicks a menu item in the Drawer the Changeindex function updates the screenView I would like to know how I can similarly update the screenView but from the BottomNavigationBarApp using the onTabTapped method.
class NavigationHomeScreen extends StatefulWidget {
#override
_NavigationHomeScreenState createState() => _NavigationHomeScreenState();
}
class _NavigationHomeScreenState extends State<NavigationHomeScreen> {
Widget screenView;
DrawerIndex drawerIndex;
#override
void initState() {
drawerIndex = DrawerIndex.HOME;
screenView = const MyHomePage();
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
color: AppTheme.nearlyWhite,
child: SafeArea(
top: false,
bottom: false,
child: Scaffold(
appBar: emptyAppbar(),
backgroundColor: AppTheme.nearlyWhite,
body: DrawerUserController(
screenIndex: drawerIndex,
drawerWidth: MediaQuery.of(context).size.width * 0.75,
onDrawerCall: (DrawerIndex drawerIndexdata) {
changeIndex(drawerIndexdata);
//callback from drawer for replace screen as user need with passing DrawerIndex(Enum index)
},
screenView: screenView,
//we replace screen view as we need on navigate starting screens like MyHomePage, HelpScreen, FeedbackScreen, etc...
),
bottomNavigationBar:BottomNavigationBarApp(context, 1),
),
),
);
}
void changeIndex(DrawerIndex drawerIndexdata) {
if (drawerIndex != drawerIndexdata) {
drawerIndex = drawerIndexdata;
if (drawerIndex == DrawerIndex.HOME) {
setState(() {
screenView = const MyHomePage();
});
} else if (drawerIndex == DrawerIndex.Help) {
setState(() {
screenView = HelpScreen();
});
} else if (drawerIndex == DrawerIndex.FeedBack) {
setState(() {
screenView = FeedbackScreen();
});
} else if (drawerIndex == DrawerIndex.Invite) {
setState(() {
screenView = InviteFriend();
});
} else {
//do in your way......
}
}
}
}
class BottomNavigationBarApp extends StatelessWidget {
final int bottomNavigationBarIndex;
final BuildContext context;
const BottomNavigationBarApp(this. context, this.bottomNavigationBarIndex);
void onTabTapped(int index) {
}
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: bottomNavigationBarIndex,
type: BottomNavigationBarType.fixed,
selectedFontSize: 10,
selectedLabelStyle: TextStyle(color: CustomColors.BlueDark),
selectedItemColor: CustomColors.BlueDark,
unselectedFontSize: 10,
items: [
BottomNavigationBarItem(
icon: Container(
margin: EdgeInsets.only(bottom: 5),
child: Image.asset(
'assets/images/home.png',
color: (bottomNavigationBarIndex == 1)
? CustomColors.BlueDark
: CustomColors.TextGrey,
),
),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Container(
margin: EdgeInsets.only(bottom: 5),
child: Image.asset(
'assets/images/task.png',
color: (bottomNavigationBarIndex == 0)
? CustomColors.BlueDark
: CustomColors.TextGrey,
),
),
title: Text('Appointments'),
),
],
onTap: onTabTapped,
);
}
}
You can pass a reference to a function down to a widget. Something like this:
In your HomeScreen:
void updateScreenView() {
//Do changes you want here. Dont forget to setState!
}
In your BottomNavigationBarApp (edit: Function must start with a capital F):
final int bottomNavigationBarIndex;
final BuildContext context;
final Function tapHandler;
const BottomNavigationBarApp(this. context, this.bottomNavigationBarIndex, this.tapHandler);
and then just pass the reference on:
bottomNavigationBar:BottomNavigationBarApp(context, 1, updateScreenView),
And assign the function to your handler.
onTap: () => tapHandler(),
The best way to do it and also recommended by the Flutter development team is to use the provider package from flutter to manage the state between those widgets.
The top answer does solves your issue if its small app, but as your application grows, you most likely want to change things far up a widget tree which makes functions impractical.

How to use two global keys for two widgets sharing the same state class

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

coloring containers by tabbing with flutter

I have column with many ((Tabbed)) items, when I tab on one of them it should be colored and the others transparent, and here is my Tabbed classthis image for what I have now with my code
class Tabbed extends StatefulWidget {
#override
_TabbedState createState() => _TabbedState();
}
class _TabbedState extends State<Tabbed> {
Color color = Colors.transparent;
#override
void initState() {
color = Colors.transparent;
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (){
print("tab");
if (color == Colors.transparent){
setState(() {
color = Colors.purple;
});
}
else{
setState(() {
color = Colors.transparent;
});
}
},
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
color: color,
border: Border.all(color: Colors.red,width: 1),
),
),
);
}
}
you can simply try creating a model for your boxes and have a boolean property and store the status of each box in a list to know whether the box is tapped or not ,you can also pefer a list of boolean but I prefer creating a list of model as it will allow you to add more properties, I have modified your code a little bit you can see it working here on dartpad as well
class Tabbed extends StatefulWidget {
#override
_TabbedState createState() => _TabbedState();
}
class _TabbedState extends State<Tabbed> {
Color color = Colors.green;
#override
void initState() {
for(int i=0;i<listLength;i++){
list1.add(
TabModel(isTapped: false)); // assigns initial value to false
}
for(int i=0;i<listLength;i++){
list2.add(
TabModel(isTapped: false)); // assigns initial value to false
}
}
Widget column1(){
return Column(
children: List.generate(
listLength,
(index) =>GestureDetector(
onTap: (){
// this selects only one out of many and disables rest
for(int i=0;i<list1.length;i++){
if(i!=index){
list1[i].isTapped=false;
}
};
setState((){
list1[index].isTapped = true;
});
},
child:Container(
margin:EdgeInsets.all(5),
color: list1[index].isTapped? Colors.red:color,
height:100,
width:100
))
));
}
Widget column2(){
return Column(
children: List.generate(
listLength,
(index) =>GestureDetector(
onTap: (){
setState((){
list2[index].isTapped = !list2[index].isTapped;
});
},
child:Container(
margin:EdgeInsets.all(5),
color: list2[index].isTapped? Colors.red:color,
height:100,
width:100
))
));
}
List<TabModel> list1 =[];
List<TabModel> list2 =[];
int listLength=5;
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
column1(),
column2()
],);
}
}
class TabModel{
bool isTapped = false;
TabModel({this.isTapped});
}
if this is not what you want or if you face any issue understanding any part of the code feel free to drop a comment I would love to help out
the bwlow output shows two independent columns in coumn1 you can select one out of many boxes and in other you can select multiple boxes