# Flutter Authentication Flow with Go Router and Provider

## Goal of this article
The goal of this article is to get a comprehensive understanding of the flutter declarative routing and the scenarios like login state, app initialization on startup, splash, onboarding, etc... 

## Problems that I'm trying to solve
assume we have a flutter application that we have to handle those situations.
- We need to show the onboarding page when the user opens the app for the first time.
- We need to execute some initialisation functions every time the app opens.
- The application should be capable of kicking out the user anywhere in the application when the login state changes (log out).
- And also app should be capable of handling the deep links regardless of login state.


![omg.webp](https://cdn.hashnode.com/res/hashnode/image/upload/v1639994698289/KrAZ9VFCM.webp)

I know, It looks like a lot. but stay with me I'm going to cover it all in this article

## Starting the project

First of all, let's create the application. assuming that you have already installed flutter (I use the currently available latest version **v2.8.0**) Create a folder and inside it run this command,
```bash
flutter create --template app --overwrite.
```
I'm sure you are using vs-code or android studio for this so you don't have to create an app manually. if you do just ignore the above command and create the app from the IDE.

throughout the project, I'm going to run the application on the web. since all plugging, I'm using here is platform-independent you can use any platform you like it's totally up to you.

Okay, Let's add plugging to the `pubspec.yaml` under `dependencies`

- [provider: ^6.0.1](https://pub.dev/packages/provider)
> we will be using the provider package to do all the state management in our application.
- [go_router: ^2.5.5](https://gorouter.dev)
> The purpose of the go_router package is to use declarative routes to reduce complexity, regardless of the platform you're targeting (mobile, web, desktop), handle deep and dynamic linking from Android, iOS and the web, along with several other navigation-related scenarios, while still (hopefully) providing an easy-to-use developer experience.
- [shared_preferences: ^2.0.11](https://pub.dev/packages/shared_preferences)
> will be used to store some persistent data that we have kept in our application.

That's it, that's all the package we are going to use, run `flutter pub get` to get the packages.
```bash
flutter pub get
```
## Let's start the coding

Open the `main.dart` file and remove everything and create the main function like this.
```dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
  runApp(MyApp(sharedPreferences: sharedPreferences));
}
```
then create the `MyApp` class by extending the `StatefulWidget` because we need `initState` inside this widget.
```dart
class MyApp extends StatefulWidget {
  final SharedPreferences sharedPreferences;
  const MyApp({
    Key? key,
    required this.sharedPreferences,
  }) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return Container(
      
    );
  }
}
```
Okay, then let's create some files for the classes,
take a close look and complete the appropriate files and folders for the next step.

![flutter_01.PNG](https://cdn.hashnode.com/res/hashnode/image/upload/v1640005651355/GXCqdsslq.png)

## Route Utils
Let's create `enum` class inside the `route_utils.dart` file that contain all the pages that our application has.
```dart
enum APP_PAGE {
  splash,
  login,
  home,
  error,
  onBoarding
}
```
Then we will add `extension` to the `APP_PAGE` for purpose convinent.
```dart
extension AppPageExtension on APP_PAGE {
  String get toPath {
    switch (this) {
      case APP_PAGE.home:
        return "/";
      case APP_PAGE.login:
        return "/login";
      case APP_PAGE.splash:
        return "/splash";
      case APP_PAGE.error:
        return "/error";
      case APP_PAGE.onBoarding:
        return "/start";
      default:
        return "/";
    }
  }

  String get toName {
    switch (this) {
      case APP_PAGE.home:
        return "HOME";
      case APP_PAGE.login:
        return "LOGIN";
      case APP_PAGE.splash:
        return "SPLASH";
      case APP_PAGE.error:
        return "ERROR";
      case APP_PAGE.onBoarding:
        return "START";
      default:
        return "HOME";
    }
  }

  String get toTitle {
    switch (this) {
      case APP_PAGE.home:
        return "My App";
      case APP_PAGE.login:
        return "My App Log In";
      case APP_PAGE.splash:
        return "My App Splash";
      case APP_PAGE.error:
        return "My App Error";
      case APP_PAGE.onBoarding:
        return "Welcome to My App";
      default:
        return "My App";
    }
  }
}
```
Next, we will create `StatelessWidget` inside every view file except `splash_page.dart` file, inside that create a `StatefulWidget`,

then, let's create the `AppService` class,

```dart
// ignore: non_constant_identifier_names
String LOGIN_KEY = "5FD6G46SDF4GD64F1VG9SD68";
// ignore: non_constant_identifier_names
String ONBOARD_KEY = "GD2G82CG9G82VDFGVD22DVG";

class AppService with ChangeNotifier {
  late final SharedPreferences sharedPreferences;
  final StreamController<bool> _loginStateChange = StreamController<bool>.broadcast();
  bool _loginState = false;
  bool _initialized = false;
  bool _onboarding = false;

  AppService(this.sharedPreferences);

  bool get loginState => _loginState;
  bool get initialized => _initialized;
  bool get onboarding => _onboarding;
  Stream<bool> get loginStateChange => _loginStateChange.stream;

  set loginState(bool state) {
    sharedPreferences.setBool(LOGIN_KEY, state);
    _loginState = state;
    _loginStateChange.add(state);
    notifyListeners();
  }

  set initialized(bool value) {
    _initialized = value;
    notifyListeners();
  }

  set onboarding(bool value) {
    sharedPreferences.setBool(ONBOARD_KEY, value);
    _onboarding = value;
    notifyListeners();
  }

  Future<void> onAppStart() async {
    _onboarding = sharedPreferences.getBool(ONBOARD_KEY) ?? false;
    _loginState = sharedPreferences.getBool(LOGIN_KEY) ?? false;

    // This is just to demonstrate the splash screen is working.
    // In real-life applications, it is not recommended to interrupt the user experience by doing such things.
    await Future.delayed(const Duration(seconds: 2));

    _initialized = true;
    notifyListeners();
  }
}
```
This is our `AppService` class, we create it with `ChangeNotifier` so we can listen for the state changes using `Provider`. 

`SharedPreferences ` instance that we created earlier will use in this class. and there are several values to keep track of the different states of the application.

While the `initialized` value only lives through the app life circle, `loginState ` and `onboarding` values will be stored in local storage, because those values need to be kept even if the app is entirely closed. the `onAppStart` method is responsible for reading those values from local storage and finishing the app initialization. we will be showing the `Splash` screen until the `onAppStart` future is completed.

Ok, now we can define the routers, I'm going to do it by using Go Router if you're not familiar with its basic concepts first of all you should take a look at its [documentation](https://gorouter.dev).

## Let's start routing

![hooray.webp](https://cdn.hashnode.com/res/hashnode/image/upload/v1640024846341/eN_eU20fs.webp)

we need to create the `AppRouter` class which contains all the routing information in our application.

```dart
 class AppRouter {
  late final AppService appService;
  GoRouter get router => _goRouter;

  AppRouter(this.appService);

  late final GoRouter _goRouter = GoRouter(
    refreshListenable: appService,
    initialLocation: APP_PAGE.home.toPath,
    routes: <GoRoute>[
      GoRoute(
        path: APP_PAGE.home.toPath,
        name: APP_PAGE.home.toName,
        builder: (context, state) => const HomePage(),
      ),
      GoRoute(
        path: APP_PAGE.splash.toPath,
        name: APP_PAGE.splash.toName,
        builder: (context, state) => const SplashPage(),
      ),
      GoRoute(
        path: APP_PAGE.login.toPath,
        name: APP_PAGE.login.toName,
        builder: (context, state) => const LogInPage(),
      ),
      GoRoute(
        path: APP_PAGE.onBoarding.toPath,
        name: APP_PAGE.onBoarding.toName,
        builder: (context, state) => const OnBoardingPage(),
      ),
      GoRoute(
        path: APP_PAGE.error.toPath,
        name: APP_PAGE.error.toName,
        builder: (context, state) => ErrorPage(error: state.extra.toString()),
      ),
    ],
    errorBuilder: (context, state) => ErrorPage(error: state.error.toString()),
    redirect: (state) {
      final loginLocation = state.namedLocation(APP_PAGE.login.toPath);
      final homeLocation = state.namedLocation(APP_PAGE.home.toPath);
      final splashLocation = state.namedLocation(APP_PAGE.splash.toPath);
      final onboardLocation = state.namedLocation(APP_PAGE.onBoarding.toPath);

      final isLogedIn = appService.loginState;
      final isInitialized = appService.initialized;
      final isOnboarded = appService.onboarding;

      final isGoingToLogin = state.subloc == loginLocation;
      final isGoingToInit = state.subloc == splashLocation;
      final isGoingToOnboard = state.subloc == onboardLocation;

      // If not Initialized and not going to Initialized redirect to Splash
      if (!isInitialized && !isGoingToInit) {
        return splashLocation;
      // If not onboard and not going to onboard redirect to OnBoarding
      } else if (isInitialized && !isOnboarded && !isGoingToOnboard) {
        return onboardLocation;
      // If not logedin and not going to login redirect to Login
      } else if (isInitialized && isOnboarded && !isLogedIn && !isGoingToLogin) {
        return loginLocation;
      // If all the scenarios are cleared but still going to any of that screen redirect to Home
      } else if ((isLogedIn && isGoingToLogin) || (isInitialized && isGoingToInit) || (isOnboarded && isGoingToOnboard)) {
        return homeLocation;
      } else {
      // Else Don't do anything
        return null;
      }
    },
  );
```

Whoa, whoa what the fudge is going on here?

![lay-down.webp](https://cdn.hashnode.com/res/hashnode/image/upload/v1640029871144/JUPaQWCJF.webp)

It looks like too much information to take but don't worry I'm going to break down it for you.
```dart
late final AppService appService;
GoRouter get router => _goRouter;
```
> Here we define the `AppService` object and the `GoRouter` object, Since the `AppService` is listenable we can listen for the state changes and rebuild the router delegate according to the app states. 

```dart
refreshListenable: appService,
initialLocation: APP_PAGE.home.toPath,
```
> `refreshListenable` is that we define a listenable class to go router to react to changes. and the `initialLocation` is the default location that the app will redirect when there is no other path when the app starts (like deep links and dynamic links).

```dart
GoRoute(
  path: APP_PAGE.home.toPath,
  name: APP_PAGE.home.toName,
  builder: (context, state) => const HomePage(),
),
```
> Here are all the predefined locations in our application. you can see that earlier we create `APP_PAGE` enum class and an extension for it. it is super convenient to define the path, and name when we have such a class that's why we created it.

> If you take a closer look you will see we use `ErrorPage` on two different locations `routers` and `errorBuilder`. one inside the `routers`, we will use for manually navigate to the error page when there is a critical error in our application. the `errorBuilder` will use by Go Router to throw exceptions when there is something like an undefined navigation path.

> one thing that I forgot to mention, there is a String argument on the error page called `error` which we will use to show an error to the user. when `errorBuilder` is triggered, always exception will be stored in the `state.error` object, when we navigate to the error page manually we will carry the error string inside the `state.extra` object. more information about it [here](https://gorouter.dev/parameters#extra-parameter).

## Redirects

This method is the most important part of our application. it will handle all the core routing logic of the application, let's break down the block by block for better understanding.

```dart
final loginLocation = state.namedLocation(APP_PAGE.login.toName);
final homeLocation = state.namedLocation(APP_PAGE.home.toName);
final splashLocation = state.namedLocation(APP_PAGE.splash.toName);
final onboardLocation = state.namedLocation(APP_PAGE.onBoarding.toName);
```
> Here we get all the locations from the router so we know where to redirect in different scenarios. 

```dart
final isLogedIn = appService.loginState;
final isInitialized = appService.initialized;
final isOnboarded = appService.onboarding;
```
> Then we get all the current states in our application from `appService` so we know what to do next when we redirect.

```dart
final isGoingToLogin = state.subloc == loginLocation;
final isGoingToInit = state.subloc == splashLocation;
final isGoingToOnboard = state.subloc == onboardLocation;
```

> Finally we get where we actually go so we know when to redirect or not.

See it's not that complicated

![complicated.webp](https://cdn.hashnode.com/res/hashnode/image/upload/v1640034664217/afbmDszO4.webp)

Ok, ok, the next part is quite complicated, let's break it down,

We know that there are two cases where the redirect method called
- When we navigate to another location.
- When `appService` object changes.
> because the router is always listing for its changes when it happens router will be rebuilt.

assume the app is starting for the first time, the sub-location of the router will be `initialLocation` which is home `/`, so this is the first case that I mentioned above the redirect method will be called, after collecting all the information we are in the `if` statement. so it will check app is `initialized` or not, obviously it is false. because we are stating the app and also we are not going to splash screen. so the statement will be valid we will return  **Splash Screen Location** `/splash`. when we do that unintentionally also triggered the **first case** because the navigator location has changed. so the redirect method will be called again but this time `isInitialized` is still false but `isGoingToInit` is true because we are going to splash the screen. so along with the if statement all other conditions will be false. we just let go of the navigator to do its thing without changing anything. so at this point,

> there is a three-state value of this application, and any of that is not changed yet.

> app is just going to the **splash screen**

> and already redirect method is called **3 times**

those are the main three-point I need you to understand here. if you didn't please repeat the above paragraph again until you do.

before we start the splash screen we have to do one thing,

## Adding Provider and Router to the App

```dart
class _MyAppState extends State<MyApp> {
  late AppService appService;

  @override
  void initState() {
    appService = AppService(widget.sharedPreferences);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<AppService>(create: (_) => appService),
        Provider<AppRouter>(create: (_) => AppRouter(appService)),
      ],
      child: Builder(
        builder: (context) {
          final GoRouter goRouter = Provider.of<AppRouter>(context, listen: false).router;
          return MaterialApp.router(
            title: "Router App",
            routeInformationParser: goRouter.routeInformationParser,
            routerDelegate: goRouter.routerDelegate,
          );
        },
      ),
    );
  }
} 
```
You can see in the above code we have created `appService` object and initialized it inside `initState` function with `sharedPreferences` instance. After that we have to wrap `MaterialApp` with `MultiProvider` and then add `appService` as `ChangeNotifierProvider` also we will declare `AppRouter` with `appService` as a provider too. finally we can add `routeInformationParser` and `routerDelegate` to the `MaterialApp.router` by getting it from `context`. We have not created `AuthService` yet, it will be added later.

Okay, back to the splash screen

## Splash Screen
There is one thing we have to do in the splash screen which is we need to call the `onAppStart` function.

```dart
class SplashPage extends StatefulWidget {
  const SplashPage({ Key? key }) : super(key: key);

  @override
  _SplashPageState createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  late AppService _appService;

  @override
  void initState() {
    _appService = Provider.of<AppService>(context, listen: false);
    onStartUp();
    super.initState();
  }

  void onStartUp() async {
    await _appService.onAppStart();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(APP_PAGE.splash.toTitle),
      ),
      body: const Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}
```
once we are on the splash screen `onAppStart` function will be triggered, so it will read `_onboarding` and `_loginState` values from the locale storage and will change `_initialized` to `true` then will call `notifyListeners` as soon as that happens, the router will be **re**built and will **re**call the `redirect` function.

oh god, that's too much **"re"** 😂

![facepalm-seriously.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1640189327132/8zxVWiZtP.gif)

Okay, back to the **redirect** function

At this point `isInitialized` is true and other values will be false so obviously, it will redirect to the `onboardLocation` and inside the `OnBoardingPage` user will press the `Done` button and we will change `_onboarding` to true, it pretty much the same thing I think you can now understand the `redirect` function logics. Let's move on to the `AuthService`

## Authentication Service

The reason why we are creating a separate `AuthService` class while tricking the login state inside the `AppService`, it will be so clean and understandable to implement authentication logic in a separate class and just expose the login state by an event stream. if you have experience with FirebaseAuth you will easily understand what I'm doing here,

```dart
class AuthService {
  final StreamController<bool> _onAuthStateChange = StreamController.broadcast();

  Stream<bool> get onAuthStateChange => _onAuthStateChange.stream;

  Future<bool> login() async {

    // This is just to demonstrate the login process time.
    // In real-life applications, it is not recommended to interrupt the user experience by doing such things.
    await Future.delayed(const Duration(seconds: 1));

    _onAuthStateChange.add(true);
    return true;
  }

  void logOut() {
    _onAuthStateChange.add(false);
  }
}
```
ok, now it's time to update the `main.dart` with `AuthService` we will declare it in the `initState` and add a listener to change the `loginState`.
 
```dart
class _MyAppState extends State<MyApp> {
  late AppService appService;
  late AuthService authService;
  late StreamSubscription<bool> authSubscription;

  @override
  void initState() {
    appService = AppService(widget.sharedPreferences);
    authService = AuthService();
    authSubscription = authService.onAuthStateChange.listen(onAuthStateChange);
    super.initState();
  }

  void onAuthStateChange(bool login) {
    appService.loginState = login;
  }

  @override
  void dispose() {
    authSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<AppService>(create: (_) => appService),
        Provider<AppRouter>(create: (_) => AppRouter(appService)),
        Provider<AuthService>(create: (_) => authService),
      ],
      child: Builder(
        builder: (context) {
          final GoRouter goRouter = Provider.of<AppRouter>(context, listen: false).router;
          return MaterialApp.router(
            title: "Router App",
            routeInformationParser: goRouter.routeInformationParser,
            routerDelegate: goRouter.routerDelegate,
          );
        },
      ),
    );
  }
}
```
Finally, we have finished almost all the coding now,

![tired-sleepy.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1640193876165/s07zsjfoT.gif)

we just need to add some buttons and run the application. I'll just add the home page code here the full source code will be available on GitHub.

```dart
class HomePage extends StatelessWidget {
  const HomePage({ Key? key }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final authService = Provider.of<AuthService>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(APP_PAGE.home.toTitle),
      ),
      body: Center(
        child: Column(
          children: [
            TextButton(
              onPressed: () {
                authService.logOut();
              },
              child: const Text(
                "Log out"
              ),
            ),
            TextButton(
              onPressed: () {
                GoRouter.of(context).goNamed(APP_PAGE.error.toName, extra: "Erro from Home");
              },
              child: const Text(
                "Show Error"
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```
Okay, let's run the application,

![2021-12-22 23-37-14.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1640197034023/3wcsHcIZR.gif)

You will clearly see everything is working as expected.

![five.webp](https://cdn.hashnode.com/res/hashnode/image/upload/v1640195701502/Vpke2_6lq.webp)

Okay, I'm going to wrap up the article here. since it is already too long, Handling **Deep Link** will be discussed in a separate article later. The link will be updated.

I know this is not the most advanced way to do it. But I think it is good, at what it is capable of. Feel free to share your thoughts in the comment section. Don't hesitate to ask questions I will help you. Don't forget to like, and share the article with your Flutter friends.

[Github Repository](https://github.com/ishangavidusha/flutter-go-router-example)

Happy coding.


![byeeee.webp](https://cdn.hashnode.com/res/hashnode/image/upload/v1640200831163/mm_iJq9ql.webp)

