I am building a Flutter App where a List is shown using ListView. Which returns a ListTile with User Information. The ListTile contains a leading, title and subtitle with trailing set as a ElevatedButton.
Here's how it looks like:
I want to tap on the 'Invite Button' and change its color, text, and subtitle of the ListTile
After tapping, it should look like this.
How can I do this? Here's the code that I wrote. But it's changing the state of every List Item.
class InviteFriends extends StatefulWidget {
const InviteFriends({Key? key}) : super(key: key);
#override
State<InviteFriends> createState() => _InviteFriendsState();
}
class _InviteFriendsState extends State<InviteFriends> {
bool isSelected = false;
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
}
UI Under ListView.builder:
ListTile(
title: const Text('Haris'),
subtitle: const Text(
isSelected ? 'Invitation Sent' : 'Not Invited Yet',
),
leading: CircleAvatar(
backgroundImage: NetworkImage(
_userProfile!.profilePhoto.toString(),
),
),
trailing: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0.0,
primary: isSelected ? Colors.orange : Colors.green,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
child: const Text(
isSelected ? 'Invited': 'Invite',
),
onPressed: () {
setState(() {
isSelected = !isSelected;
});
},
),
);
I also tried Keys here in ListTile by using ValueKey(index) but it didn't work either. What should I do? Thanks
Bring out Tile as a separate StatefulWidget so that each has its own state.
Do not modify State List.builder.
And you use one isSelected field for all Tile, and you will all refer to this condition.
Please provide more code and I can help. So that I understand the full picture
You need create different class for listtiles. Do as follows:
ListView.builder(
itemCount: 3,
shrinkWrap: true,
itemBuilder: (ctx, i) {
return MyListItems();
}))
then MyListItems.
class MyListItems extends StatefulWidget {
#override
MyListState createState() => MyListState();
}
class MyListState extends State<MyListItems> {
bool isSelected = false;
#override
Widget build(BuildContext context) {
return ListTile(
title: const Text('Haris'),
subtitle: Text(
isSelected ? "Invitation Sent" : 'Not Invited Yet',
),
leading: const CircleAvatar(
backgroundImage: NetworkImage(
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYtfZRhbGQtq2BapB2MXJfWIO2QriO5Wx3qQ&usqp=CAU'),
),
// use your image here
trailing: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0.0,
primary: isSelected ? Colors.orange : Colors.green,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
child: Text(
isSelected ? 'Invited' : 'Invite',
),
onPressed: () {
setState(() {
isSelected = !isSelected;
});
},
),
);
}}
return ListView.builder(
itemCount: itemCount.length
itemBuilder: (BuildContext context, int index) {
return Friend(
//arg if available
);
});
class Friend extends StatefulWidget {
const Friend({Key? key}) : super(key: key);
#override
_FriendState createState() => _FriendState();
}
class _FriendState extends State<Friend> {
bool isSelected = false;
#override
Widget build(BuildContext context) {
//put ListTile detail here
return Row(
children: [
FlatButton(//deprecated but other buttons work
key: PageStorageKey('random num'),//if you are interested in keeping the state of the button while navigating
onPressed: () {
setState(() {
isSelected = !isSelected;
});
},
child: Text(isSelected ? "INVITED" : "INVITE"))
],
);
}
}
You create a list say "i" (variable) to know the state of each tile and initilze all with false. On tap change there state to true.
final List<bool> selected = List.generate(20, (i) => false);
Pass the List "i" to the listview.builder like:-
itemBuilder: (BuildContext context, i)
See the full code
import 'package:flutter/material.dart';
class InviteFriends extends StatefulWidget {
const InviteFriends({Key? key}) : super(key: key);
#override
State<InviteFriends> createState() => _InviteFriendsState();
}
class _InviteFriendsState extends State<InviteFriends> {
bool isSelected = false;
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
final List<bool> selected = List.generate(20, (i) => false);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.only(top: 60),
child: ListView.builder(
// itemCount: i,
itemBuilder: (BuildContext context, i) {
return ListTile(
title: const Text('Haris'),
subtitle: Text(
selected[i] ? 'Invitation Sent' : 'Not Invited Yet',
),
leading: const CircleAvatar(
backgroundImage: AssetImage('assets/profile.gif'),
),
trailing: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0.0,
primary: selected[i] ? Colors.orange : Colors.green,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
child: Text(
selected[i] ? 'Invited' : 'Invite',
),
onPressed: () {
setState(() => selected[i] = !selected[i]);
// setState(() {
// isSelected = !isSelected;
// });
}),
);
}),
),
);
}
}
Thank you
Output
Related
I created a checkbox Listtile, where I can click on an Info button to receive further info if neccessary. Therefore I created a main Widget which contains the Listtile widget. When the Info button gets clicked, the detail page opens and reads the specific details from the model class.Up to that point everything works fine.
My leading is a checkbox. If it gets clicked not just one checkbox gets checked, but all of them. how can I write my code, that they arent checked all at the same time automatically?
thank you very much for your help
Kind regards
Here is my code:
//This is my model
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class Info {
String title;
String description;
//String image;
Info(
{required this.title,
required this.description,
// #required this.image
});
}
List<Info> ModelList = [
Info(
title: 'title1',
description: 'description1'
),
Info(
title: 'title2',
description: 'description2'
),
];
//This is the widget
class MainWidget extends StatefulWidget {
const MainWidget({Key? key}) : super(key: key);
#override
State<MainWidget> createState() => _MainWidgetState();
}
class _MainWidgetState extends State<MainWidget> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder
(itemCount: ModelList.length,
itemBuilder: (context,index)
{Info cardname = ModelList[index];
return Card(
child: CheckboxListTile(
tileColor: const Color(0xFF5D6D7E),
shape: RoundedRectangleBorder(
side: const BorderSide (color:Colors.white,width: 2),
borderRadius: BorderRadius.circular(10),
),
contentPadding: const EdgeInsets.symmetric(vertical: 10.0),
value: timeDilation !=1.0,
onChanged: (bool? value) {
setState (() {
timeDilation = value! ? 5.0 : 1.0;
});
},
title: Text(
cardname.title,
style: const TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.w600,
color: Colors.white),
),
//an welcher Stelle die Checkbox ist (links oder rechts)
controlAffinity:
ListTileControlAffinity.leading,
secondary: IconButton(icon: const Icon(Icons.info_outlined,size: 40,color: Colors.orange,), onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context)=> DetailScreen(cardname)
));
},),
)
);
}
,),
)
;
}
}
//Detail screen
//Detail Screen;
class DetailScreen extends StatelessWidget {
final Info cardname ;
const DetailScreen (this.cardname, {super.key});
//const DetailScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: customAppBar
((cardname.title),),
body: Padding(
padding: const EdgeInsets.all(8.0) ,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Image.network(
// cardname.imageUrl,
//height: 500,
//),
//
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
cardname.description,
textAlign: TextAlign.justify,
style: const TextStyle(fontSize: 22.0),
),
),
],
),
),
) ,
);
}
}
I tried to put the Item builder in an extra widget and return it into the main widget as shown below, but this didn`t work as well
class SubWidget extends StatefulWidget {
const SubWidget({Key? key}) : super(key: key);
#override
State<SubWidget> createState() => _SubWidgetState();
}
class _SubWidgetState extends State<SubWidget> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder
(itemCount: ModelList.length,
itemBuilder: (context,index)
{Info cardname = ModelList[index];
return MainWidget()
Add variable is isSelected bool for Info class
And after onChanged change value: isSelected = !isSelected
Try this:
class Info {
String title;
String description;
bool isSelected;
//String image;
Info({
required this.title,
required this.description,
required this.isSelected,
// #required this.image
});
}
List<Info> ModelList = [
Info(title: 'title1', description: 'description1', isSelected: false),
Info(title: 'title2', description: 'description2', isSelected: false),
];
//This is the widget
class MainWidget extends StatefulWidget {
const MainWidget({Key? key}) : super(key: key);
#override
State<MainWidget> createState() => _MainWidgetState();
}
class _MainWidgetState extends State<MainWidget> {
double timeDilation = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: ModelList.length,
itemBuilder: (context, index) {
Info cardname = ModelList[index];
return Card(
child: CheckboxListTile(
tileColor: const Color(0xFF5D6D7E),
shape: RoundedRectangleBorder(
side: const BorderSide(color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(10),
),
contentPadding: const EdgeInsets.symmetric(vertical: 10.0),
value: cardname.isSelected,
onChanged: (bool? value) {
setState(() {
cardname.isSelected = !cardname.isSelected;
});
},
title: Text(
cardname.title,
style: const TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.w600,
color: Colors.white),
),
//an welcher Stelle die Checkbox ist (links oder rechts)
controlAffinity: ListTileControlAffinity.leading,
secondary: IconButton(
icon: const Icon(
Icons.info_outlined,
size: 40,
color: Colors.orange,
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(cardname),
),
);
},
),
),
);
},
),
);
}
}
I was trying to code a TikTakToe game in FLutter but failed at the point where I tried to make a button which resets the fields.
class TikTakToe extends StatefulWidget {
const TikTakToe({Key? key}) : super(key: key);
#override
State<TikTakToe> createState() => _TikTakToeState();
}
class _TikTakToeState extends State<TikTakToe> {
int currentindex = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.teal[100],
appBar: AppBar(
title: const Text("Tik Tak Toe"),
backgroundColor: Colors.teal[400],
),
...
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
ElevatedButton(
onPressed: () {
setState(() {
gameOver = true;
});
}, child: const Icon(Icons.refresh))
],
),
],
),
),
),
);
}
}
bool gameOver = false;
class Field extends StatefulWidget {
const Field({Key? key, required this.fieldnumber}) : super(key: key);
final int fieldnumber;
#override
State<Field> createState() => _FieldState();
}
class _FieldState extends State<Field> {
String playersign = "";
#override
Widget build(BuildContext context) {
return TextButton(
child: Text(
gameOver ? "" : signlist[widget.fieldnumber],
style: const TextStyle(fontSize: 60),
),
style: TextButton.styleFrom(
shape: const RoundedRectangleBorder(
side: BorderSide(color: Color.fromARGB(255, 88, 221, 208), width: 2),
borderRadius: BorderRadius.all(Radius.zero),
),
enableFeedback: false,
primary: Colors.amber[800],
fixedSize: const Size(120.0, 120.0),
backgroundColor: Colors.white,
),
onPressed: () {
setState(() {
... Winner evaluation
}
}
}
);
},
);
}
}
Now I have the problem that the buttons (Fields) are actually resetting but only after clicking on them and not instant after clicking the reset button In the TikTakToe class.
The only thing that worked was adding
(context as Element).reassamble(); to the onPressed of the Elevated button
setState(() {
gameOver = true;
(context as Element).reassemble();
});
but I got this warning:
The member 'reassemble' can only be used within instance members of subclasses of 'package:flutter/src/widgets/framework.dart'.
Thanks!
Change gameover variable place into _FieldState class.
class Field extends StatefulWidget {
const Field({Key? key, required this.fieldnumber}) : super(key: key);
final int fieldnumber;
#override
State<Field> createState() => _FieldState();
}
class _FieldState extends State<Field> {
String playersign = "";
bool gameover = false;
#override
Widget build(BuildContext context) {
return TextButton(
child: Text(
gameover ? "" : signlist[widget.fieldnumber],
style: const TextStyle(fontSize: 60),
),: const TextStyle(fontSize: 60),
),
You need to put gameOver variable inside of Field widget and dont forget to use camelCase in naming variables, on the other hand you have to pass onPressed to TextButton if you dont want to be clickable just pass onPressed: null
class Field extends StatefulWidget {
const Field({Key key}) : super(key: key);
#override
State<Field> createState() => _FieldState();
}
class _FieldState extends State<Field> {
String playerSign = "";
bool gameOver = false; // put var inside
#override
Widget build(BuildContext context) {
return Column(
children: [
TextButton(
onPressed: null,
child: Text(
gameOver ? "" : signlist[widget.fieldnumber],
style: const TextStyle(fontSize: 60),
),
),
TextButton(
onPressed: () {
setState(() {
setState(() {
gameOver = true;
});
});
},
child: const Text(
"reset",
style: TextStyle(fontSize: 60),
),
),
],
);
}
}
suggest to read this doc
I have setup a Firestore database in which I have a collection 'products'. I use a ListView builder to print them out on ListTiles.
I have also created leading "checkmark" IconButtons that appear for all ListTiles.
My goal is to be able to press whichever of these checkmark buttons and change their color independently. Currently, all of the checkmark buttons change color when you press one of them.
I don't know how to achieve this and would appreciate some help.
class HomeScreenProductList extends StatefulWidget {
const HomeScreenProductList({Key? key}) : super(key: key);
#override
State<HomeScreenProductList> createState() => _HomeScreenProductListState();
}
class _HomeScreenProductListState extends State<HomeScreenProductList> {
final CollectionReference _productsCollection =
FirebaseFirestore.instance.collection('products');
final Stream<QuerySnapshot> _products = FirebaseFirestore.instance
.collection('products')
.orderBy('product-name')
.snapshots();
deleteProduct(id) async {
await _productsCollection.doc(id).delete();
}
Color tileColor = const Color.fromARGB(100, 158, 158, 158);
Color iconColor = Colors.red;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _products,
builder: (
BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot,
) {
if (snapshot.hasError) {
return const Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text('Loading data');
}
final productData = snapshot.requireData;
return ListView.builder(
padding: const EdgeInsets.only(top: 16.0),
itemCount: productData.size,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.check, color: iconColor),
onPressed: () {
setState(() {
if (iconColor != Colors.green) {
iconColor = Colors.green;
} else {
iconColor = Colors.red;
}
});
},
)
],
),
tileColor: tileColor,
shape: const UnderlineInputBorder(
borderSide: BorderSide(
width: 1, color: Color.fromARGB(120, 220, 220, 220))),
title: Text('${productData.docs[index]['product-name']}'),
textColor: const Color.fromARGB(255, 0, 0, 0),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.delete),
color: Colors.red,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
print("Delete Button Pressed");
deleteProduct(snapshot.data?.docs[index].id);
},
)
],
),
),
);
},
);
});
}
}
Full Code:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const ShoppingListApp());
}
class ShoppingListApp extends StatelessWidget {
const ShoppingListApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return HomeScreenAppBar();
}
}
class HomeScreenAppBar extends StatelessWidget {
HomeScreenAppBar({Key? key}) : super(key: key);
final CollectionReference _products =
FirebaseFirestore.instance.collection('products');
final _controller = TextEditingController();
String? _productName;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color.fromARGB(245, 244, 253, 255),
appBar: AppBar(
backgroundColor: Colors.amberAccent,
leading: IconButton(
icon: const Icon(Icons.settings),
iconSize: 20,
color: Colors.black,
splashColor: Colors.white,
onPressed: () {
print("Settings Button Pressed");
},
),
title: TextFormField(
style: const TextStyle(fontSize: 18, fontFamily: 'Raleway'),
controller: _controller,
decoration: const InputDecoration(
enabledBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: Color.fromARGB(245, 244, 253, 255))),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.black)),
hintText: 'Enter product',
hintStyle:
TextStyle(color: Color.fromARGB(200, 255, 255, 255))),
onChanged: (value) {
_productName = value;
},
),
actions: [
IconButton(
icon: const Icon(Icons.add),
iconSize: 24,
color: Colors.black,
splashColor: Colors.white,
onPressed: () {
if (_controller.text == '') {
return;
} else {
_products
.add({'product-name': _productName, 'isbought': false})
.then(
(value) => print('New Product "$_productName" Added'))
.catchError((error) => print(
'Failed To Add Product "$_productName": $error'));
_controller.clear();
}
},
)
],
),
body: const HomeScreenProductList());
}
}
class HomeScreenProductList extends StatefulWidget {
const HomeScreenProductList({Key? key}) : super(key: key);
#override
State<HomeScreenProductList> createState() => _HomeScreenProductListState();
}
class _HomeScreenProductListState extends State<HomeScreenProductList> {
final CollectionReference _productsCollection =
FirebaseFirestore.instance.collection('products');
final Stream<QuerySnapshot> _products = FirebaseFirestore.instance
.collection('products')
.orderBy('product-name')
.snapshots();
deleteProduct(id) async {
await _productsCollection.doc(id).delete();
}
Color tileColor = const Color.fromARGB(100, 158, 158, 158);
Color iconColor = Colors.red;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _products,
builder: (
BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot,
) {
if (snapshot.hasError) {
return const Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text('Loading data');
}
final productData = snapshot.requireData;
return ListView.builder(
padding: const EdgeInsets.only(top: 16.0),
itemCount: productData.size,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.check, color: iconColor),
onPressed: () {
setState(() {
if (iconColor != Colors.green) {
iconColor = Colors.green;
} else {
iconColor = Colors.red;
}
});
},
)
],
),
tileColor: tileColor,
shape: const UnderlineInputBorder(
borderSide: BorderSide(
width: 1, color: Color.fromARGB(120, 220, 220, 220))),
title: Text('${productData.docs[index]['product-name']}'),
textColor: const Color.fromARGB(255, 0, 0, 0),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.delete),
color: Colors.red,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
print("Delete Button Pressed");
deleteProduct(snapshot.data?.docs[index].id);
},
)
],
),
),
);
},
);
});
}
}
You are using single variable iconColor to change all items. That's why all items are getting effected by changing any of it. You can create a List<int> to hold selected index
, List<YourModelClass> or on your model create another bool variable bool icChecked = false.
This approach is holding selected index on state class.
On state class(_HomeScreenProductListState) create
List<int> selectedIndex = []
And on item tap event
icon: Icon(Icons.check, color: selectedIndex.contains(index)?mySelectedColor:unSelectedColor)
onPressed: () {
if (selectedIndex.contains(index)) {
selectedIndex.remove(index);
} else {
selectedIndex.add(index);
}
setState(() {});
},
mySelectedColor and unSelectedColor are two Color objects based on your need.
This is because you are using IconColor as a singular global variable. What you could possible do instead is create a boolean list for every data in the ProductData
List<bool> iconColorList = List<bool>.filled(ProductData.size, false);
and in the listview builder
IconButton(
icon: Icon(Icons.check, color: iconColorList[index] ? Colors.green : Colors.false),
onPressed: () {
setState(() {
if (iconColorList[index]) {
iconColorList[index] = false;
} else {
iconColorList[index] = true;
}
});
},
)
The reason all checkmarks change color is that you are saving the color in iconColor and then assigning that color to all the IconButtons.
This happens in the following lines of code:
IconButton(
icon: Icon(Icons.check, color: iconColor),
onPressed: () {
setState(() {
if (iconColor != Colors.green) {
iconColor = Colors.green;
} else {
iconColor = Colors.red;
}
});
},
)
To achieve the desired behaviour you should save the state of every single checkmark.
Take a look at state management if you haven't already.
All List elements are referencing the same Color variable. So, when that variable changes, all associated elements change accordingly.
I suggest you create separate widget for your itemBuilder. That way you can track each element state internally. Here's the example:
class HomeScreenProductList extends StatefulWidget {
const HomeScreenProductList({Key? key}) : super(key: key);
#override
State<HomeScreenProductList> createState() => _HomeScreenProductListState();
}
class _HomeScreenProductListState extends State<HomeScreenProductList> {
// Sample data structure
final productData = [
{'id': '1', 'product-name': 'Product 1'},
{'id': '2', 'product-name': 'Product 2'},
{'id': '3', 'product-name': 'Product 3'},
{'id': '4', 'product-name': 'Product 4'},
{'id': '5', 'product-name': 'Product 5'},
];
void _deleteProduct(String id) {
productData.removeWhere((el) => el['id'] == id);
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: productData.length,
itemBuilder: (context, index) {
final product = productData[index];
// Separate widget for each List element
return ProductListViewTile(
key: Key('$index'),
product: product,
deleteProduct: _deleteProduct,
);
}),
);
}
}
// Extract List element to a separate widget and track it's state locally
class ProductListViewTile extends StatefulWidget {
const ProductListViewTile({
Key? key,
required this.product,
required this.deleteProduct,
}) : super(key: key);
// Product to display
final Map<String, String> product;
// Callback method
final void Function(String id) deleteProduct;
#override
State<ProductListViewTile> createState() => _ProductListViewTileState();
}
class _ProductListViewTileState extends State<ProductListViewTile> {
Color iconColor = Colors.red;
#override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.check, color: iconColor),
onPressed: () {
setState(() {
if (iconColor != Colors.green) {
iconColor = Colors.green;
} else {
iconColor = Colors.red;
}
});
},
)
],
),
tileColor: const Color.fromARGB(100, 158, 158, 158),
shape: const UnderlineInputBorder(borderSide: BorderSide(width: 1, color: Color.fromARGB(120, 220, 220, 220))),
title: Text(widget.product['product-name']!),
textColor: const Color.fromARGB(255, 0, 0, 0),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.delete),
color: Colors.red,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
print("Delete Button Pressed");
widget.deleteProduct(widget.product['id']!);
},
)
],
),
),
);
}
}
I have this error when I navigate from one page to another page using getx library, in first page I use indexed stack and second page using scaffold, I don't know what to do. Please help me solve this problem, I found many solutions in here but these does not same my problem.
First page:
class HomePage extends StatefulWidget {
HomePage({Key? key}) : super(key: key);
static String route = '/home';
final ScrollController _scrollController = ScrollController();
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool _isVisible = true;
void _hideFloationgButton(UserScrollNotification notification) {
final ScrollDirection direction = notification.direction;
setState(() {
if (direction == ScrollDirection.reverse) {
_isVisible = false;
} else if (direction == ScrollDirection.forward) {
_isVisible = true;
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFEDF0F3),
appBar: HomeAppBar('DAK'),
body: NotificationListener<UserScrollNotification>(
onNotification: (notification) {
_hideFloationgButton(notification);
return true;
},
child: SingleChildScrollView(
controller: widget._scrollController,
child: Column(
children: const [
StoryList(),
SizedBox(
height: 30,
),
SocialList(),
SizedBox(
height: 30,
),
MiddleNavList(),
SizedBox(
height: 30,
),
PostList(),
],
),
),
),
floatingActionButton: Visibility(
visible: _isVisible,
child: FloatingActionButton(
backgroundColor: prototypeColor,
onPressed: () {
Get.toNamed(Routes.POST);
},
child: const Icon(
Icons.add_box,
color: Colors.white,
),
)));
}
}
And the second page:
class PostPage extends StatelessWidget {
const PostPage({Key? key}) : super(key: key);
static String route = '/post';
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
appBar: AppBar(
leading: InkWell(
onTap: () {
Get.back();
},
child: const Icon(
Icons.arrow_back_ios,
color: accentColor,
),
),
title: const Text(
'Create post',
style: TextStyle(color: accentColor),
),
backgroundColor: backgroundColor,
elevation: 0,
),
body: const PostBody(),
);
}
}
for many of you this is an obvious / stupid question, but I've come to a point where I don't have a clue anymore. I have real difficulties understanding State Management with Bloc / Cubit.
Expectation: I have a page with a ListView (recipe_list) of all recipes and an 'add' button. Whenever I click on a ListItem or the 'add' button I go to the next page (recipe_detail). On this page I can create a new recipe (if clicked the 'add' button before), update or delete the existing recipe (if clicked on ListItem before). When I click the 'save' or 'delete' button the Navigator pops and I go back to the previous page (recipe_list). I used Cubit to manage the state of the recipe list. My expectation is that the ListView updates automatically after I clicked 'save' or 'delete'. But I have to refresh the App to display the changes.
main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Recipe Demo',
home: BlocProvider<RecipeCubit>(
create: (context) => RecipeCubit(RecipeRepository())..getAllRecipes(),
child: const RecipeList(),
)
);
}
}
recipe_list.dart
class RecipeList extends StatefulWidget {
const RecipeList({Key? key}) : super(key: key);
#override
_RecipeListState createState() => _RecipeListState();
}
class _RecipeListState extends State<RecipeList> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 24.0
),
color: const Color(0xFFF6F6F6),
child: Stack(
children: [
Column(
children: [
Container(
margin: const EdgeInsets.only(
top: 32.0,
bottom: 32.0
),
child: const Center(
child: Text('Recipes'),
),
),
Expanded(
child: BlocBuilder<RecipeCubit, RecipeState>(
builder: (context, state) {
if (state is RecipeLoading) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (state is RecipeError) {
return const Center(
child: Icon(Icons.close),
);
} else if (state is RecipeLoaded) {
final recipes = state.recipes;
return ListView.builder(
itemCount: recipes.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return BlocProvider<RecipeCubit>(
create: (context) => RecipeCubit(RecipeRepository()),
child: RecipeDetail(recipe: recipes[index]),
);
}
));
},
child: RecipeCardWidget(
title: recipes[index].title,
description: recipes[index].description,
),
);
},
);
} else {
return const Text('Loading recipes error');
}
}
),
),
],
),
Positioned(
bottom: 24.0,
right: 0.0,
child: FloatingActionButton(
heroTag: 'addBtn',
onPressed: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return BlocProvider<RecipeCubit>(
create: (context) => RecipeCubit(RecipeRepository()),
child: const RecipeDetail(recipe: null),
);
}
));
},
child: const Icon(Icons.add_rounded),
backgroundColor: Colors.teal,
),
)
],
),
),
),
);
}
}
recipe_detail.dart
class RecipeDetail extends StatefulWidget {
final Recipe? recipe;
const RecipeDetail({Key? key, required this.recipe}) : super(key: key);
#override
_RecipeDetailState createState() => _RecipeDetailState();
}
class _RecipeDetailState extends State<RecipeDetail> {
final RecipeRepository recipeRepository = RecipeRepository();
final int _recipeId = 0;
late String _recipeTitle = '';
late String _recipeDescription = '';
final recipeTitleController = TextEditingController();
final recipeDescriptionController = TextEditingController();
late FocusNode _titleFocus;
late FocusNode _descriptionFocus;
bool _buttonVisible = false;
#override
void initState() {
if (widget.recipe != null) {
_recipeTitle = widget.recipe!.title;
_recipeDescription = widget.recipe!.description;
_buttonVisible = true;
}
_titleFocus = FocusNode();
_descriptionFocus = FocusNode();
super.initState();
}
#override
void dispose() {
recipeTitleController.dispose();
recipeDescriptionController.dispose();
_titleFocus.dispose();
_descriptionFocus.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 24.0
),
color: const Color(0xFFF6F6F6),
child: Stack(
children: [
Column(
children: [
Align(
alignment: Alignment.topLeft,
child: InkWell(
child: IconButton(
highlightColor: Colors.transparent,
color: Colors.black54,
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back_ios_new_rounded),
),
),
),
TextField(
focusNode: _titleFocus,
controller: recipeTitleController..text = _recipeTitle,
decoration: const InputDecoration(
hintText: 'Enter recipe title',
border: InputBorder.none
),
style: const TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold
),
onSubmitted: (value) => _descriptionFocus.requestFocus(),
),
TextField(
focusNode: _descriptionFocus,
controller: recipeDescriptionController..text = _recipeDescription,
decoration: const InputDecoration(
hintText: 'Enter recipe description',
border: InputBorder.none
),
),
],
),
Positioned(
bottom: 24.0,
left: 0.0,
child: FloatingActionButton(
heroTag: 'saveBtn',
onPressed: () {
if (widget.recipe == null) {
Recipe _newRecipe = Recipe(
_recipeId,
recipeTitleController.text,
recipeDescriptionController.text
);
context.read<RecipeCubit>().createRecipe(_newRecipe);
//recipeRepository.createRecipe(_newRecipe);
Navigator.pop(context);
} else {
Recipe _newRecipe = Recipe(
widget.recipe!.id,
recipeTitleController.text,
recipeDescriptionController.text
);
context.read<RecipeCubit>().updateRecipe(_newRecipe);
//recipeRepository.updateRecipe(_newRecipe);
Navigator.pop(context);
}
},
child: const Icon(Icons.save_outlined),
backgroundColor: Colors.amberAccent,
),
),
Positioned(
bottom: 24.0,
right: 0.0,
child: Visibility(
visible: _buttonVisible,
child: FloatingActionButton(
heroTag: 'deleteBtn',
onPressed: () {
context.read<RecipeCubit>().deleteRecipe(widget.recipe!.id!);
//recipeRepository.deleteRecipe(widget.recipe!.id!);
Navigator.pop(context);
},
child: const Icon(Icons.delete_outline_rounded),
backgroundColor: Colors.redAccent,
),
),
),
],
),
),
),
);
}
}
recipe_state.dart
part of 'recipe_cubit.dart';
abstract class RecipeState extends Equatable {
const RecipeState();
}
class RecipeInitial extends RecipeState {
#override
List<Object> get props => [];
}
class RecipeLoading extends RecipeState {
#override
List<Object> get props => [];
}
class RecipeLoaded extends RecipeState {
final List<Recipe> recipes;
const RecipeLoaded(this.recipes);
#override
List<Object> get props => [recipes];
}
class RecipeError extends RecipeState {
final String message;
const RecipeError(this.message);
#override
List<Object> get props => [message];
}
recipe_cubit.dart
part 'recipe_state.dart';
class RecipeCubit extends Cubit<RecipeState> {
final RecipeRepository recipeRepository;
RecipeCubit(this.recipeRepository) : super(RecipeInitial()) {
getAllRecipes();
}
void getAllRecipes() async {
try {
emit(RecipeLoading());
final recipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(recipes));
} catch (e) {
emit(const RecipeError('Error'));
}
}
void createRecipe(Recipe recipe) async {
await recipeRepository.createRecipe(recipe);
final newRecipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(newRecipes));
}
void updateRecipe(Recipe recipe) async {
await recipeRepository.updateRecipe(recipe);
final newRecipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(newRecipes));
}
void deleteRecipe(int id) async {
await recipeRepository.deleteRecipe(id);
final newRecipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(newRecipes));
}
}
It looks like you're creating another BlocProvider when you're navigating to RecipeDetail page. When you're pushing new MaterialPageRoute, this new page gets additionally wrapped in new RecipeCubit. Then, when you're calling context.read<RecipeCubit>(), you're referencing that provider (as this is closest BlocProvider in the widget tree). Your RecipeList can't react to those changes because it's BlocBuilder is looking for a BlocProvider declared above it in the widget tree (the one in MyApp).
Besides that, newly created provider gets removed from the widget tree anyway when you're closing RecipeDetail page as it is declared in the MaterialPageRoute which has just been pushed off the screen.
Try to remove the additional BlocProvider (the one in RecipeList, in OnTap function of RecipeCardWidget):
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return BlocProvider<RecipeCubit>( // remove this BlocProvider
create: (context) => RecipeCubit(RecipeRepository()),
child: RecipeDetail(recipe: recipes[index]),
);
}
));
},