skip to Main Content

So am getting data from api and then based on my search field and search type
I update the UI.

For State management am using riverpod.

this is my code

final countriesProvider =
    StateNotifierProvider<CountriesProvider, List<Country>>((ref) {
  return CountriesProvider([]);
});

final futureCountriesProviderFiltred = FutureProvider<List<Country>>(
  (ref) async {
    final countries = ref.watch(countriesProvider);
    final searchType = ref.watch(searchTypeProvider);
    final search = ref.watch(searchProvider);
    print(search); // here
    print(countries.isEmpty);
    if (countries.isEmpty) {
      await ref.read(countriesProvider.notifier).fetchAndSetCountries();
      return countries;
    } else {
      if (searchType == SearchType.name) {
        await ref
            .read(countriesProvider.notifier)
            .fetchAndSetCountriesByName(search);
        return countries;
      } else {
        await ref
            .read(countriesProvider.notifier)
            .fetchAndSetCountriesByRegion(search);
        return countries;
      }
    }
  },
);

final searchTypeProvider = StateProvider<SearchType>((ref) {
  return SearchType.name;
});

enum SearchType { name, region }

final searchProvider = StateProvider<String>((ref) {
  return '';
});

class CountriesProvider extends StateNotifier<List<Country>> {
  final CountriesServices _countriesServices = CountriesServices();

  CountriesProvider(super.state);

  Future<void> fetchAndSetCountries() async {
    try {
      final List<Country> loadedCountries =
          await _countriesServices.getCountries();
      state = loadedCountries;
    } catch (error) {
      if (error == 'Country not found') {}
    }
  }

  Future<void> fetchAndSetCountriesByName(String name) async {
    try {
      final List<Country> loadedCountries =
          await _countriesServices.getCountriesByName(name);
      state = loadedCountries;
    } catch (error) {
      if (error == 'Country not found') {}
    }
  }

  Future<void> fetchAndSetCountriesByRegion(String region) async {
    try {
      final List<Country> loadedCountries =
          await _countriesServices.getCountriesByRegion(region);
      state = loadedCountries;
    } catch (error) {
      if (error == 'Country not found') {}
    }
  }
}

the problem is that the UI is never done loading when there is value in searchProvider
once the value is back to "" empty string, suddenly, it’s done loading

also when there is value in searchProvider the print statement got called to many times till
the value in searchProvider back to "" empty string then it stops

this is the UI code


Widget build(BuildContext context, WidgetRef ref) {
    final countriesProvider = ref.watch(futureCountriesProviderFiltred);   
    return Scaffold(
      appBar: AppBar(
        title: Center(
          child: SizedBox(
              child: SearchBar(
                onChanged: (value) {
                  ref.read(searchProvider.notifier).state = value;
                },
                trailing: [
                  SizedBox(
                    child: DropdownButton<String>(
                      items: <SearchType>[SearchType.name, SearchType.region]
                          .map<DropdownMenuItem<String>>((SearchType value) {
                        return DropdownMenuItem<String>(
                          value: value.toString(),
                          child: Text(
                            value.toString(),
                            style: TextStyle(fontSize: 15),
                          ),
                        );
                      }).toList(),
                      onChanged: (String? newValue) {
                        if (newValue == SearchType.name.toString())
                          ref.read(searchTypeProvider.notifier).state =
                              SearchType.name;
                        else
                          ref.read(searchTypeProvider.notifier).state =
                              SearchType.region;
                      },
                    ),
                  ),
                ],
                
              )),
        ),
      ),
      body: countriesProvider.when(
        data: (countries) {
          return ListView.builder(
            itemCount: countries.length,
            itemBuilder: (context, index) {
              return CountryCard(
                country: countries[index],
              );
            },
          );
        },
        loading: // ui for loading,
        error: (error, stackTrace) => Center(
          child: Text(error.toString()),
        ),
      ),
    );



I tried multiple things but I could not solve it. I know that watch will be listening for any changes in the provider but in the UI I only update it once for every letter the user write (onChanged).

So am just change the state of the searchProvider once. but what I noticed that once the state is not empty string the futureCountriesProviderFiltred provider will updated infinitely.

is it because of this code here?

        await ref
            .read(countriesProvider.notifier)
            .fetchAndSetCountriesByName(search);

And I tried to hard code the value in searchProvider and once I launch the app it will get stuck at loading

final searchProvider = StateProvider<String>((ref) {
  return 'USA';
});

enter image description here

updated

I reduced the code of futureCountriesProviderFiltred to

final futureCountriesProviderFiltred = FutureProvider<List<Country>>(
  (ref) async {
    final countries = ref.watch(countriesProvider);

    await ref.read(countriesProvider.notifier).fetchAndSetCountries();
    return countries;
  },
);

and still print get called many times with loading UI!

2

Answers


  1. You need to change your logic. You have a circular addiction going on:

    start in widget -> final countriesProvider = ref.watch(futureCountriesProviderFiltred)
    -> ref.watch(countriesProvider) in futureCountriesProviderFiltred
    -> update state `countriesProvider` when call onChanged(){}
    if (countries.isEmpty) {
      await ref.read(countriesProvider.notifier).fetchAndSetCountries();
    }
    

    and the circle is closed, because at this point the futureCountriesProviderFiltred update will happen. And because of countries.isEmpty it happens exactly as you described.

    Login or Signup to reply.
  2. This is broken:

    await ref.read(countriesProvider.notifier).fetchAndSetCountries();
    

    Never mutate another provider during the build/create of a provider. There are lints to detect this, and it will almost always lead to circular builds.

    You need to have a proper Directed Acyclic Graph for your provider dependencies. Mutations are essentially backward-pointing (that depends on this) links, and spoil your DAG.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search