Reputation: 2445
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:
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:
Upvotes: 2
Views: 1034
Reputation: 2050
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();
await
and make method as Future
.Upvotes: 3