The state here is maintained in a list of instances of Products called _shoppingCart
(The following code is an example from https://docs.flutter.dev/development/ui/widgets-intro#keys). The state is being mapped to widgets and every time a change is made to the list of products, all the widgets part of the list, regardless of being changed, still rebuild. Is this how it is supposed to be? or is there a better way?
import 'package:flutter/material.dart';
class Product {
const Product({required this.name});
final String name;
}
typedef CartChangedCallback = Function(Product product, bool inCart);
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({
required this.product,
required this.inCart,
required this.onCartChanged,
}) : super(key: ObjectKey(product));
final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;
Color _getColor(BuildContext context) {
// The theme depends on the BuildContext because different
// parts of the tree can have different themes.
// The BuildContext indicates where the build is
// taking place and therefore which theme to use.
return inCart //
? Colors.black54
: Theme.of(context).primaryColor;
}
TextStyle? _getTextStyle(BuildContext context) {
if (!inCart) return null;
return const TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}
#override
Widget build(BuildContext context) {
print("rebuilding ${product.name}");
return ListTile(
onTap: () {
onCartChanged(product, inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
title: Text(
product.name,
style: _getTextStyle(context),
),
);
}
}
class ShoppingList extends StatefulWidget {
const ShoppingList({required this.products, super.key});
final List<Product> products;
// The framework calls createState the first time
// a widget appears at a given location in the tree.
// If the parent rebuilds and uses the same type of
// widget (with the same key), the framework re-uses
// the State object instead of creating a new State object.
#override
State<ShoppingList> createState() => _ShoppingListState();
}
class _ShoppingListState extends State<ShoppingList> {
final _shoppingCart = <Product>{};
void _handleCartChanged(Product product, bool inCart) {
setState(() {
// When a user changes what's in the cart, you need
// to change _shoppingCart inside a setState call to
// trigger a rebuild.
// The framework then calls build, below,
// which updates the visual appearance of the app.
if (!inCart) {
_shoppingCart.add(product);
} else {
_shoppingCart.remove(product);
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Shopping List'),
),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 8.0),
children: widget.products.map((product) {
return ShoppingListItem(
//key: ObjectKey(product),
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}
void main() {
runApp(const MaterialApp(
title: 'Shopping App',
home: ShoppingList(
products: [
Product(name: 'Eggs'),
Product(name: 'Flour'),
Product(name: 'Chocolate chips'),
],
),
));
}
I think you don't need to worry about this, keep using const or value keys where you think that these widgets will not be changed,
Otherwise let Flutter framework handle this,
Flutter framework is smart enough, during setState it will only update the element tree, will not create/paint it from start,
So only updated elements will be repainted rest will be there.
Related
I'm totally new to Flutter/Dart, I've done all the layouts for my application, and now it's time to make my application's API calls. I'm trying to manage the forms as cleanly as possible.
I created a class that manages TextFields data (values and errors), if my API returns an error I would like the screen to update without having to call setState(() {}), is this possible?
In addition, many of my application's screens use values that the user enters in real time, if that happened I would have to call the setState(() {}) methodmany times.
Any idea how to do this with the excess calls to the setState(() {}) method?
I created a test project for demo, these are my files:
File path: /main.dart
import 'package:flutter/material.dart';
import 'login_form_data.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final LoginFormData _loginFormData = LoginFormData();
void _submitLoginForm() {
// Validate and then make a call to the login api
// If the api returns any erros inject then in the LoginFormData class
_loginFormData.setError('email', 'Invalid e-mail');
setState(() {}); // Don't want to call setState
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test App'),
),
body: Padding(
padding: const EdgeInsets.all(30),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
decoration: InputDecoration(
errorText: _loginFormData.firstError('email'),
labelText: 'E-mail',
),
onChanged: (value) => _loginFormData.setValue('email', value),
),
TextField(
decoration: InputDecoration(
errorText: _loginFormData.firstError('password'),
labelText: 'Password',
),
obscureText: true,
onChanged: (value) =>
_loginFormData.setValue('password', value),
),
ElevatedButton(
onPressed: _submitLoginForm,
child: const Text('Login'),
)
],
),
),
),
);
}
}
File path: /login_form_data.dart
import 'form/form_data.dart';
import 'form/form_field.dart';
class LoginFormData extends FormData {
#override
Map<String, FormField> fields = {
'email': FormField(),
'password': FormField(),
'simple_account': FormField(
value: true,
),
};
LoginFormData();
}
File path: /form/form_data.dart
class FormData {
final Map<String, dynamic> fields = {};
dynamic getValue(
String key, {
String? defaultValue,
}) {
return fields[key]?.value ?? defaultValue;
}
void setValue(
String key,
String value,
) {
fields[key].value = value;
}
void setError(
String key,
String error,
) {
fields[key]?.errors.add(error);
}
dynamic firstError(
String key,
) {
return fields[key]?.errors.length > 0 ? fields[key]?.errors[0] : null;
}
FormData();
}
File path: /form/form_field.dart
class FormField {
dynamic value;
List errors = [];
FormField({
this.value,
});
}
You are essentially looking for a State Management solution.
There are multiple solutions (you can read about them here: https://docs.flutter.dev/development/data-and-backend/state-mgmt/options)
State Management allows you to declare when you want your widgets to change state instead of having to imperatively call a setState method.
Flutter recommends Provider as a beginner solution, and you can find many tutorials online.
With that being said, let me show you how to achieve this result with a very basic solution: Change Notifier
Quoting flutter documentation :
” A class that can be extended or mixed in that provides a change
notification API using VoidCallback for notifications.”
We are going to make FormData a Change notifier, and them we are going to make your app listen to changes on the instance, and rebuild itself based on them.
Step 1:
Based on the code you posted, I can tell that you will interact with LoginFormData based on the methods setValue and setError from the parent class FormData. So we are going to make FormData inherit ChangeNotifer, and make a call to notifyListeners() on these two methods.
class FormData extends ChangeNotifier {
final Map<String, dynamic> fields = {};
dynamic getValue(
String key, {
String? defaultValue,
}) {
return fields[key]?.value ?? defaultValue;
}
void setValue(
String key,
String value,
) {
fields[key].value = value;
notifyListeners();
}
void setError(
String key,
String error,
) {
fields[key]?.errors.add(error);
notifyListeners();
}
dynamic firstError(
String key,
) {
return fields[key]?.errors.length > 0 ? fields[key]?.errors[0] : null;
}
FormData();
}
Now, every time you call either setValue or setError, the instance of FormData will notify the listeners.
Step2:
Now we have to setup a widget in your app to listen to these changes. Since your app is still small, it’s easy to find a place to put this listener. But as your app grows, you will see that it gets harder to do this, and that’s where packages like Provider come in handy.
We are going to wrap your Padding widget that is the first on the body of your scaffold, with a AnimatedBuilder. Despite of the misleading name, animated builder is not limited to animations. It is a widget that receives any listenable object as a parameter, and rebuilds itself every time it gets notified, passing down the updated version of the listenable.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final LoginFormData _loginFormData = LoginFormData();
void _submitLoginForm() {
// Validate and then make a call to the login api
// If the api returns any erros inject then in the LoginFormData class
_loginFormData.setError('email', 'Invalid e-mail');
//setState(() {}); No longer necessary
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test App'),
),
body: AnimatedBuilder(
animation: _loginFormData,
builder: (context, child) {
return Padding(
padding: const EdgeInsets.all(30),
child: Center(
child: Column(
//... The rest of your widgets
),
),
);
}
),
);
}
}
My question concerns Riverpod.
I have a widget with 2 tabs. When i change the tab, i want to update a property (infoShare) with a "address" value if i choose the 1st tab and with a "publicKey" value if i choose the 2nd tab.
I used notifier to do that. No issue with that.
But when i instanciate the 1st time the main widget, my provider is not initialize.
So i need to fix it to create a specific provider to initialize it and use a ProviderScope.
Is it the good way to initialize my widget ?
Here's my code. It works but i don't know if it's the good solution.
And perhaps Riverpod's annotations could be helpfull
final _contactDetailInfoShareProviderArgs = Provider<Contact>(
(ref) {
throw UnimplementedError();
},
);
final _contactDetailInfoShareProvider = NotifierProvider.autoDispose
.family<ContactDetailInfoShareNotifier, String, Contact>(
() {
return ContactDetailInfoShareNotifier();
},
);
class ContactDetailInfoShareNotifier
extends AutoDisposeFamilyNotifier<String, Contact> {
ContactDetailInfoShareNotifier();
#override
String build(Contact arg) {
return arg.address.toUpperCase();
}
void setInfoShare(int tab, Contact contact) {
if (tab == 1) {
state = contact.publicKey.toUpperCase();
} else {
state = contact.address.toUpperCase();
}
}
}
abstract class ContactDetailProvider {
static final contactDetailInfoShare = _contactDetailInfoShareProvider;
static final contactDetailInfoShareProviderArgs =
_contactDetailInfoShareProviderArgs;
}
class ContactDetail extends ConsumerWidget {
const ContactDetail({
required this.contact,
super.key,
});
final Contact contact;
#override
Widget build(BuildContext context, WidgetRef ref) {
return ProviderScope(
overrides: [
ContactDetailProvider.contactDetailInfoShareProviderArgs
.overrideWithValue(
contact,
),
],
child: ContactDetailBody(
contact: contact,
),
);
}
}
class ContactDetailBody extends ConsumerWidget {
const ContactDetailBody({
required this.contact,
super.key,
});
final Contact contact;
#override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
children: <Widget>[
Expanded(
child: Container(
child: ContainedTabBarView(
tabs: [
Text('tab1'),
Text('tab2'),
],
views: [
ContactDetailTab1(),
ContactDetailTab2(),
],
onChange: (p0) {
ref
.watch(
ContactDetailProvider.contactDetailInfoShare(contact)
.notifier,
)
.setInfoShare(p0, contact);
},
),
),
),
AppButton(
'Share',
onPressed: () {
print(
ref.watch(
ContactDetailProvider.contactDetailInfoShare(
ref.read(
ContactDetailProvider.contactDetailInfoShareProviderArgs,
),
),
),
);
},
)
],
);
}
}
ProviderScope should not be used other than the root of your app, or just for testing. It is not designed to be used in your widgets. There are many other options to init your providers, consider using .family or using some StateNotifierProvider with three states loading, error, value, and you can have the initial value to be loading and after initialization you can have the value state. Or consider using new Riverpod code generation to have some more complex logic to init your provider.
I want to implement rows after named section, something like in Android phone contacts:
I mean a section name (for example * Favorites) and one or more rows after this name (TechBone net). Could anyone say what widget(s) should be used in Flutter for such task?
You can use MixedList by customising this example (provided by Flutter Official Documentation)
import 'package:flutter/material.dart';
void main() {
runApp(
MyApp(
items: List<ListItem>.generate(
1000,
(i) => i % 6 == 0
? HeadingItem('Alphabet $i')
: MessageItem('Contact $i', 'Number $i'),
),
),
);
}
class MyApp extends StatelessWidget {
final List<ListItem> items;
const MyApp({super.key, required this.items});
#override
Widget build(BuildContext context) {
const title = 'Mixed List';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: const Text(title),
),
body: ListView.builder(
// Let the ListView know how many items it needs to build.
itemCount: items.length,
// Provide a builder function. This is where the magic happens.
// Convert each item into a widget based on the type of item it is.
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: item.buildTitle(context),
subtitle: item.buildSubtitle(context),
);
},
),
),
);
}
}
/// The base class for the different types of items the list can contain.
abstract class ListItem {
/// The title line to show in a list item.
Widget buildTitle(BuildContext context);
/// The subtitle line, if any, to show in a list item.
Widget buildSubtitle(BuildContext context);
}
/// A ListItem that contains data to display a heading.
class HeadingItem implements ListItem {
final String heading;
HeadingItem(this.heading);
#override
Widget buildTitle(BuildContext context) {
return Text(
heading,
style: Theme.of(context).textTheme.headline6,
);
}
#override
Widget buildSubtitle(BuildContext context) => const SizedBox.shrink();
}
/// A ListItem that contains data to display a message.
class MessageItem implements ListItem {
final String sender;
final String body;
MessageItem(this.sender, this.body);
#override
Widget buildTitle(BuildContext context) => Text(sender);
#override
Widget buildSubtitle(BuildContext context) => Text(body);
}
Do check it out, it might help you.
In sample we pass the single model like this
return ScopedModel<CounterModel>(
model: CounterModel(), // only one model here
child: MaterialApp(
title: 'Scoped Model Demo',
home: CounterHome('Scoped Model Demo'),
),
);
How to pass some models instead of single model? So that to use them later this way
Widget build(BuildContext context) {
final username =
ScopedModel.of<UserModel>(context, rebuildOnChange: true).username;
final counter =
ScopedModel.of<CounterModel>(context, rebuildOnChange: true).counter;
return Text(...);
}
You can copy paste and run the full code below.
You can reference this https://newcodingera.com/scoped-model-in-flutter/
You can pass your three models and wrap them nested
Code snippet
void main() {
runApp(MyApp(
counterModel: CounterModel(),
userModel: UserModel('Brian'),
dataModel: DataModel('this is test'),
));
}
class MyApp extends StatelessWidget {
final CounterModel counterModel;
final UserModel userModel;
final DataModel dataModel;
const MyApp({
Key key,
#required this.counterModel,
#required this.userModel,
#required this.dataModel,
}) : super(key: key);
#override
Widget build(BuildContext context) {
// At the top level of our app, we'll, create a ScopedModel Widget. This
// will provide the CounterModel to all children in the app that request it
// using a ScopedModelDescendant.
return ScopedModel<DataModel>(
model: dataModel,
child: ScopedModel<UserModel>(
model: userModel,
child: ScopedModel<CounterModel>(
model: counterModel,
Working demo
Full code
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
void main() {
runApp(MyApp(
counterModel: CounterModel(),
userModel: UserModel('Brian'),
dataModel: DataModel('this is test'),
));
}
class MyApp extends StatelessWidget {
final CounterModel counterModel;
final UserModel userModel;
final DataModel dataModel;
const MyApp({
Key key,
#required this.counterModel,
#required this.userModel,
#required this.dataModel,
}) : super(key: key);
#override
Widget build(BuildContext context) {
// At the top level of our app, we'll, create a ScopedModel Widget. This
// will provide the CounterModel to all children in the app that request it
// using a ScopedModelDescendant.
return ScopedModel<DataModel>(
model: dataModel,
child: ScopedModel<UserModel>(
model: userModel,
child: ScopedModel<CounterModel>(
model: counterModel,
child: MaterialApp(
title: 'Scoped Model Demo',
home: CounterHome('Scoped Model Demo'),
),
),
),
);
}
}
// Start by creating a class that has a counter and a method to increment it.
//
// Note: It must extend from Model.
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
// First, increment the counter
_counter++;
// Then notify all the listeners.
notifyListeners();
}
}
class UserModel extends Model {
String _username;
UserModel(String username) : _username = username;
String get username => _username;
set username(String newName) {
_username = newName;
notifyListeners();
}
}
class DataModel extends Model {
String _data;
DataModel(String data) : _data = data;
String get data => _data;
set data(String newData) {
_data = newData;
notifyListeners();
}
}
class CounterHome extends StatelessWidget {
final String title;
CounterHome(this.title);
#override
Widget build(BuildContext context) {
final counter =
ScopedModel.of<CounterModel>(context, rebuildOnChange: true).counter;
final userModel = ScopedModel.of<UserModel>(context, rebuildOnChange: true);
final dataModel = ScopedModel.of<DataModel>(context, rebuildOnChange: true);
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('${userModel.username} pushed the button this many times:'),
Text('${dataModel.data} From data model'),
// Create a ScopedModelDescendant. This widget will get the
// CounterModel from the nearest parent ScopedModel<CounterModel>.
// It will hand that CounterModel to our builder method, and
// rebuild any time the CounterModel changes (i.e. after we
// `notifyListeners` in the Model).
Text('$counter', style: Theme.of(context).textTheme.display1),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: Text('Change Username'),
onPressed: () {
userModel.username = 'Suzanne';
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
child: Text('Change Data'),
onPressed: () {
dataModel.data = 'data changed';
},
),
)
],
),
),
// Use the ScopedModelDescendant again in order to use the increment
// method from the CounterModel
floatingActionButton: ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return FloatingActionButton(
onPressed: model.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
);
},
),
);
}
}
I'm codeing an app with flutter an i'm haveing problems with the development. I'm trying to have a listview with a custom widget that it has a favourite icon that represents that you have liked it product. I pass a boolean on the constructor to set a variables that controls if the icons is full or empty. When i click on it i change it state. It works awesome but when i scroll down and up again it loses the lastest state and returns to the initial state.
Do you know how to keep it states after scrolling?
Ty a lot <3
Here is my code:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index){
return new LikeClass(liked: false);
},
),
);
}
}
class LikeClass extends StatefulWidget {
final bool liked;//i want this variable controls how heart looks like
LikeClass({this.liked});
#override
_LikeClassState createState() => new _LikeClassState();
}
class _LikeClassState extends State<LikeClass> {
bool liked;
#override
void initState() {
liked=widget.liked;
}
#override
Widget build(BuildContext context) {
return new Container(
child: new Column(
children: <Widget>[
new GestureDetector(
onTap:((){
setState(() {
liked=!liked;
//widget.liked=!widget.liked;
});
}),
child: new Icon(Icons.favorite, size: 24.0,
color: liked?Colors.red:Colors.grey,
//color: widget.liked?Colors.red:Colors.grey,//final method to control the appearance
),
),
],
),
);
}
}
You have to store the state (favorite or not) in a parent widget. The ListView.builder widget creates and destroys items on demand, and the state is discarded when the item is destroyed. That means the list items should always be stateless widgets.
Here is an example with interactivity:
class Item {
Item({this.name, this.isFavorite});
String name;
bool isFavorite;
}
class MyList extends StatefulWidget {
#override
State<StatefulWidget> createState() => MyListState();
}
class MyListState extends State<MyList> {
List<Item> items;
#override
void initState() {
super.initState();
// Generate example items
items = List<Item>();
for (int i = 0; i < 100; i++) {
items.add(Item(
name: 'Item $i',
isFavorite: false,
));
}
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListItem(
items[index],
() => onFavoritePressed(index),
);
},
);
}
onFavoritePressed(int index) {
final item = items[index];
setState(() {
item.isFavorite = !item.isFavorite;
});
}
}
class ListItem extends StatelessWidget {
ListItem(this.item, this.onFavoritePressed);
final Item item;
final VoidCallback onFavoritePressed;
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(item.name),
leading: IconButton(
icon: Icon(item.isFavorite ? Icons.favorite : Icons.favorite_border),
onPressed: onFavoritePressed,
),
);
}
}
If you don't have many items in the ListView you can replace it with a SingleChildScrollview and a Column so that the Widgets aren't recycled. But it sounds like you should have a list of items where each item has an isFavourite property, and control the icon based on that property. Don't forget to setState when toggling the favorite.
Other answer are better for your case but this an alternative and can be used if you want to only keep several elements alive during a scroll. In this case you can use AutomaticKeepAliveClientMixin with keepAlive.
class Foo extends StatefulWidget {
#override
FooState createState() {
return new FooState();
}
}
class FooState extends State<Foo> with AutomaticKeepAliveClientMixin {
bool shouldBeKeptAlive = false;
#override
Widget build(BuildContext context) {
super.build(context);
shouldBeKeptAlive = someCondition();
return Container(
);
}
#override
bool get wantKeepAlive => shouldBeKeptAlive;
}
ListView.builder & GridView.builder makes items on demand. That means ,they construct item widgets & destroy them when they going beyond more than cacheExtent.
So you cannot keep any ephemeral state inside that item widgets.(So most of time item widgets are Stateless, but when you need to use keepAlive you use Stateful item widgets.
In this case you have to keep your state in a parent widget.So i think the best option you can use is State management approach for this. (like provider package, or scoped model).
Below link has similar Example i see in flutter.dev
Link for Example
Hope this answer will help for you
A problem with what you are doing is that when you change the liked variable, it exists in the Widget state and nowhere else. ListView items share Widgets so that only a little more than are visible at one time are created no matter how many actual items are in the data.
For a solution, keep a list of items as part of your home page's state that you can populate and refresh with real data. Then each of your LikedClass instances holds a reference to one of the actual list items and manipulates its data. Doing it this way only redraws only the LikedClass when it is tapped instead of the whole ListView.
class MyData {
bool liked = false;
}
class _MyHomePageState extends State<MyHomePage> {
List<MyData> list;
_MyHomePageState() {
// TODO use real data.
list = List<MyData>();
for (var i = 0; i < 100; i++) list.add(MyData());
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView.builder(
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return new LikeClass(list[index]);
},
),
);
}
}
class LikeClass extends StatefulWidget {
final MyData data;
LikeClass(this.data);
#override
_LikeClassState createState() => new _LikeClassState();
}
class _LikeClassState extends State<LikeClass> {
#override
Widget build(BuildContext context) {
return new Container(
child: new Column(
children: <Widget>[
new GestureDetector(
onTap: (() {
setState(() {
widget.data.liked = !widget.data.liked;
});
}),
child: new Icon(
Icons.favorite,
size: 24.0,
color: widget.data.liked ? Colors.red : Colors.grey,
),
),
],
),
);
}
}