rrousselGit / provider

InheritedWidgets, but simple
https://pub.dev/packages/provider
MIT License
5.12k stars 512 forks source link

[Question] Nested Providers and Lists #151

Closed stargazing-dino closed 5 years ago

stargazing-dino commented 5 years ago

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.

rrousselGit commented 5 years ago

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(),
);
stargazing-dino commented 5 years ago

Thank you! Works just fine now

ChristBKK commented 5 years ago

@rrousselGit thanks a lot that actually helped me as well fixing my crashes.

jostschmithals commented 5 years ago

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.

marizar commented 5 years ago

Thank you now works fine foe me.

jtlapp commented 5 years ago

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.

rrousselGit commented 5 years ago

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.

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

jtlapp commented 5 years ago

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.

firatcetiner commented 4 years ago

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.