Saghir
Saghir

Reputation: 2445

Riverpod StateNotifierProvider not updating Widget on state update after async call

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:

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:

Upvotes: 2

Views: 1034

Answers (1)

Rohan Jariwala
Rohan Jariwala

Reputation: 2050

  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.

Upvotes: 3

Related Questions