1

In Flutter Web, I depend on StateNotifierProvider to fetch data from API call and using another widget to update this API need to trigger the state change to do a refetch.

I use ref.watch for viewing the data and refetch data by ref.read. However, clicking the button in the widget responsible for updating the state actually does an API call and after that call I use ref.read to execute the fetching again and that does NOT cause the data to be updated after going back to the first widget that is responsible of listing the data in the state.

Tries and conclusions:

  • Removing the async call to API when pressing the button causes everything to work normally.
  • Putting the class of the widget code in the same file as the main file, causes everything to work normally.
  • Verified that using ref.read actually executes the API call and the response is correct (chrome network tab in devtools)

This is a sample replicating the problem. For the sake of simplicity, the API call fires on pressing the button actually does a normal get request.

Dependencies used

dependencies:
  flutter:
    sdk: flutter
  flutter_hooks: ^0.18.0
  hooks_riverpod: ^2.3.10
  dio: ^5.1.1 # Http client
  go_router: ^6.5.7

main.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'add_fact_page.dart';
import 'providers.dart';

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends HookConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    var router = GoRouter(
      debugLogDiagnostics: true,
      initialLocation: '/home',
      routes: [
        GoRoute(
          name: 'home',
          path: '/home',
          builder: (context, state) =>
              const MyHomePage(title: 'Flutter Demo Home Page'),
          routes: <GoRoute>[GoRoute(
            name: 'addNewFact',
            path: 'new',
            builder: (context, state) => const AddFact(),
          )],
        ),
      ],
    );

    return MaterialApp.router(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      routerConfig: router,
    );
  }
}

class MyHomePage extends StatefulHookConsumerWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

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

class MyHomePageState extends ConsumerState<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    final facts = ref.watch(factsNotifierProvider);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.separated(
              itemCount: facts.length,
              separatorBuilder: (BuildContext context, int index) =>
              const Divider(),
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text('fact: ${facts[index].fact}'),
                  subtitle: Text('length: ${facts[index].length}'),
                );
              },
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          GoRouter.of(context).goNamed('addNewFact');
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

providers.dart

import 'package:dio/dio.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final httpClientProvider = Provider<Dio>((ref) {
  return Dio();
});

final factsNotifierProvider =
StateNotifierProvider<FactsNotifier, List<Fact>>((ref) {
  return FactsNotifier(
    httpClient: ref.read(httpClientProvider),
  );
});

class FactsNotifier extends StateNotifier<List<Fact>> {
  FactsNotifier({required this.httpClient}) : super([]) {
    fetchFact();
  }

  final Dio httpClient;
  List<Fact> facts = [];

  void fetchFact() async {
    final httpClient = Dio();
    var factResponse = await httpClient.get("https://catfact.ninja/fact",
        options: Options(contentType: Headers.jsonContentType));

    var fact = Fact.from(factResponse.data as Map<String, dynamic>);
    facts = [...facts, fact];
    state = facts;
  }
}

class Fact {
  String fact;
  int length;

  Fact({required this.fact, required this.length});

  factory Fact.from(Map<String, dynamic> json) => Fact(
    fact: json["fact"],
    length: json["length"],
  );
}

add_fact_page.dart

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'providers.dart';

class AddFact extends HookConsumerWidget {
  const AddFact({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final dio = ref.watch(httpClientProvider);

    return Scaffold(
      appBar: AppBar(
          title: const Text('Separate Scaffold'),
          leading: IconButton(
            onPressed: () {
              ref.read(factsNotifierProvider.notifier).fetchFact();
              GoRouter.of(context).pop();
            },
            icon: const Icon(Icons.arrow_back),
          )),
      body: Center(
        child: ElevatedButton.icon(
          style: ElevatedButton.styleFrom(
            backgroundColor: Theme.of(context).primaryColor,
            foregroundColor: Colors.white,
          ),
          onPressed: () async {
            var response = await dio.get("https://catfact.ninja/fact",
                options: Options(contentType: Headers.jsonContentType));

            if (!context.mounted) return;
            if (response.statusCode == 200) {
              ref.read(factsNotifierProvider.notifier).fetchFact();

              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(
                  content: Text('State Updated!'),
                ),
              );
            }

            GoRouter.of(context).pop();
          },
          icon: const Icon(
            Icons.save_rounded,
            size: 20,
          ),
          label: const Text('Add'),
        ),
      ),
    );
  }
}

The questions:

  • Why this behavior occurs ? I suspect the async call when pressing the button but why that is an issue ?
  • How to resolve that behaviour to achieve the goal of refreshing the state after an API call based on button pressed ?
  • What would be the alternative to do an async call and force refresh the state to refresh the data by calling the API ?
Anddo
  • 2,144
  • 1
  • 14
  • 33
  • I did but may you explain where exactly as `ref.refresh` actually returns the data. But to answer your question, yes I did in replacement to `ref.read` after checking the status code. Same effect. – Anddo Sep 01 '23 at 15:47
  • The literature is rushing to catch up with Remi's prolific development. In brief, avoid legacy ChangeNotifier, StateNotifier (and their providers) and StateProvider. Use only Provider, FutureProvider, StreamProvider, and Notifier, AsyncNotifier, StreamNotifier (and their providers). – Randal Schwartz Sep 01 '23 at 18:13
  • Hmm that is not what the [official doc](https://riverpod.dev/docs/concepts/providers) is talking about. Actually, I used `StateNotifierProvider` trying to follow what is recommended. In that specific case, what do you recommend to use as alternative to `StateNotifierProvider` and avoid using `StateNotifier ` ? – Anddo Sep 02 '23 at 12:47
  • The "official" doc is in transition. What I said is the shared folklore current story, and the literature is rushing to catch up. Use a Notifier[Provider] to replace the legacy providers. – Randal Schwartz Sep 02 '23 at 16:54

1 Answers1

2
  1. Why does this behavior occur?
  • The behaviour occurs because you are performing an async call when pressing the button in the AddFact widget. As this call waits and you're not waiting while calling method.
  1. How to resolve this behavior to achieve the goal of refreshing the state after an API call based on a button press?
  • The first and foremost thing you can do is make your fetchFact as a Future method like below
    Future<void> fetchFact() async {
        // .... CODE_AS_IT_IS
      }

And then use await with you call like below

await ref.read(factsNotifierProvider.notifier).fetchFact();
  1. What would be the alternative to do an async call and force refresh the state to refresh the data by calling the API?
  • The only option you have is you've to add await and make method as Future.
Rohan Jariwala
  • 1,564
  • 2
  • 7
  • 24