A utility for safely manipulating asynchronous data.
Using AsyncValue ensures that you never forget to load an asynchronous operation/handle an error condition occurs.
There are also several utilities available to help you convert AsyncValues to other objects.
For example, a Flutter Widget can be used to convert an AsyncValue into either a progress indicator, an error screen, or a display of data.
AsyncValue converts a Future that may fail into something that can be safely read. This is useful to avoid tedious try/catch operations instead of below:
class MyNotifier extends StateNotifier<AsyncValue<MyData> {
MyNotifier(): super(const AsyncValue.loading()) {
_fetchData();
}
Future<void> _fetchData() async {
state = const AsyncValue.loading();
try {
final response = await dio.get('my_api/data');
final data = MyData.fromJson(response);
state = AsyncValue.data(data);
} catch (err, stack) {
state = AsyncValue.error(err, stack);
}
}
}
As the official translation shows, using AsyncValue we can ensure that we don't forget to load asynchronous operations/handle error situations.
When an exception occurs, it is used to push out snack bars or display loading processing.
A demo APP with anonymous login has been created. The function of the async notifier described here and later has not changed.
Here first we created a authRepositoryProvider using FirebaseAuth as parameter to Provider. Since we are not going to change FirebaseAuth instance passing it inside Provider<T>
is just fine.
StateNotifier using with AsyncValue
import 'dart:developer';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_tutorial/auth/auth_page.dart';
import 'package:state_tutorial/auth/hello_page.dart';
//Instantiate the provider of FirebaseAuth.
final authRepositoryProvider =
Provider<FirebaseAuth>((ref) => FirebaseAuth.instance);
//
class AuthController extends StateNotifier<AsyncValue<void>> {
AuthController(this.ref) : super(const AsyncData(null));
final Ref ref;
// Method to perform anonymous login.
Future<void> signInAnonymously(BuildContext context) async {
final authRepository = ref.read(authRepositoryProvider);
state = const AsyncLoading();
state = await AsyncValue.guard(authRepository.signInAnonymously);
log(state.toString());
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const HelloPage()),
(route) => false);
}
// Method to logout.
Future<void> signOutAnonymously(BuildContext context) async {
final authRepository = ref.read(authRepositoryProvider);
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => authRepository.signOut());
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const AuthPage()),
(route) => false);
}
}
// Provider that calls StateNotifier in an external file.
final authControllerProvider =
StateNotifierProvider<AuthController, AsyncValue<void>>((ref) {
return AuthController(ref);
});
You should notice that here we are passing AsyncValue as data type for StateNotifier<T>. This would make sure we have access to loading() and guard() function.
Here in the above code we have both sing in and sign out method. They both use guard() function of AsyncValue which is extremely useful for doing asyncronomous operations. Because we are using AsyncValue.guard()
, we don't need to use try/catch
clause.
Eventually we have created our authControllerProvider which is a global instance of our Provider as a controller. This instance would be available inside UI using ref object.
Button to display loading
Use the callback function, pass the provider to the first argument to ref.listen, and perform processing depending on the old state and new state. When an exception occurs, the snack bar is displayed, and when logging in, a loading process occurs.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_tutorial/auth/auth_state.dart';
// auth button that can be used for signing in or signing out
class AuthButton extends ConsumerWidget {
const AuthButton({super.key, required this.text, required this.onPressed});
final String text;
final VoidCallback onPressed;
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<AsyncValue<void>>(
authControllerProvider,
(_, state) {
if (state.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.asError.toString())),
);
}
},
);
final state = ref.watch(authControllerProvider);
return SizedBox(
width: 200,
height: 60,
child: ElevatedButton(
onPressed: state.isLoading ? null : onPressed,
child: state.isLoading
? const CircularProgressIndicator()
: Text(text,
style: Theme.of(context)
.textTheme
.headline6!
.copyWith(color: Colors.white)),
),
);
}
}
The above code would through error. But we catch the error and show in a SnackBar. This is possible due to earlier we have used AsyncValue.guard() function.
Anonymous login page
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_tutorial/auth/auth_button.dart';
import 'package:state_tutorial/auth/auth_state.dart';
class AuthPage extends ConsumerWidget {
const AuthPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('AsyncNotifier'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AuthButton(
text: "Use without registration",
onPressed: () => ref
.read(authControllerProvider.notifier)
.signInAnonymously(context)),
],
),
),
);
}
}
Now with the above code, if you haven't set up Firebase you will get error and it would be shown in the SnackBar. If you have set up Firebase, then we will redirect you to next page.
Logout page
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_tutorial/auth/auth_state.dart';
class HelloPage extends ConsumerWidget {
const HelloPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final authController = ref.read(authControllerProvider.notifier);
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () async {
authController.signOutAnonymously(context);
},
icon: const Icon(Icons.logout))
],
title: const Text('Auth'),
),
body: Center(child: Text('Hello World')),
);
}
}
Entry point of the code
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_tutorial/auth/auth_page.dart';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const AuthPage(),
);
}
}
The below course has used latest Riverpod 2.0 features including AsyncValue.