0

I'm learning Flutter and there is something I cannot grasp my head around. I implemented a Infinite scroll pagination, with a package (infine_scroll_pagination), it works fine, but the data this Package is getting, comes from a Future call, which takes data from the WEB, and parses it in my Provider Class.

My issue is, the data that is loaded by the Infinite Scroll widget, cannot be accessed, in its state, anywhere else.

Example: Let's take a contact list, that loads 10 contacts at a time:

class ContactsBody extends StatefulWidget {
  @override
  _ContactsBodyState createState() => _ContactsBodyState();
}

class _ContactsBodyState extends State<ContactsBody> {
  static const _pageSize = 10;
  final PagingController<int, Contact> pagingController =
      PagingController(firstPageKey: 0);

  @override
  void initState() {
    super.initState();
    pagingController.addPageRequestListener((pageKey) {
      _fetchPage(pageKey);
    });
  }

  Future<void> _fetchPage(int pageKey) async {
    try {
      final newItems = await ContactsService().fetchContactsPaged(pageKey, _pageSize);
      final isLastPage = newItems.length < _pageSize;

      if (isLastPage) {
        pagingController.appendLastPage(newItems.contacts);
      } else {
        final nextPageKey = pageKey + 1;
        pagingController.appendPage(newItems.contacts, nextPageKey);
      }
    } catch (error) {
      pagingController.error = error;
    }
  }
  @override
  Widget build(BuildContext context) {
    return ContactsList(pagingController);
  }

  @override
  void dispose() {
    pagingController.dispose();
    super.dispose();
  }

So basically this Infinite Scroll package, will fetch my contacts, 10 at a time, and here my ContactsService call:

 Future<Contacts> fetchContactsPaged(int pageKey, int pageSize) async {
    final response = await http.get(.....);

    if (response.statusCode == 200) {
    return Contacts.fromJson(jsonDecode(response.body));
    } else {
      throw Exception('Failed to load contacts');
    }
  }

And finally, as you can see here above, it initializes my Provider class (Contacts), using its factory method, "fromJson()", and returns the parsed data.

Now my Provider class:

class Contacts extends ChangeNotifier {
      List<Contact> _contacts = <Contact>[];
    
      Contacts();
    
      factory Contacts.fromJson(final Map<String, dynamic> json) {
        final Contacts contacts = Contacts();
        if (json['data'] != null) {
          json['data'].forEach((contact) {
            contacts.add(Contact.fromJson(contact));
          });
        }
        return contacts;
      }

  void add(final Contact contact) {
    this._contacts.add(contact);
    this.notifyListeners();
  }

The problem I'm having here is, when the Inifinite Scroll listView is loaded, and for example I change the state of a single contact (contacts can be set as favorite for example),

How can I access the SAME instance of the Contacts() class, that the FUTURE call initialized, so that I can access the current state of the data in that class?

Of course if I were to POST my changes onto the API, and refetch the new values where I need them, I would get the updated state of my data, but I want to understand how to access the same instance here and make the current data available inside the app everywhere

AJ-
  • 1,638
  • 1
  • 24
  • 49
  • [flutter share state across multiple instances of a widget](https://stackoverflow.com/questions/61688479/flutter-share-state-across-multiple-instances-of-a-widget) – Daniil Loban Mar 11 '21 at 07:47
  • I tried in another widget, to just call Contacts() but the data in that instance is not the same, so I am missing something and I don't know what – AJ- Mar 11 '21 at 07:50

1 Answers1

1

EDIT : I removed the original answer to give a better sample of what the OP wants to achieve.

I made a repo on GitHub to try to show you what you want to achieve: https://github.com/Kobatsu/stackoverflow_66578191

There are a few confusing things in your code :

  • When to create instances of your objects (ContactsService, Contacts)
  • Provider usage
  • (Accessing the list of the pagingController ?)
  • Parsing a JSON / using a factory method

The repository results in the following : enter image description here

When you update the list (by scrolling down), the yellow container is updated with the number of contacts and the number of favorites. If you click on a Contact, it becomes a favorite and the yellow container is also updated.

I commented the repository to explain you each part.

Note: the Contacts class in your code became ContactProvider in mine.

The ContactsService class to make the API call :

class ContactsService {
  static Future<List<Contact>> fetchContactsPaged(
      int pageKey, int pageSize) async {
    // Here, you should get your data from your API

    // final response = await http.get(.....);
    // if (response.statusCode == 200) {
    //   return Contacts.fromJson(jsonDecode(response.body));
    // } else {
    //   throw Exception('Failed to load contacts');
    // }

    // I didn't do the backend part, so here is an example
    // with what I understand you get from your API:
    var responseBody =
        "{\"data\":[{\"name\":\"John\", \"isFavorite\":false},{\"name\":\"Rose\", \"isFavorite\":false}]}";
    Map<String, dynamic> decoded = json.decode(responseBody);
    List<dynamic> contactsDynamic = decoded["data"];

    List<Contact> listOfContacts =
        contactsDynamic.map((c) => Contact.fromJson(c)).toList();

    // you can return listOfContacts, for this example, I will add 
    // more Contacts for the Pagination plugin since my json only has 2 contacts
    for (int i = pageKey + listOfContacts.length; i < pageKey + pageSize; i++) {
      listOfContacts.add(Contact(name: "Name $i"));
    }
    return listOfContacts;
  }
}

Usage of Provider :

Consumer<ContactProvider>(
        builder: (_, foo, __) => Container(
              child: Text(
                  "${foo.contacts.length} contacts - ${foo.contacts.where((c) => c.isFavorite).length} favorites"),
              padding: EdgeInsets.symmetric(
                  horizontal: 20, vertical: 10),
              color: Colors.amber,
            )),
    Expanded(child: ContactsBody())
  ]),
)

Fetch page method in the ContactsBody class, where we add the contact to our ContactProvider :

  Future<void> _fetchPage(int pageKey) async {
    try {
      // Note : no need to make a ContactsService, this can be a static method if you only need what's done in the fetchContactsPaged method
      final newItems =
          await ContactsService.fetchContactsPaged(pageKey, _pageSize);
      final isLastPage = newItems.length < _pageSize;
      if (isLastPage) {
        _pagingController.appendLastPage(newItems);
      } else {
        final nextPageKey = pageKey + newItems.length;
        _pagingController.appendPage(newItems, nextPageKey);
      }

      // Important : we add the contacts to our provider so we can get
      // them in other parts of our app
      context.read<ContactProvider>().addContacts(newItems);
    } catch (error) {
      print(error);
      _pagingController.error = error;
    }
  }

ContactItem widget, in which we update the favorite statuts and notify the listeners :

class ContactItem extends StatefulWidget {
  final Contact contact;
  ContactItem({this.contact});

  @override
  _ContactItemState createState() => _ContactItemState();
}

class _ContactItemState extends State<ContactItem> {
  @override
  Widget build(BuildContext context) {
    return InkWell(
        child: Padding(child: Row(children: [
          Expanded(child: Text(widget.contact.name)),
          if (widget.contact.isFavorite) Icon(Icons.favorite)
        ]), padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10),),
        onTap: () {
          // the below code updates the item
          // BUT others parts of our app won't get updated because
          // we are not notifying the listeners of our ContactProvider !
          setState(() {
            widget.contact.isFavorite = !widget.contact.isFavorite;
          });

          // To update other parts, we need to use the provider
          context.read<ContactProvider>().notifyContactUpdated(widget.contact);
        });
  }
}

And the ContactProvider :

class ContactProvider extends ChangeNotifier {
  final List<Contact> _contacts = [];
  List<Contact> get contacts => _contacts;

  void addContacts(List<Contact> newContacts) {
    _contacts.addAll(newContacts);
    notifyListeners();
  }

  void notifyContactUpdated(Contact contact) {
    // You might want to update the contact in your database,
    // send it to your backend, etc...
    // Here we don't have these so we just notify our listeners :
    notifyListeners();
  }
}
leb1755
  • 1,386
  • 2
  • 14
  • 29
  • Thank you for the answer, this is clear, I managed to succesfully use Providers in other Widgets of my App, the problem here is, do you see my ContactsService? There I have a Future call, that is used by my "FetchPage" method for Pagination. I dont understand how can I retrieve the state, of the Data that is initialized by my Infinite Load widget? – AJ- Mar 11 '21 at 08:23
  • If you have only one instance of Contacts and that your ContactService use this same instance, your data will be in the Contacts class and you can access it from there using `context.read()` – leb1755 Mar 11 '21 at 08:29
  • does it not matter if the instance of Contacts Class had been made with ChangeNotifierProvider.value( value: Contacts() ... or it does not? Because in the ContactService I initialise Contacts, by invoking in the API method the factory constructor of Contacts – AJ- Mar 11 '21 at 12:43
  • I would not initialize Contacts in the API method of your ContactService. I would initialize the Contacts class in the `Provider(create: (_)=>Contacts)` method, then get it using `context.read()`. You can also eventually give the ContactService a Contacts attribute in its constructor. – leb1755 Mar 11 '21 at 15:14
  • then I'm not following, in my ContactsService, I need to parse the JSON data I get from the API, and to do so, I call: return Contacts.fromJson(jsonDecode(res.body)); – AJ- Mar 12 '21 at 06:42
  • this creates an instance of my class right? if I dont do this, what other ways there are to approach this issue? – AJ- Mar 12 '21 at 06:43
  • I would have one `Contacts` instance only (which is a list of `Contact`). In my API call, instead of having a factory, I would just use a regular method, e.g.: `void addContactsFromJson(final Map json)`in which you parse the json and add the list of `Contact` parsed to the `_contacts` variable, using your `add()` method for instance. – leb1755 Mar 12 '21 at 10:04
  • could I kindly ask you to edit your answer with a small example of the changes you proposed? I think I almost understood everything but I feel like there is one piece missing and if I see everything written down it might help, and so I can also mark you answer as correct! thank you – AJ- Mar 12 '21 at 11:56
  • I have updated my answer with a sample repository, I should have done that earlier. You can clone the repository from GitHub and play with it, it might help you understand what is happening. Hope it helps ! – leb1755 Mar 12 '21 at 16:49
  • 1
    thank you very much @leb1755, now everything is clear, the part I was missing was, adding my data in the _fetchPage method to my Provider! Now it all makes sense! thank you – AJ- Mar 15 '21 at 07:33
  • I have a question for you @leb1755, in the Contact.dart class file, what is the difference in doing "factory Contact.fromJson" and return a Contact() object from my json data, and what you are doing, not using "factory" and not initiliazing a new Contact() object? – AJ- Mar 15 '21 at 08:41
  • This link explains it : https://stackoverflow.com/a/56107639/3545278 . I didn't see the need of a factory constructor here – leb1755 Mar 15 '21 at 10:18