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 ?