I am building an app with flutter and the provider pattern. I have particular one ViewModel, that gets provided with Provider.of<AddressBookModel>(context).

class HomeScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return ChangeNotifierProvider<AddressBookViewModel>(
          builder:(_) => AddressBookViewModel(),
          child: Scaffold(
              body: _getBody(context);
    }

    Widget _getBody(BuildContext context) {
        AddressBookViewModel vm = Provider.of<AddressBookViewModel>(context);

        // AddressBookViewModel holds a list of contact objects 
        // (id, name, street, starred etc.)
        List<Contact> contacts = vm.contacts; 
        return ListView.builder(
              itemCount: contacts.length,
              itemBuilder: (context, index) => ListTile(
                    title: Text(contacts[index].name),
                    trailing: contacts[index].starred
                        ? Icon(Icons.star))
                        : null,
                        /**
                         * Changing one object rebuilds and redraws the whole list
                         */
                        onLongPress: () => vm.toggleStarred(contacts[index]);
          ));
    }
}

And the respective ViewModel

class AddressBookViewModel with ChangeNotifier {
    final List<Contact> contacts;

    AddressBookViewModel({this.contacts = []});

    void toggleStarred(Contact contact) {
        int index = contacts.indexOf(contact);
        // the contact object is immutable
        contacts[index] = contact.copy(starred: !contact.starred);
        notifyListeners();
    }
}

The problem I am facing is, once I am changing one contact object in the list with toggleStarred(), the provider is rebuilding and redrawing the whole list. This is not necessary in my opinion, as only the one entry needs to be rebuild. Is there any way to have a provider that is only responsible for one list item? Or any other way to solve this problem?


Solution 1: Mohamed Elrashid

Note : full code available on the end

gistshowng showing the app working with the logs

Step 1 : extend Contact class with ChangeNotifier class

class Contact with ChangeNotifier {  }

Step 2 : remove final form starred field

  bool starred;

Step 3 : move toggleStarred method form AddressBookViewModel class to Contact class

  void toggleStarred() {
    starred = !starred;
    notifyListeners();
  }

Steps[1,2,3] Code Changes Review :

class Contact with ChangeNotifier {
  final String name;
  bool starred;
  Contact(this.name, this.starred);

  void toggleStarred() {
    starred = !starred;
    notifyListeners();
  }
}

Step 4 : move ListTile to sprate StatelessWidget called ContactView

class ContactView extends StatelessWidget {
   Widget build(BuildContext context) {
    return ListTile();
  }
}

Step 5 : Change ListView itemBuilder method

(context, index) {
return ChangeNotifierProvider.value(
  value: contacts[index],
  child: ContactView(),
);

Step 6 : on the new StatelessWidget ContactView get Contact using Provider

final contact = Provider.of<Contact>(context);

Step 7 :change onLongPress to use the new toggleStarred

onLongPress: () => contact.toggleStarred(),

Steps[4,6,7] Code Changes Review :

class ContactView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final contact = Provider.of<Contact>(context);
    print("building ListTile item with contact " + contact.name);
    return ListTile(
      title: Text(contact.name),
      trailing: contact.starred ? Icon(Icons.star) : null,
      onLongPress: () => contact.toggleStarred(),
    );
  }
}

Steps[5] Code Changes Review :

return ListView.builder(
  itemCount: contacts.length,
  itemBuilder: (context, index) {
    print("building ListView item with index $index");
    return ChangeNotifierProvider.value(
      value: contacts[index],
      child: ContactView(),
    );
  },
);

Full Code

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider<AddressBookViewModel>(
      builder: (context) => AddressBookViewModel(),
      child: HomeScreen(),
    ),
  );
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<AddressBookViewModel>(
      builder: (context) => AddressBookViewModel(),
      child: MaterialApp(
        home: Scaffold(
          body: _getBody(context),
        ),
      ),
    );
  }

  Widget _getBody(BuildContext context) {
    AddressBookViewModel vm = Provider.of<AddressBookViewModel>(context);

    final contacts = vm.contacts;
    return ListView.builder(
      itemCount: contacts.length,
      itemBuilder: (context, index) {
        print("building ListView item with index $index");
        return ChangeNotifierProvider.value(
          value: contacts[index],
          child: ContactView(),
        );
      },
    );
  }
}

// product_item.dart
class ContactView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final contact = Provider.of<Contact>(context);
    print("building ListTile item with contact " + contact.name);
    return ListTile(
      title: Text(contact.name),
      trailing: contact.starred ? Icon(Icons.star) : null,
      onLongPress: () => contact.toggleStarred(),
    );
  }
}

class AddressBookViewModel with ChangeNotifier {
  final contacts = [
    Contact("Contact A", false),
    Contact("Contact B", false),
    Contact("Contact C", false),
    Contact("Contact D", false),
  ];
  void addcontacts(Contact contact) {
    contacts.add(contact);
    notifyListeners();
  }
}

class Contact with ChangeNotifier {
  final String name;
  bool starred;
  Contact(this.name, this.starred);

  void toggleStarred() {
    starred = !starred;
    notifyListeners();
  }
}

Ref :


Solution 2: Rémi Rousselet

When working with lists, you'll want to have a "provider" for each item of your list and extract the list item into a constant – especially if the data associated to your item is immutable.

Instead of:

final contactController = Provider.of<ContactController>(context);
return ListView.builder(
  itemCount: contactController.contacts.length,
  builder: (_, index) {
    reurn Text(contactController.contacts[index].name);
  }
)

Prefer:

final contactController = Provider.of<ContactController>(context);
return ListView.builder(
  itemCount: contactController.contacts.length,
  builder: (_, index) {
    reurn Provider.value(
      value: contactController.contacts[index],
      child: const ContactItem(),
    );
  }
)

Where ContactItem is a StatelessWidget that typically look like so:

class ContactItem extends StatelessWidget {
  const ContactItem({Key key}): super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(Provider.of<Contact>(context).name);
  }
}