Riverpod AsyncNotifier may sound a litle confusing if you are just coming to Riverpod. After all Riverpod and the latest versions they introduced many concepts. AsyncNotifier works with AsyncNotifierProvider. AsyncNotifier is a wrapper around your state or data which works asyncronomously(await and async).
Later in the tutorial we will see how AsyncNotifier wraps our data type in the file generated by code if you create Riverpod providers using code generation. One can also create AsyncNotifier wrapper manually if you don't use code generation tool.
In general, you may be just satisfied with Notifier. It's suitable if you don't do any network request or retreive data from the database. If you do network request, we may need to use AsyncNotifier.
Simply speaking
AsyncNotifier is used for network call or retreiving data from storage
And how to use it then?
Notifier
Well, first we know the Riverpod 2.0 introduce a build() method. This method defines the return type of a provider.
This class is a Notifier class and see the build() method. It returns an empty List. You may also say it returns List Notifier.
I know it's confusing
But this is Riverpod
Simply speaking
This is a Notifier class which exposes a List to NotifierProvider
But even if you don't know these terms, just remember, this class has build method and it returns a List.
Learn about Riverpod 2.0 Stream Provider and Future Provider.
Now let's dive into AsyncNotifier !!
AsyncNotifier
It's used for network call. We know that network type returns Future type. So our above Todo class build() method return type has to change now.
Look at the build() method first, we see that it returns FutureOr which is like a Future. Even though _fetchTodo() returns a List(look at the top), but since it's coming from network or external world, we use FutureOr
AsyncNotifier also comes with some helping method like
AsyncValue.loading()
We don't directly use AsyncValue.loading() in inside build() method. We use it before we actually start to load or make request, you may think it as if its the loading icon.
AsyncValue.loading() is like a circular progress indicator.
AsyncValue.guard()
It's used right after calling AsyncValue.loading(). Gaurd() method helps posting or deleting or updating network request.
In general you will call addTodo() method from UI using AsyncNotifier. And what does it mean?
Here you see we are calling asyncTodosProvider. In this case asyncTodosProvider is our AsyncNotifier.
The last line is read like this
We are passing an AsyncNotifier to AsyncNotifierProvider
ref.watch() may take NotifierProvider or AsyncNotifierProvider
AsyncValue.data()
This is used for data assignment. AsyncValue.data() takes any data type and the data you pass to data() method, it should match the return type of build() method. Of course, if you create a custom method, you may not need to have a return type. In that case AsyncValue.data() method would assign value to state variable.
See a complete code AsyncNotifier
We will create a AsyncNotifierController
@riverpod
class AsyncNotifierScreenController extends _$AsyncNotifierScreenController {
@override
FutureOr build() async {
final response =
await http.get(Uri.parse('https://random-word-api.herokuapp.com/word'));
if (response.statusCode == 200) {
final jsonResponse = jsonDecode(response.body);
final randomWord = jsonResponse[0];
return randomWord;
} else {
print('Request failed with status: ${response.statusCode}.');
}
return '';
}
void delete() {
state = const AsyncValue.data('');
}
Future setNewWord() async {
state = const AsyncLoading();
state = AsyncValue.data(await getNewWord());
}
Future getNewWord() async {
final response =
await http.get(Uri.parse('https://random-word-api.herokuapp.com/word'));
if (response.statusCode == 200) {
final jsonResponse = jsonDecode(response.body);
final randomWord = jsonResponse[0];
return randomWord;
} else {
print('Request failed with status: ${response.statusCode}.');
}
return 'ERROR';
}
}
Here we all these three methods return Future type since we are doing a network request. Here we have AsyncLoading() function is available which is coming from Riverpod. This is used as state during data loading. Because of this you can use show CircularProgressIndicator inside when() function of Riverpod providers.
AsyncLoading() would be active until all the data has been loaded correctly. Here we see AsyncLoading() has been assinged to state variable. This means in the frontend we will be able to access data loading status. In the frontend there would be a property called loading, this would directly connect with the AsyncLoading() state.
Once we get the data from the server, we first put inside AsyncValue.data() function. Depending on your data type, you can put anything inside AsyncValue.data().
Look at setNewWord() method and it has loading mechanism and setting data mechanism. Simply AsyncValue.Loading() and AsyncValue.data() are doing the job.
We can easily call setNewWord() from the UI using our provider.
void main() {
runApp( ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: AsyncNotifierScreen(),
);
}
}
class AsyncNotifierScreen extends ConsumerWidget {
const AsyncNotifierScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(asyncNotifierScreenControllerProvider);
return Scaffold(
backgroundColor: Colors.grey,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
state.when(
data: (data) => Text(
data,
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text(
'$error',
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
)),
floatingActionButton: Row(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton.extended(
label: Text('Delete'),
heroTag: 'delete',
onPressed: () {
ref.read(asyncNotifierScreenControllerProvider.notifier).delete();
},
),
const SizedBox(width: 20),
FloatingActionButton.extended(
label: Text('Get'),
heroTag: 'get',
onPressed: () async {
await ref
.read(asyncNotifierScreenControllerProvider.notifier)
.setNewWord();
},
),
],
),
);
}
}
@riverpod
class AsyncNotifierScreenController extends _$AsyncNotifierScreenController {
@override
FutureOr build() async {
final response =
await http.get(Uri.parse('https://random-word-api.herokuapp.com/word'));
if (response.statusCode == 200) {
final jsonResponse = jsonDecode(response.body);
final randomWord = jsonResponse[0];
return randomWord;
} else {
print('Request failed with status: ${response.statusCode}.');
}
return '';
}
void delete() {
state = const AsyncValue.data('');
}
Future setNewWord() async {
state = const AsyncLoading();
state = AsyncValue.data(await getNewWord());
}
Future getNewWord() async {
final response =
await http.get(Uri.parse('https://random-word-api.herokuapp.com/word'));
if (response.statusCode == 200) {
final jsonResponse = jsonDecode(response.body);
final randomWord = jsonResponse[0];
return randomWord;
} else {
print('Request failed with status: ${response.statusCode}.');
}
return 'ERROR';
}
}
The course that uses AsyncNotifier for real app using Riverpod state management. Click here to know more about the Riverpod course.
AsyncNotifierProvider Dive Deep
Let's take a closer look of the AsyncNotifier through below example.
Here build method returns fetchCourseList() which is a FutureOr type. And it's data type is List
Here you see the last line, AsyncNotifier<List
So we can say that AsyncNotifier equivalent to Future. From last time we also see the we have a typedef _$HomeCourseList.
This typedef is shorthand notation for our data state AsyncNotifier<List
So our homeCourseListProvider is a AsyncNotifierProvider type. And this provider is auto generated when you mention the build() method return type as Future or FutureOr.
In the course, you will learn how to use more complex situation using AsyncNotifier and FutureOr data type.