Hi!
TL;DR
If a ChangeNotifierProvider with nested ListenableProvider(s) is disposed, will it also dispose the ListenableProvider(s)?
I recently found a cool way to use Providers by Acedemind I'd never seen before. Here is an example where he has a Products notifier that handles a list of products and a Product notifier that itself is a single instance of a product:
// products.dart
import './product.dart';
class Products with ChangeNotifier {
List<Product> _items = [
Product(
id: 'p1',
title: 'Red Shirt',
description: 'A red shirt - it is pretty red!',
price: 29.99,
imageUrl:
'https://cdn.pixabay.com/photo/2016/10/02/22/17/red-t-shirt-1710578_1280.jpg',
),
// ...
];
List<Product> get items => [..._items];
Product findById(String id) => _items.firstWhere((prod) => prod.id == id);
void addProduct(Product product) {
_items.add(product);
notifyListeners();
}
}
class Product with ChangeNotifier {
final String id;
final String title;
final String description;
final double price;
final String imageUrl;
bool isFavorite;
Product({
@required this.id,
@required this.title,
@required this.description,
@required this.price,
@required this.imageUrl,
this.isFavorite = false,
});
void toggleFavoriteStatus() {
isFavorite = !isFavorite;
notifyListeners();
}
}
And then to show the items, you'd say something like this:
// products_list.dart
import '../providers/products.dart';
import './product_item.dart';
class ProductsList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final productsData = Provider.of<Products>(context);
final products = productsData.items;
return ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) {
return ChangeNotifierProvider(
builder: (c) => products[i],
child: ProductItem(),
);
},
);
}
}
// product_item.dart
class ProductItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final product = Provider.of<Product>(context);
return ListTile(
title: product.description,
onTap: () {
product.toggleFavoriteStatus();
},
trailing: Icon(
Icons.favorite,
color: product.isFavorite ? Colors.red : Colors.grey,
),
);
}
}
And so if you tap on the tile and hit product.toggleFavoriteStatus() it won't actually notify everything that's this one notifier has changed, just that one tile and the heart will change color.
I really like this idea of nested providers but there was an issue when applying it to a ListView.builder when the list scrolls. If your list is long enough the ChangeNotifierProvider on the product will dispose of an item that is no longer visible (when probably the ListState is still referencing it) and you'll get:
I/flutter (27720): โโโก EXCEPTION CAUGHT BY WIDGETS LIBRARY โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
I/flutter (27720): The following assertion was thrown building NotificationListener<KeepAliveNotification>:
I/flutter (27720): A Product was used after being disposed.
I/flutter (27720): Once you have called dispose() on a Product, it can no longer be used.
Changing the ChangeNotifierProvider to a ListenableProvider will fix that issue, but I'm worried about memory leaks now as nothing is explicitly disposing of the product.
That exception is happening because you're misusing ChangeNotifierProvider.
You don't want to use the builder constructor when you already have an instance of ChangeNotifier. The default constructor will dispose the ChangeNotifier when ChangeNotifierProvider() is unmounted.
Instead, you probably want ChangeNotifierProvider.value:
ChangeNotifierProvider.value(
value: products[i],
child: ProductItem(),
);
Thank you! Works just fine now
@rrousselGit thanks a lot that actually helped me as well fixing my crashes.
Also in the mentioned course the recommended approach is to use the .value() constructor.
From the final code of the related course section:
itemBuilder: (ctx, i) => ChangeNotifierProvider.value(
value: products[i],
child: ProductItem(),
),
But it's an important info of course, that and why the other approach can't work.
Thank you now works fine foe me.
I scanned through nearly 100 old issues looking for the purpose of the .value constructors. Now I think I understand: when you need to manage disposal yourself.
I scanned through nearly 100 old issues looking for the purpose of the
.valueconstructors. Now I think I understand: when you need to manage disposal yourself.
That's only a side effect
The main purpose is: when the provider is not the owner of the value to provides.
And since the provider is not the object owner, it doesn't make sense for the provider to dispose of the value. Hence why you need to dispose it yourself in such situation
The main purpose is: when the provider is not the owner of the value to provides.
Yes, I would have understood an "ownership" explanation of the difference. Ownership to me means controlling the lifespan of an object.
I'm mentoring someone who is learning Java (and some software design) as she creates unit tests for a project of mine. She does not yet understand what "ownership" means, so I suspect this explanation mainly speaks to experienced devs.
And since the provider is not the object owner, it doesn't make sense for the provider to dispose of the value. Hence why you need to dispose it yourself in such situation
Yes, it's a consequence of ownership. But in the context of provider, this seems to be the only consequence that matters: who manages the lifetime of the object.
May I ask how to properly dispose a ChangeNotifier, especially in the ListView.builder as a child? Since It is not a good idea to dispose inside the widget which is not the owner of the object.
Most helpful comment
Also in the mentioned course the recommended approach is to use the
.value()constructor.From the final code of the related course section:
But it's an important info of course, that and why the other approach can't work.