Flutter and State Management
While working on the Skica app, one issue I noticed is that when closing and reopening an app, the users are asked to login every time since the login screen is the first page they're intended to see.
I started thinking of different solutions. But, the one I ended up settling on was using a "wrapper" that is loaded first. This wrapper checks to see if a user is already logged in. If so, it sends them to the home page. If not, to the login.
Because the Wrapper is a top-level page, we must use the Provider package (Provider is the standard for state management), to pass that information up our widget tree to the Wrapper.
I will show you how this was done below.
app.dart
class App extends StatelessWidget {
static FirebaseAnalytics analytics = FirebaseAnalytics();
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthService>(create: (_) => AuthService()),
StreamProvider<User>.value(
value: FirebaseAuth.instance.authStateChanges(),
),
],
child: MaterialApp(
home: Wrapper(),
navigatorObservers: [FirebaseAnalyticsObserver(analytics: analytics)],
theme: skicaTheme,
),
);
}
}
This is the entry point to the app. As you can see, MaterialApp
has Wrapper returned as its home option. So when the app is started, it starts by creating a MultiProvider
widget(where we declare our different state objects) and that has a MaterialApp child.
wrapper.dart
class Wrapper extends StatelessWidget {
Wrapper({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final User user = Provider.of<User>(context);
final AuthService authService = Provider.of<AuthService>(context);
if (authService.loading) {
return Spinner();
} else if (user == null) {
return LoginPage();
} else {
return HomePage();
}
}
}
Our wrapper.dart is pretty simple. If the AuthService
is loading, it gives us a loading spinner. If user, which is equal to a Provider, is null, we get the login page. Otherwise, give us the home page.
Now to see how the state is passed, I will show a snippet of the Google auth in my AuthService
class.
auth.dart
class AuthService with ChangeNotifier {
bool loading = false;
// Signin with Google.
Future<void> signInWithGoogle() async {
// See: https://firebase.flutter.dev/docs/auth/social/
loading = true;
notifyListeners();
final GoogleSignInAccount googleUser = await GoogleSignIn().signIn();
if (googleUser?.authentication != null) {
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
final GoogleAuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
await FirebaseAuth.instance.signInWithCredential(credential);
}
loading = false;
notifyListeners();
}
Here you can see that the signInWithGoogle
function has a loading boolean set to true at the beginning of the function. This is what is being referenced in the Wrapper. The wrapper is aware of this because of the line below the loading boolean which is notifyListeners()
.
notifyListeners()
is what notifies your Providers of the current state of something. This is why the Wrapper is able to check on the value of authService.loading
.
Because this app is integrated with Firebase Firestore, we have a nice simple way to handle social logins. If you look back to the app.dart
file in our MultiProvider
, you can see where the Providers are listening for the changes.
app.dart
providers: [
ChangeNotifierProvider<AuthService>(create: (_) => AuthService()),
StreamProvider<User>.value(
value: FirebaseAuth.instance.authStateChanges(),
),
],
If there are changes, the state is made available to the widgets children. ChangeNotifierProvider
and StreamProvider
do behave in slightly different ways, but essentially they both make the state of something available.
I know to some this may seem a bit complicated. I know it was one of the harder things for me to grasp at first. But, once you start trying to implement it yourself, it becomes a lot easier to understand.
That's all for my little intro to Flutter and State!