zino-hofmann / graphql-flutter

A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package.
https://zino-hofmann.github.io/graphql-flutter
MIT License
3.25k stars 620 forks source link

fetchMore and refetch don't fetch information at all #1198

Closed businessAccountFlutter closed 2 years ago

businessAccountFlutter commented 2 years ago

Just like in the title, I was wondering how to use fetchMore. I was trying a lot of different technics (like trying to do it with refetch) but I am not moving forward with this at all, it just doesn't pull the new information. Code below is what I'm stuck with now. This is meant to be my pagination attempt for my work project. I feel like I cannot find too much information on how to get this to work - it doesn't fetchMore results, the result.data doesn't get updated with newly fetched data - basically it doesn't work. Also, I don't really understand how to get the new Map (I would love for the new results to be merged into the old map) as a variable after previous result and new result are merged together - I would like it to be hopefully a smooth experience and for the data to show up right on the bottom and for the user to not be transported to the top of the list.

class Page extends StatefulWidget {
  const Page({Key? key}) : super(key: key);

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

var howManyToFetch = 3;
var offset = 0;
List repositories = [];
bool refetched = false;

late String readRepositories = '''
          query MyQuery {
(*branch element*)(first: $howManyToFetch, offset: $offset, orderBy: "-published") {
(*I cannot show you the rest of the query*)
  ''';

class PageState extends State<Page> {

  var hasMore = true;
  final controller = ScrollController();

  Future fetch() async {
    if (hasMore) {
      offset = offset+3;
      print("How many to fetch?"+howManyToFetch.toString());
      refetched = true;
      setState(() {});
    } else {
      setState(() {});
    }
  }

  @override
  void initState() {
    super.initState();
    controller.addListener(() {
      if (controller.position.maxScrollExtent == controller.offset) {
        fetch();
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(`
        backgroundColor: Colors.white60,
        appBar: PreferredSize(
          preferredSize:
          Size.fromHeight(MediaQuery.of(context).size.height * 0.07),
          child: Stack(
            children: [
              AppBar(),
              Container(
                alignment: Alignment.bottomCenter,
                child: Image.asset(
                  assetName,
                  fit: BoxFit.contain,
                  height: MediaQuery.of(context).size.height * 0.07,
                ),
              )
            ],
          ),
        ),

       body: Query(
          options: QueryOptions(document: gql(readRepositories)),
          builder:
              (QueryResult result, {
            VoidCallback? refetch,
            FetchMore? fetchMore,
          }) {
            if (result.hasException) {
              return Text(result.exception.toString());
            }

            if (result.isLoading) {
              return const Center(
                child: CircularProgressIndicator(),
              );
            }

            if (repositories.isEmpty) {
              repositories = result.data?(*Rest of the path to edges*);
              print("Empty "+(result.data?(*Rest of the path to edges*).length).toString());
            }
            else {
              if (refetched) {
                refetch!();
                fetchMore!(FetchMoreOptions(updateQuery: (prev, fetched) => prev = deeplyMergeLeft(
                  [prev, fetched],
                ), variables: {"offset":offset, "first":howManyToFetch}));
                print("Not empty "+(result.data?[(*Rest of the path to edges*).length).toString());
                List repositories1 = result.data?(*Rest of the path to edges*);
                print(repositories1.length);
                // List newRepositories = result.data?(*Rest of the path to edges*);
                // for (int i=0;i<newRepositories.length;i++) {
                //   repositories.add(newRepositories[i]);
                // }
                refetched = false;
              }
            }

            hasMore = (result.data?(*Rest of the path to hasNextPage*));

            if (repositories == null) {
              return const Text('No repositories');
            }

            return ListView.builder(
              controller: controller,
                itemCount: repositories.length+1,
                itemBuilder: (context, index) {
                  if (index < repositories.length){
                    final repository = repositories[index];

(*List elements' variables*)

                    (*Component for creating list elements*) {
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                              builder: (context) => (*Builder function*)
                        );
                      },
                    );
                  } else {
                    return Padding(padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), child: Center(child: hasMore ? const CircularProgressIndicator() : const Text("No more information", style: TextStyle(color: Colors.grey),),),);
                  }
                });
          },
        )
    );
  }
}

UPDATE It seems as if it couldn't take new variable values or even the query itself because when I wanted to check if variables update they did but the result was still the same, but after I added a part where if it refetches/fetches more it has to clear the result data from the map and it now shows null, doesn't add new data to it.

The code is now (I am constantly commenting and uncommenting different parts of it):

if (repositories.isEmpty) {
              repositories = result.data?(*path*)?['edges'];
              print("Empty "+(result.data?(*path*)?['edges'].length).toString());
            }
            else {
              if (refetched) {
                result.data?.clear();
                refetch!();
                // var opts = FetchMoreOptions.partialUpdater((prev, fetched) => deeplyMergeLeft([prev, fetched]));
                // fetchMore!(FetchMoreOptions(updateQuery: opts, variables: {"offset": offset, "first":howManyToFetch}));
                // fetchMore!(FetchMoreOptions(updateQuery: (prev, fetched) => deeplyMergeLeft(
                //   [prev, fetched],
                //   ), variables: {"offset":offset, "first":howManyToFetch}));
                print("offset "+offset.toString());
                print("How many to fetch? "+howManyToFetch.toString());
                print("Not empty "+(result.data?(*path*).length).toString());
                // List newRepositories = result.data?(*path*);
                // for (int i=0;i<newRepositories.length;i++) {
                //   repositories.add(newRepositories[i]);
                // }
                refetched = false;
              }
            }
budde377 commented 2 years ago

TBH I'm having a hard time following your question here.

Are you wondering how you can merge the result? - Then look at the updateQuery: argument to your FetchOptions. The deeplyMergeLeft function doesn't concat arrays so you'll need to implement your own logic.

majorsigma commented 2 years ago

I am also having serious issue while using the fetchMore method and there are scarce resources on the web to help with this. I'll appreciate it if a clearer example of how this method can be used is provided.

budde377 commented 2 years ago

@majorsigma please describe your issue in more detail and we can help.

majorsigma commented 2 years ago

@majorsigma please describe your issue in more detail and we can help.

Here is a method I created to fetch latest ads

 Future<QueryResult<dynamic>> fetchMoreLatestAds({
    int? adLocationId,
    String? sortOrder,
    String? adCondition,
    String? adType,
    int? limit,
    int? offset,
  }) async {
    FetchMoreOptions fetchMoreOptions = FetchMoreOptions(
        document: gql(
"""
query FetchLatestAds(
    ${sortOrder == null ? '' : '\$sortOrder: order_by,'}
    ${adCondition == null ? '' : '\$adCondition: String,'}
    ${adType == null ? '' : '\$adType: String,'}
    ${adLocationId == null ? '' : '\$adLocationId: Int,'}
    ${limit == null ? '' : '\$limit: Int,'}
    ${offset == null ? '' : '\$offset: Int,'}
) {
  Adverts(
    where: {
      status: {_eq: LIVE}
      condition: ${adCondition == null ? '{}' : '{_eq: \$adCondition}'},
      type: ${adType == null ? '{}' : '{_eq: \$adType}'}
      local_government: {
        id: ${adLocationId == null ? '{}' : '{_eq: \$adLocationId}'}
      }
    },
   ${sortOrder == null ? '' : 'order_by: {createdAt: $sortOrder}'}
   ${limit == null ? '' : 'limit: \$limit'}
   ${offset == null ? '' : 'offset: \$offset'}
  ) {
    promotion {
      status
      expiry
      transaction {
        promotion_package {
          name
          type
        }
      }
    }
    ID
    category_id
    condition
    createdAt
    discountPrice
    isNegotiable
    name_of_store
    priceAndOthers {
      ads_id
      discountPrice
      id
      jobMaxSalary
      jobRole
      jobSalary
      maxPrice
      jobType
      rentPeriod
      reservePrice
      standardPrice
      startPrice
      yearsOfExperience
    }
    user {
      userProfiles {
        first_name
        last_name
        display_name
      }
    }
    standardPrice
    state
    status
    sub_category_id
    title
    termsAndCondition
    type
    user_id
    phone
    LGA
    adds_category {
      title
    }
    Images {
      thumbnail
    }
    AddsReviews {
      id
    }
    views {
      id
    }
    ads_state {
      id
      name
    }
    local_government {
      id
      name
    }
    AddsReviews_aggregate {
      aggregate {
        avg {
          rating
        }
      }
    }
  }
}
"""
),
        variables: determineQueryVariablesForLatestAdsPage(
          sortOrder: sortOrder,
          adCondition: adCondition,
          adLocationId: adLocationId,
          adType: adType,
          limit: limit,
        ),
        updateQuery: (
          Map<String, dynamic>? previousResultData,
          Map<String, dynamic>? fetchMoreResultData,
        ) {
          final List<dynamic> adverts = [
            ...previousResultData?['Adverts'],
            ...fetchMoreResultData?['Adverts'],
          ];

         _logger.d("Fetched adverts: $adverts");
          return {"Adverts": adverts};
        });

Is the way I implemented it okay?

businessAccountFlutter commented 2 years ago

TBH I'm having a hard time following your question here.

Are you wondering how you can merge the result? - Then look at the updateQuery: argument to your FetchOptions. The deeplyMergeLeft function doesn't concat arrays so you'll need to implement your own logic.

Yes but there is a more important issue which is that neither fetchMore nor refetch work - fetchMore doesn't fetch any data as to my understanding while refetch doesn't refresh the values of variables used in the query.

Could you help me understand what I'm doing wrong? As @majorsigma said there is not enough information on the topic of fetchMore.

Update: I've now implemented something that I thought might work but it still didn't. Here's the code and debug response: Zrzut ekranu 2022-08-16 o 10 36 07

budde377 commented 2 years ago

Help me help you. I understand that it's frustrating that you don't feel that this works and is properly documented. We're always open to PRs! For me to understand your question and help you, can you please write a minimal reproducible example with an explanation on what you're expecting and what you're seeing. I can't reproduce screenshots.

businessAccountFlutter commented 2 years ago

Help me help you. I understand that it's frustrating that you don't feel that this works and is properly documented. We're always open to PRs! For me to understand your question and help you, can you please write a minimal reproducible example with an explanation on what you're expecting and what you're seeing. I can't reproduce screenshots.

My file is responsible for showing cards with data fetched from database. Here is a fragment of the Scaffold body with Query widget.

body: Query(
          options: QueryOptions(document: gql(readRepositories)),
          builder:
              (QueryResult result, {
            VoidCallback? refetch,
            FetchMore? fetchMore,
          }) {
            if (result.hasException) {
              return Text(result.exception.toString());
            }

            if (result.isLoading) {
              return const Center(
                child: CircularProgressIndicator(),
              );
            }

            if (repositories.isEmpty) {
              repositories = result.data?['allNews']?['edges'];
              print("Empty "+(result.data?['allNews']?['edges'].length).toString());
            }
            else {
              if (refetched) {

                //result.data?.clear();
                //refetch!();
                Map<String,dynamic> newMap = {};
                // var opts = FetchMoreOptions.partialUpdater((prev, fetched) => newMap = {
                //   ...?prev,
                //   ...?fetched,
                // });
                fetchMore!(FetchMoreOptions(updateQuery: (prev, fetched) => newMap = {
                  ...?prev,
                  ...?fetched,
                }, variables: {"offset": offset, "first":howManyToFetch}));
                // fetchMore!(FetchMoreOptions(updateQuery: (prev, fetched) => deeplyMergeLeft(
                //  [prev, fetched],
                //  ), variables: {"offset":offset, "first":howManyToFetch}));
                print("offset "+offset.toString());
                print("How many to fetch? "+howManyToFetch.toString());
                print("Not empty "+(newMap.length).toString());
                List newRepositories = result.data?['allNews']?['edges'];
                for (int i=0;i<newRepositories.length;i++) {
                  repositories.add(newRepositories[i]);
                }
                refetched = false;
              }
            }

If you need me to I can send you the whole code that is in the dart file.

vincenzopalazzo commented 2 years ago

You should be able to reproduce your problem in a clean repository or a simple query, we are preparing a debug API that you can do for this job https://api.chat.graphql-flutter.dev/graphql

P.S: This process is always required to give us the possibility to debug it with a monkey example, and debug it. Most of the issue can be cause from other stuff that is going on your code, so it is useful also to you this to avoid waste time to catch bugs that may not exit.

Have fun!

Sorry if I jump in the conversation

businessAccountFlutter commented 2 years ago

You should be able to reproduce your problem in a clean repository or a simple query, we are preparing a debug API that you can do for this job https://api.chat.graphql-flutter.dev/graphql

P.S: This process is always required to give us the possibility to debug it with a monkey example, and debug it. Most of the issue can be cause from other stuff that is going on your code, so it is useful also to you this to avoid waste time to catch bugs that may not exit.

Have fun!

Sorry if I jump in the conversation

Ok, I'll post the whole code if it helps but I'm not sure if it does. I don't really know how to give you the example so you could reproduce it.

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:graphql/src/utilities/helpers.dart';
import 'package:intl/intl.dart';

import '../tile_widget/article_tile_big_photo.dart';
import 'informacje_page_inside.dart';

class InformacjePage extends StatefulWidget {
  const InformacjePage({Key? key}) : super(key: key);

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

var howManyToFetch = 3;
var offset = 0;
List repositories = [];
bool refetched = false;

late String readRepositories = '''
          query MyQuery {
            allNews(first: $howManyToFetch, offset: $offset, orderBy: "-published") {
              edges {
                node {
                  name
                  id
                  published
                  newsPhotos {
                    edges {
                      node {
                        file
                      }
                    }
                  }
                }
              }
              pageInfo {
                hasNextPage
              }
            }
          }
  ''';

class _InformacjePageState extends State<InformacjePage> {

  var korekta = 0;

  var hasMore = true;
  final controller = ScrollController();

  Future fetch() async {
    if (hasMore) {
      offset = offset+3;
      refetched = true;
      setState(() {});
    } else {
      setState(() {});
    }
  }

  @override
  void initState() {
    super.initState();
    controller.addListener(() {
      if (controller.position.maxScrollExtent == controller.offset) {
        fetch();
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.white60,
        appBar: PreferredSize(
          preferredSize:
          Size.fromHeight(MediaQuery.of(context).size.height * 0.07),
          child: Stack(
            children: [
              AppBar(),
              Container(
                alignment: Alignment.bottomCenter,
                child: Image.asset(
                  'assets/logo_herb_poziom_sww_kolor.png',
                  fit: BoxFit.contain,
                  height: MediaQuery.of(context).size.height * 0.07,
                ),
              )
            ],
          ),
        ),
        body: Query(
          options: QueryOptions(document: gql(readRepositories)),
          builder:
              (QueryResult result, {
            VoidCallback? refetch,
            FetchMore? fetchMore,
          }) {
            if (result.hasException) {
              return Text(result.exception.toString());
            }

            if (result.isLoading) {
              return const Center(
                child: CircularProgressIndicator(),
              );
            }

            if (repositories.isEmpty) {
              repositories = result.data?['allNews']?['edges'];
              print("Empty "+(result.data?['allNews']?['edges'].length).toString());
            }
            else {
              if (refetched) {

                //result.data?.clear();
                //refetch!();
                Map<String,dynamic> newMap = {};
                // var opts = FetchMoreOptions.partialUpdater((prev, fetched) => newMap = {
                //   ...?prev,
                //   ...?fetched,
                // });
                fetchMore!(FetchMoreOptions(updateQuery: (prev, fetched) => newMap = {
                  ...?prev,
                  ...?fetched,
                }, variables: {"offset": offset, "first":howManyToFetch}));
                // fetchMore!(FetchMoreOptions(updateQuery: (prev, fetched) => deeplyMergeLeft(
                //  [prev, fetched],
                //  ), variables: {"offset":offset, "first":howManyToFetch}));
                print("offset "+offset.toString());
                print("How many to fetch? "+howManyToFetch.toString());
                print("Not empty "+(newMap.length).toString());
                List newRepositories = result.data?['allNews']?['edges'];
                for (int i=0;i<newRepositories.length;i++) {
                  repositories.add(newRepositories[i]);
                }
                refetched = false;
              }
            }

            hasMore = (result.data?['allNews']?['pageInfo']?['hasNextPage']);

            if (repositories == null) {
              return const Text('No repositories');
            }

            return ListView.builder(
              controller: controller,
                itemCount: repositories.length+1,
                itemBuilder: (context, index) {
                  if (index < repositories.length){
                    final repository = repositories[index];

                    var title = (repository['node']?['name'] ?? "Brak danych");

                    final List photos =
                        repository['node']?['newsPhotos']?['edges'];
                    var photo = "Brak zdjecia";
                    if (photos.isNotEmpty) {
                      photo = (photos[0]?['node']?['file'] ?? "Brak zdjecia");
                    }
                    var photoLink =
                        "http://ns31200045.ip-51-83-143.eu:25001/media/" +
                            photo;

                    DateTime dt =
                        DateTime.parse(repository['node']?['published']);
                    final createdDay =
                        DateFormat('dd.MM.yyyy, kk:mm').format(dt);

                    var city = "";

                    if (index > 3) {
                      korekta = index ~/ 4;
                    } else {
                      korekta = 0;
                    }
                    var newColor = index - korekta * 4;

                    return ArticleTileBigPhoto(
                      thumbnail: NetworkImage(photoLink),
                      title: title,
                      publishDate: createdDay,
                      city: city,
                      colorInt: newColor,
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                              builder: (context) => ArticlePageInside(
                                  id: repository['node']?['id'], name: title,photoLink: photoLink,),),
                        );
                      },
                    );
                  } else {
                    return Padding(padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), child: Center(child: hasMore ? const CircularProgressIndicator() : const Text("Brak informacji do pobrania", style: TextStyle(color: Colors.grey),),),);
                  }
                });
          },
        )
    );
  }
}
budde377 commented 2 years ago

Cheers, now please explain what you're seeing and what you're expecting in a way you'd explain it to a junior dev on your team

businessAccountFlutter commented 2 years ago

Cheers, now please explain what you're seeing and what you're expecting in a way you'd explain it to a junior dev on your team

Well, what I'm seeing is something like that:

Zrzut ekranu 2022-08-16 o 13 14 03

The problem is that it seems like the fetchMore function doesn't work or I simply cannot use it. After it's called my new results (if they even exist because I cannot work out whether fetchMore works or not but it may work because when it runs it reloads the list) are not merged into one map with previous results which is shown by the "Not empty 0" debug message which is responsible for showing merged results map. What I need is to update my list of news objects (being title, photo and publication date) with newly downloaded data, for now 3 at a time. I am trying to use pagination because I simply don't want to download all the data at the same time. I would like it to also not take the user to the top of the list when it fetches more data.

I've tried to use result.data after refetch but it didn't work, especially after clear() the result.data. I also tried updating the list I'm using to create the ListView but it only caused a flickering effect.

I hope I explained it good enough. Sorry if anything is unclear.

businessAccountFlutter commented 2 years ago

I've changed the fetchMore part in the Query builder to: Zrzut ekranu 2022-08-17 o 08 33 57

The result I'm getting is all the same results as I got previously so prev = fetched even tho I have the variables listed. And also, the fetchMore function is done after all the other things in the function placed after it. Here it is represented by debug and screenshot from my app which already displays the data of the first query but fetch downloaded the same data again against the variables being prepared to download completely new data. Zrzut ekranu 2022-08-17 o 08 33 13

Update: It seems to me like the fetchMore doesn't consider the variables I pass. How to get them to work? Here is a reference picture of what the debug shows after printing both previous and newly fetched data: Zrzut ekranu 2022-08-17 o 11 14 33

Here is the length of both results: Zrzut ekranu 2022-08-17 o 11 19 27

budde377 commented 2 years ago

Alright, so I think I know what's going on here. It's pretty hard to follow your code but I understand that you're having problems propagating your variables because this is how you build your query:

late String readRepositories = '''
          query MyQuery {
            allNews(first: $howManyToFetch, offset: $offset, orderBy: "-published") {
              edges {
                node {
                  name
                  id
                  published
                  newsPhotos {
                    edges {
                      node {
                        file
                      }
                    }
                  }
                }
              }
              pageInfo {
                hasNextPage
              }
            }
          }
  ''';

Your query should never change. It should be constant. So injecting your variables like this will fail.

const String readRepositories = r'''
          query MyQuery($first: Int!, $offset: Int!) {
            allNews(first: $first, offset: $offset, orderBy: "-published") {
              edges {
                node {
                  name
                  id
                  published
                  newsPhotos {
                    edges {
                      node {
                        file
                      }
                    }
                  }
                }
              }
              pageInfo {
                hasNextPage
              }
            }
          }
  ''';

now you can correctly pass new variables when calling fetch more.

businessAccountFlutter commented 2 years ago

@budde377 it still doesn't work

(first: $first, offset: $offset, orderBy: "-published")

I get an error that constant variables can't be assigned a value. I know that but I need to assign offset with new value for it to change. Also, this: Zrzut ekranu 2022-08-24 o 12 24 50

vincenzopalazzo commented 2 years ago

you are missing the r in front of the string, otherwise this error make no sense to me, the query look sane

businessAccountFlutter commented 2 years ago

I've change code to:

int first = 3;
int offset = 0;
List<dynamic> repositories = [];
bool refetched = false;

const String readRepositories = r'''
          query MyQuery($first: Int!, $offset: Int!) {
            allNews(first: $first, offset: $offset, orderBy: "-published") {
              edges {
                node {
                  name
                  id
                  published
                  newsPhotos {
                    edges {
                      node {
                        file
                      }
                    }
                  }
                }
              }
              pageInfo {
                hasNextPage
              }
            }
          }
  ''';

Now I'm getting this error: Zrzut ekranu 2022-08-24 o 12 33 38

budde377 commented 2 years ago

The error states that you're missing a variable. Check that you're providing the variable in your query

businessAccountFlutter commented 2 years ago

The error states that you're missing a variable. Check that you're providing the variable in your query

Ok, sorry, it was my mistake. Thank you, everythings working. Still, don't really know how to connect both results into one.

budde377 commented 2 years ago

That depends on your use case. You have the new and the old data and you need to return whatever you expect to be the result.

businessAccountFlutter commented 2 years ago

That depends on your use case. You have the new and the old data and you need to return whatever you expect to be the result.

Ok, somehow I understood how to do this. Could you please tell me how can I prevent the app from going to the top of my ListView after fetchMore?

budde377 commented 2 years ago

You mean scrolling to the top?

businessAccountFlutter commented 2 years ago

You mean scrolling to the top?

Right now when my app fetches more data it automatically scrolls to the top by itself. What I would like is for it to stay in the same place the user scrolled to.

budde377 commented 2 years ago

I can't help you with this because it's highly dependent on your setup and out of scope of this library, I.e. the behaviour would probably be the same if you had any other method of fetching data.

Take a look at scroll controllers and scroll behaviour of your widget? I suspect that you're not storing scroll state in the shape of a scroll controller outside of your build method and thus any re build of your widget will yield a new instance of the scrollable widget being mounted and thus reset. Maybe you can solve this by adding a key to the scrollable view or wrap the query widget inside the scroll view.

businessAccountFlutter commented 2 years ago

I can't help you with this because it's highly dependent on your setup and out of scope of this library, I.e. the behaviour would probably be the same if you had any other method of fetching data.

Take a look at scroll controllers and scroll behaviour of your widget? I suspect that you're not storing scroll state in the shape of a scroll controller outside of your build method and thus any re build of your widget will yield a new instance of the scrollable widget being mounted and thus reset. Maybe you can solve this by adding a key to the scrollable view or wrap the query widget inside the scroll view.

Oh, ok. Thank you so much for all your help!

budde377 commented 2 years ago

No worries. You can also try and reach out on our discord (link should be in the readme) for quicker answers from the whole community