I can not get it done to access a simple property of a stateless widget from another widget...
Class Month:
class Month {
final String name;
const Month(this.name);
}
MonthCardWidget:
class MonthCardWidget extends StatelessWidget {
final Month month;
const MonthCardWidget({Key key, this.month}) : super(key: key);
...
in my Stateful widget:
final List months = [
MonthCardWidget(month: Month('January')),
MonthCardWidget(month: Month('February')),
MonthCardWidget(month: Month('March')),
MonthCardWidget(month: Month('April')),
MonthCardWidget(month: Month('May')),
MonthCardWidget(month: Month('Juni')),
MonthCardWidget(month: Month('Juli')),
MonthCardWidget(month: Month('August')),
MonthCardWidget(month: Month('September')),
MonthCardWidget(month: Month('October')),
MonthCardWidget(month: Month('November')),
MonthCardWidget(month: Month('December')),
];
and I try to access the name like this:
months[index].month.name
but this is not working... What am I missing here?
Would it not be easier to make a list of Month models instead of widgets?
I think what you want is a state management solution. For this you can use packages like provider or GetX. My personal favorite is the GetX Package. You just need to create a class where you put your lists,variables,... you want to access globally and then call them with a controller.
For a detailed documentation check out their package on pub.dev
https://pub.dev/packages/get
Good Luck! :)
It's probably a bad idea to access the properties of the Widget from its ancestor. The data flows in the other direction. Using State Management is a great way to manage/share your data around your Widget Tree. Check this curated list of State Management Packages. My personal favorite is Riverpod (a new package from the same author as the Provider package).
For your particular case, I'm not even sure you need a Month class and a List<MonthCardWidget> months. Months are just numbers from 1 to 12.
The children of your wheel can be generated as a List:
ClickableListWheelScrollView(
[...]
child: ListWheelScrollView(
[...]
children: List.generate(12, (index) => MonthCardWidget(month: index + 1)),
),
);
And then, your MonthCardWidget just receives an int month and displays the month name thanks to the intl package, giving you i18n for free:
class MonthCardWidget extends StatelessWidget {
final int month;
const MonthCardWidget({Key key, this.month}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.amber.shade300,
child: Center(
child: Text(DateFormat.MMMM().format(DateTime(2000, month))),
),
);
}
}
Full source code
import 'package:clickable_list_wheel_view/clickable_list_wheel_widget.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Calendar Wheel Demo',
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: MyListWheel(),
);
}
}
class MyListWheel extends StatefulWidget {
#override
_MyListWheelState createState() => _MyListWheelState();
}
class _MyListWheelState extends State {
final _scrollController = FixedExtentScrollController(initialItem: 11);
#override
Widget build(BuildContext context) {
final double _itemHeight = MediaQuery.of(context).size.height / 3;
return ClickableListWheelScrollView(
scrollController: _scrollController,
itemHeight: _itemHeight,
itemCount: 12,
scrollOnTap: true,
child: ListWheelScrollView(
controller: _scrollController,
itemExtent: _itemHeight,
physics: FixedExtentScrollPhysics(),
diameterRatio: 3,
squeeze: 0.95,
children:
List.generate(12, (index) => MonthCardWidget(month: index + 1)),
),
);
}
}
class MonthCardWidget extends StatelessWidget {
final int month;
const MonthCardWidget({Key key, this.month}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.amber.shade300,
child: Center(
child: Text(DateFormat.MMMM().format(DateTime(2000, month))),
),
);
}
}
Related
I am confused about Provider. I think Provider is meant to encapsulate the state of a Widget so it can be accessed somewhere else throughout the program. The problem is: What if I want a certain stateless widget multiple times? I created an example for this:
Lets say we want to model a few pieces of paper. Each piece of paper has some unique writing on it. I could now make a provider for a single piece of paper like this:
class PaperSheetProvider extends ChangeNotifier {
String uniqueText = "";
void setUniqueText(String newText) {
uniqueText = newText;
notifyListeners();
}
}
and I make a simple paper widget to consume that provider like:
class PaperPieceWidget extends StatelessWidget {
const PaperPieceWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<PaperSheetProvider>(
builder: ((context, value, child) => Text(value.uniqueText)),
);
}
}
and at last, I make 2 paper widgets along with a button to change the text of the paper:
Column(
children: [
PaperPieceWidget(),
PaperPieceWidget(),
OutlinedButton(
onPressed: () {
Provider.of<PaperSheetProvider>(context, listen: false).setUniqueText('blablaablaa');
},
child: Text("change paper contents"))
],
),
(The ChangeNotifierProvider is near the root of the whole widget tree to simplify the code a bit)
Simple enough. But now If I click the button, I get:
Basically, the two paper pieces have the same writing. Which should not be the case, each piece of paper should have their own, unique writing. How do I do this correctly?
Full code in case anything is unclear:
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String title = 'Shortcuts and Actions Demo';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Column(
children: [
PaperPieceWidget(),
PaperPieceWidget(),
OutlinedButton(
onPressed: () {
Provider.of<PaperSheetProvider>(context, listen: false)
.setUniqueText('blablaablaa');
},
child: Text("change paper contents"))
],
),
);
}
}
void main() {
runApp(MultiProvider(providers: [
ChangeNotifierProvider(create: ((context) => PaperSheetProvider()))
], child: const MyApp()));
}
class PaperPieceWidget extends StatelessWidget {
const PaperPieceWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<PaperSheetProvider>(
builder: ((context, value, child) => Text(value.uniqueText)),
);
}
}
//(provider is posted entirely above)
Wrap each widget with own provider. Something like this:
PaperSheetProvider provider1;
ChangeNotifierProvider.value(
value: provider1,
child: PaperPieceWidget(),
)
Get the working code sample here
I have an RxList of addOnProducts which contains product and selected attributes.
I am trying to implement the simple multiSelectable grid View, but on clicking the checkBox the selected attribute changes but it is not reflected back to the ui,
If i refresh it will be updated.
I tried Obx()=> (); widget , It is still not updating
My ProductController
class ProductsController extends GetxController {
late Worker worker;
static ProductsController instance = Get.find();
RxList<ProductModel> products = RxList<ProductModel>([]);
RxList<CheckProduct> addOnProducts = <CheckProduct>[].obs;
String collection = "products";
#override
void onReady() {
super.onReady();
products.bindStream(getAllProducts());
worker = once(products, (List<ProductModel> value) {
fillAddOnProducts(value);
}, condition: () => products.isNotEmpty);
}
Stream<List<ProductModel>> getAllProducts() => FirebaseFirestore.instance
.collection(collection)
.snapshots()
.map((query) => query.docs
.map((item) => ProductModel.fromMap(item.data(), item.id))
.toList());
void fillAddOnProducts(List<ProductModel> products) => {
products.forEach((element) {
addOnProducts.add(CheckProduct(product: element, selected: false));
})
};
}
class CheckProduct {
ProductModel product;
bool selected;
CheckProduct(
{required ProductModel this.product, required bool this.selected});
}
My Grid View
class AddOns extends StatelessWidget {
const AddOns({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [],
title: Text("Select Addons"),
),
body: Obx(() => GridView.count(
crossAxisCount: 2,
children: productsController.addOnProducts
.map((element) => ProductWidget(product: element))
.toList(),
)));
}
}
class ProductWidget extends StatelessWidget {
final CheckProduct product;
const ProductWidget({Key? key, required this.product}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
margin: EdgeInsets.all(10),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 4,
left: 4,
child: Checkbox(
value: product.selected,
onChanged: (value) {
print("value of the value is : $value");
print("value of product selected before is: " +
product.selected.toString());
product.selected = value!;
print("value of product selected after is: " +
product.selected.toString());
},
),
),
],
));
}
}
Therefore in the console it is :
I/flutter (20067): value of the value is : true
I/flutter (20067): value of product selected before is: false
I/flutter (20067): value of product selected after is: true
But the checkBox is not updating, it updates only when i refresh, How to overCome this? Adding Obx() to the parent isn't helping..
Find the github link to code below here which has just the question and and the problem faced..
After going through your code. I've implemented the following that will change state without hot reload:
In your main dart you do not need to put your product controller here as you are not using it
main.dart
import 'package:flutter/material.dart';
import 'grid.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(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GridSelect(),
);
}
}
Next, I have changed your grid class to generate a list of product widget as the size of the addProduct list length. In my opinion this is a better way to write GridView counts children. Remove obx from your gridview and change your stateful widget to stateless as you are using Getx. It will manage your state even in a stateless widget. Add your product controller here as you will access addProduct list from the controller class.
grid.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:test_project/controllers/productController.dart';
import 'package:test_project/productWidget.dart';
class GridSelect extends StatelessWidget {
final _controller = Get.put(ProductController());
GridSelect({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.count(
crossAxisCount: 2,
children: List.generate(_controller.addOnProducts.length, (index) => ProductWidget(index: index))
),
);
}
}
In your product controller class, remove the instance as it is not important. That is the only change here:
ProductController.dart
import 'package:get/get.dart';
import 'package:test_project/models/productModel.dart';
class ProductController extends GetxController {
RxList<CheckProduct> addOnProducts = <CheckProduct>[].obs;
#override
void onReady() {
super.onReady();
addOnProducts.add(CheckProduct(product: ProductModel('productOne', 20)));
addOnProducts.add(CheckProduct(product: ProductModel('productTwo', 25)));
addOnProducts.add(CheckProduct(product: ProductModel('productThree', 30)));
addOnProducts.add(CheckProduct(product: ProductModel('productFour', 40)));
}
}
class CheckProduct {
ProductModel product;
RxBool selected = false.obs;
CheckProduct({
required this.product,
});
}
Lastly, your productWidget class needs a required value index. So, the widget knows which index in gridview the user is clicking and use Obx() here in checkbox as you have an observable value selected here. Remember to always use Obx() when you have an obs value. This will update the widget whenever an obs value changes. Here, if you notice we are using Get.find() instead of Put as Get.put is already inside the scope so all you need to do is find the controller that you will use. You can find or put multiple controllers and update values as much as you want.
productWidget.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:test_project/controllers/productController.dart';
class ProductWidget extends StatelessWidget {
final ProductController _controller = Get.find();
final int index;
ProductWidget({Key? key, required this.index}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
margin: EdgeInsets.all(20),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 4,
left: 4,
child: Obx(()=>Checkbox(
value: _controller.addOnProducts[index].selected.value,
onChanged: (value) {
print("value of the value is : $value");
print("value of product selected before is: " +
_controller.addOnProducts[index].selected.toString());
_controller.addOnProducts[index].selected.value = value!;
print("value of product selected after is: " +
_controller.addOnProducts[index].selected.toString());
},
)),
)
],
),
);
}
}
Go through GetX documentation for proper use of GetX. Even though I have 2 apps in Playstore with GetX, I still go through documentation from time to time. They have a clear documentation on how to manage state.
In ProductWidget adding an additional Obx() solved my problem
class ProductWidget extends StatelessWidget {
final CheckProduct product;
const ProductWidget({Key? key, required this.product}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
margin: EdgeInsets.all(10),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 4,
left: 4,
// Even the child needs Obx() ; The parent's Obx() is not reflected here
child: Obx(()=>(Checkbox(
value: product.selected,
onChanged: (value) {
print("value of the value is : $value");
print("value of product selected before is: " +
product.selected.toString());
product.selected = value!;
print("value of product selected after is: " +
product.selected.toString());
},
),))
),
],
));
}
I cannot use StreamGroup or StreamZip for some reason
here's the code (that is complete useless, but I don't want to anger the admins)
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp() : super(key: const Key('MyApp'));
#override
Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatefulWidget {
const MyHomePage() : super(key: const Key('MyHomePage'));
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const _duration = Duration(seconds: 3);
static const _initialCx = 3.0;
final _pageStream = Stream<int>.periodic(_duration, (int i) => i);
ScrollController _scrollController;
StreamController<double> _throttle;
#override
void initState() {
_scrollController = ScrollController();
_throttle = StreamController();
StreamGroup<num>.merge([_pageStream, _throttle.stream]); // <= this
StreamZip([_pageStream, _throttle.stream]); // or this
/// i NEED TO IMPLEMENT A LISTENER HERE... THE CODE IS USELESS OTHERWISE
super.initState();
}
#override
void dispose() {
_scrollController?.dispose();
_throttle?.close();
super.dispose();
}
#override
Widget build(BuildContext context) => Scaffold(
body: Stack(children: <Widget>[
ListView.builder(
controller: _scrollController,
itemBuilder: (BuildContext context, int index) => const Center(
child: FlutterLogo(size: 160),
),
),
Align(
alignment: Alignment.bottomCenter,
child: StreamBuilder<double>(
builder: (context, throttle) => Slider(
min: 1,
max: 5,
onChanged: (double value) => _throttle.sink.add(value),
value: throttle.data ?? _initialCx,
),
),
),
]),
);
}
here are the errors respectively for
StreamZip
The method 'StreamZip' isn't defined for the type '_MyHomePageState'.
Try correcting the name to the name of an existing method, or defining
a method named 'StreamZip'.
SteamGroup
The name 'StreamGroup' isn't a class. Try correcting the name to match
an existing class.
I admittedly don't have much experience with those classes, so the solution might be trivial,
can you please explain me how do I access those?
Thank you
It's not obvious due to the way that the Flutter API documentation merges everything together, but StreamZip and StreamGroup come from package:async, not from dart:async. You'll need to replace import 'dart:async'; with import 'package:async/async.dart'; and add an async dependency in your pubspec.yaml file.
(Even though package:async is from the Dart developers, it's separate from dart:async because:
The classes and functions in package:async aren't considered to be core parts of the Dart SDK.
package:async can be implemented in pure Dart code.
A separate package can be updated separately from the Dart SDK, allowing for more frequent updates.)
I am trying to use the BLoC pattern to manage data from an API and show them in my widget. I am able to fetch data from API and process it and show it, but I am using a bottom navigation bar and when I change tab and go to my previous tab, it returns this error:
Unhandled Exception: Bad state: Cannot add new events after calling close.
I know it is because I am closing the stream and then trying to add to it, but I do not know how to fix it because not disposing of the publish subject will result in a memory leak.
I know maybe this question is almost the same as this question.
But I have implemented it and it doesn't work in my case, so I make questions with a different code and hope someone can help me in solving my case. I hope you understand, Thanks.
Here is my BLoC code:
import '../resources/repository.dart';
import 'package:rxdart/rxdart.dart';
import '../models/meals_list.dart';
class MealsBloc {
final _repository = Repository();
final _mealsFetcher = PublishSubject<MealsList>();
Observable<MealsList> get allMeals => _mealsFetcher.stream;
fetchAllMeals(String mealsType) async {
MealsList mealsList = await _repository.fetchAllMeals(mealsType);
_mealsFetcher.sink.add(mealsList);
}
dispose() {
_mealsFetcher.close();
}
}
final bloc = MealsBloc();
Here is my UI code:
import 'package:flutter/material.dart';
import '../models/meals_list.dart';
import '../blocs/meals_list_bloc.dart';
import '../hero/hero_animation.dart';
import 'package:dicoding_submission/src/app.dart';
import 'detail_screen.dart';
class DesertScreen extends StatefulWidget {
#override
DesertState createState() => new DesertState();
}
class DesertState extends State<DesertScreen> {
#override
void initState() {
super.initState();
bloc.fetchAllMeals('Dessert');
}
#override
void dispose() {
bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: getListDesert()
);
}
getListDesert() {
return Container(
color: Color.fromRGBO(58, 66, 86, 1.0),
child: Center(
child: StreamBuilder(
stream: bloc.allMeals,
builder: (context, AsyncSnapshot<MealsList> snapshot) {
if (snapshot.hasData) {
return _showListDessert(snapshot);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white)
));
},
),
),
);
}
Widget _showListDessert(AsyncSnapshot<MealsList> snapshot) => GridView.builder(
itemCount: snapshot == null ? 0 : snapshot.data.meals.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
child: Card(
elevation: 2.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5))),
margin: EdgeInsets.all(10),
child: GridTile(
child: PhotoHero(
tag: snapshot.data.meals[index].strMeal,
onTap: () {
showSnackBar(context, snapshot.data.meals[index].strMeal);
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 777),
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
DetailScreen(
idMeal: snapshot.data.meals[index].idMeal),
));
},
photo: snapshot.data.meals[index].strMealThumb,
),
footer: Container(
color: Colors.white70,
padding: EdgeInsets.all(5.0),
child: Text(
snapshot.data.meals[index].strMeal,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold, color: Colors.deepOrange),
),
),
),
),
);
},
);
}
If you need the full source code, this is the repo with branch submission-3
bloc.dispose(); is the problem.
Since the bloc is initialised outside your UI code, there is no need to dispose them.
Why are you instantiating your bloc on the bloc class?
You must add your bloc instance somewhere in your widget tree, making use of a InheritedWidget with some Provider logic. Then in your widgets down the tree you would take that instance and access its streams. That is why this whole process it is called 'lifting up the state'.
That way, your bloc will always be alive when you need it, and the dispose would still be called sometime.
A bloc provider for example:
import 'package:flutter/material.dart';
abstract class BlocBase {
void dispose();
}
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}) : super(key: key);
final T bloc;
final Widget child;
#override
State<StatefulWidget> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context) {
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider = context
.ancestorInheritedElementForWidgetOfExactType(type)
?.widget;
return provider?.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<T>> {
#override
Widget build(BuildContext context) {
return new _BlocProviderInherited(
child: widget.child,
bloc: widget.bloc
);
}
#override
void dispose() {
widget.bloc?.dispose();
super.dispose();
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
_BlocProviderInherited({
Key key,
#required Widget child,
#required this.bloc
}) : super(key: key, child: child);
final T bloc;
#override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
It makes use of a combination of InheritedWidget (to be available easily down the widget tree) and StatefulWidget (so it can be disposable).
Now you must add the provider of some bloc somewhere into your widget tree, that is up to you, I personally like to add it between the routes of my screens.
In the rout of my MaterialApp widget:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MyApp',
onGenerateRoute: _routes,
);
}
Route _routes(RouteSettings settings) {
if (settings.isInitialRoute)
return MaterialPageRoute(
builder: (context) {
final mealsbloc = MealsBloc();
mealsbloc.fetchAllMeals('Dessert');
final homePage = DesertScreen();
return BlocProvider<DesertScreen>(
bloc: mealsbloc,
child: homePage,
);
}
);
}
}
With the help of routes, the bloc was created 'above' our homePage. Here I can call wherever initialization methods on the bloc I want, like .fetchAllMeals('Dessert'), without the need to use a StatefulWidget and call it on initState.
Now obviously for this to work your blocs must implements the BlocBase class
class MealsBloc implements BlocBase {
final _repository = Repository();
final _mealsFetcher = PublishSubject<MealsList>();
Observable<MealsList> get allMeals => _mealsFetcher.stream;
fetchAllMeals(String mealsType) async {
MealsList mealsList = await _repository.fetchAllMeals(mealsType);
_mealsFetcher.sink.add(mealsList);
}
#override
dispose() {
_mealsFetcher.close();
}
}
Notice the override on dispose(), from now on, your blocs will dispose themselves, just make sure to close everything on this method.
A simple project with this approach here.
To end this, on the build method of your DesertScreen widget, get the available instance of the bloc like this:
var bloc = BlocProvider.of<MealsBloc>(context);
A simple project using this approach here.
For answers that resolve my problem, you can follow the following link: This
I hope you enjoy it!!
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,
),
),
],
),
);
}
}