How to Handle and Share Login State with Provider in Flutter App

Implementing and Sharing Login State with Auto-Login in Flutter Apps: A Practical Guide Using Provider

Matatias Situmorang
5 min readJan 19, 2024
Photo by Onur Binay on Unsplash

This might be a very common feature that we found in many applications. Also, there are some workarounds on how to handle the login state in the Flutter app. But in this article, I just want to provide one of the examples of mine. It's simple, but I am happy I can learn this thing and implement it in my current project.

Let's start with the folder structure:

my_flutter_app/
|-- lib/
| |-- main.dart
| |-- splash_screen.dart
|--|-- features/
| |-- auth/
| | |-- data/
| | |-- domain/
| | | |-- model.dart
| | |-- presentation/
| | | |-- pages/
| | | | |-- login_page.dart
| | | | |-- signup_page.dart
| | | | |-- onboard_page.dart
| | | |-- controller/
| | | | |-- auth_provider.dart
|--|-- home/
| | |-- presentation/
| | | |-- pages/
| | | | |-- home_page.dart
|--|-- profile/
| | |-- presentation/
| | | |-- pages/
| | | | |-- profile_page.dart
|-- pubspec.yaml

The concept is to use Future.builder() on top of my Flutter App. Or maybe you can also change it with Stream.builder() to keep listening to the expired token. In this case, I will use a simple example. We will handle some cases:

  • First-time installed app: Navigate to Onboard Screen (welcoming user)
  • Users logged out: Navigate to the Login screen for cases: experienced onboarding, Post logout, or Re-login.
  • Users Logged in: Navigate to Home Screen: Handling Automatic Login for Returning Users with Saved Data

First, this is my model user. You may need to modify it as yours

model.dart

@JsonSerializable()
class UserApp {
final int id;
final String username;
final String email;
final String apiToken;

const UserApp({ .... });
}

We will focus on the provider that handles the state of user authentication. In this case, I place it inside my controlled folder on the auth feature.

auth_provider.dart

class AuthState extends ChangeNotifier {
/// state of user information
UserApp _user = const UserApp();
UserApp get userInfo => _user;
void setUser(UserApp user) {
_user = user;
notifyListeners();
}

void logout(){
_user = const UserApp(); // api token is empty
notifyListeners();
}

bool get isAuthorized {
return _user.apiToken.isNotEmpty;
}

bool displayedOnboard = false;

Future<bool> tryLogin() async {
final preferences= await SharedPreferences.getInstance();

// chehck onboad from local storage
displayedOnboard = preferences.getBool('showOnboard') ?? false;
if(!displayedOnboard){
// directly return false, when onboard never displayed
return false;
}

// fetch user info
final user = await fetchUser();
if (user != null) {
_user = user;
return true; // has a login record.
}
return false;
}

This provider will only handle the authentication state of the application. We must avoid putting another field except for those related to authentication.

If we look at the code above, I have a method tryLogin(). You can put there any condition case based on your needs. In this case, I save it as local storage for onboard and user info. So, when my apps have stored user info, it will automatically navigate to the home screen without login.

next move to the main app, where we invoke the method.

main.dart

We define our provider here and another initial setup.

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AuthState()),
// other providers
],
builder: (context, child) => MaterialApp(
title: 'My Flutter App',
theme: ThemeData(primarySwatch: Colors.indigo),
home: Consumer<AuthState>(builder: (context, authState, _) {
return authState.isAuthorized
? const HomeScreen()
: FutureBuilder(
future: authState.tryLogin(),
builder: (context, snapshot) =>
snapshot.connectionState == ConnectionState.waiting
? const SplashScreen()
: authState.displayedOnboard
? const LoginPage()
: const OnboardScreen(),
);
}),
),
);
}
}

What we have above:

  • First, we register our provider on the above of our MaterialApp

ChangeNotifierProvider(create: (context) => AuthState()),

  • Next, wrap your initial screen with Consumer because we need to watch the login state in the whole app.

home: Consumer<AuthState>(builder: (context, authState, _) {

  • Check the first condition, if the app already running and the user has been logged in. We can get it from our provider.

authState.isAuthorized

  • We will display the HomeScreen if it's Authorized or Try to log in if not.
? const HomeScreen()
: FutureBuilder( future: authState.tryLogin(),
  • while loading to execute the tryLogin() method, we can display a splash screen.
  • After finishing the execution, check again if it's a new installation that never displays the onboard screen or if it's an old user that has not log in yet.
snapshot.connectionState == ConnectionState.waiting
? const SplashScreen()
: authState.displayedOnboard
? const LoginPage()
: const OnboardScreen(),

Thats very simple right?

to complete it, let's see each page.

splash_screen.dart

adjust as your spin animation.

class SplasScreen extends StatelessWidget {
const SplasScreen({super.key});

@override
Widget build(BuildContext context) {
return const Center(child: CircularProgressIndicator());
}
}

onboard_page.dart

Make sure, after onboarding is complete, save it to local storage indicating the user has seen the welcoming page.

class OnboardScreen extends StatefulWidget {
const OnboardScreen({super.key});
....

@override
Widget build(BuildContext context) {
// your Onboard UI here
TextButton(
child: const Text("done"),
onPressed: () async {
final preferences= await SharedPreferences.getInstance();
await preferences.setBool('showOnboard', true);
}

login_page.dart

call your login function and don’t forget to save the user info and token to local storage. We will use it for automatic login.

class LoginPage extends StatefulWidget {
const LoginPage({super.key});
....

@override
Widget build(BuildContext context) {
// your LOGIN UI here
ElevatedButton(
child: const Text("LOGIN"),
onPressed: () async {
// your login funtion invoke here

// set userinfo if sucess login.
// this will rebuild the consumer in main.dart
// and navigate to homescreen
context.read<AuthState>().setUser(youruserinfo);


// then save the user info
final preferences= await SharedPreferences.getInstance();
await preferences.setString('userInfo', "userData");
}

then last but not least, you can use the auth state in any pages you need. for example

home_page.dart

class HomePage extends StatelessWidget {
const HomePage({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
body: Text("Username: ${context.read<AuthState>().userInfo.username}"),
);
}
}

or for logout like below

profile_page.dart

class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
child: const Text("Logout"),
onPressed: () async {
// call your logout function

// set userinfo to null, will rebuild the consumer in main.dart
context.read<AuthState>().logout();

// then clear the user info from local storage
final preferences = await SharedPreferences.getInstance();
await preferences.remove('userInfo');
}),
);
}
}

you could check all your pages if it's still authorized or not with the code below. it will check if the user info in the app state has a valid token or not.

bool isLoggedIn = context.read<AuthState>().isAuthorized

Thank you for having the end. Don’t forget to clap 👏 if you like this article. 😃

Reach me out on X: Matatias Situmorang (@pmatatias_) / X (twitter.com)

--

--

Matatias Situmorang

Flutter developer | Blogger | Reach me on twitter @pmatatias_