Onboarding With Go Router in Flutter
Published: 2022-04-14 19:24:27

## About 

Hi, welcome to the 2<sup>nd</sup> part of the flutter app development series. In the last [part](https://dev.to/kcl/create-splash-screen-and-launch-icon-in-flutter-1kdg), we successfully created a flutter starter app and set up launch Icon and Splash Screens for our 'Astha - Being Hindu' app. So far our app has a custom icon and a splash screen shows up when we launch our app. In this section of the tutorial, we will make an onboarding screen and apply it to our app. For that,  we'll install [GoRouter](https://pub.dev/packages/go_router), [Provider](https://pub.dev/packages/provider), and [Shared_Preferences](https://pub.dev/packages/shared_preferences) packages for our app.

### To Onboard Or Not 
Let's again have a look at the user-screen flow from the image below.
![App Launch.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1648189137341/YEfWDZHId.png)

Let's go over the routing logic we'll be using for onboarding. 

__Has the user/app been onboarded?__

-> If yes, then no need to onboard until the lifetime of the application i.e until the app is uninstalled.

-> If no, then go to the onboarding screen, just once and then never in the app's lifetime.

__So, how are we going to achieve this?__

It is simple, you see go router has [redirect](https://gorouter.dev/redirection) option available, where the __state__ property of redirect,  can be used to write check statements, to inquire about current routes: where is it now, where is it heading, and such. With this, we can redirect to onboard or not.

__That's great but what/how will we check?__

That's where shared preferences come in. We can store simple data in local storage using shared preferences. So during app initialization:
1.  We'll fetch an integer/key stored in local storage, that's responsible to keep count of onboard state, via shared preferences.

2.  When the app is launched for the first time, the shared preferences will return null because the integer does not exist yet. For us, it's equivalent to the idea of never having been onboarded. 

3. After that on the router we'll check if the integer is null, if so then go to the onboard screen.

4. When onboarding is done, here we'll finally set that non-existent integer to a non-null value and save it in the local storage using the Provider package.

5. Now when we launch an app again for the second time, the router will find that the integer is not a null value anymore, so it'll redirect to our next page instead of the onboard screen.


## Install Packages
You can find the code of the project up until now in this [repository](https://github.com/nibukdk/astha-tutorial/tree/master/part_one). So, let's go to our projects pubspec.yaml file and add the following packages.

```
dependencies:
  flutter:
    sdk: flutter
  flutter_native_splash: ^2.1.1
# Our new pacakges
  go_router: ^3.0.5
  shared_preferences: ^2.0.13
  provider: ^6.0.2
``` 



## Reformat 

Before we start doing things, let's do some changes to our project. First, we'll create some files and folders. 

```
#Your cursor should be inside lib your lib folder
# make some folder
mkdir globals screens  globals/providers globals/settings  globals/settings/router globals/settings/router/utils  screens/onboard  screens/home

# make some files
touch app.dart globals/providers/app_state_provider.dart  globals/settings/router/app_router.dart  globals/settings/router/utils/router_utils.dart screens/onboard/onboard_screen.dart  screens/home/home.dart
``` 

 The main.dart file is like this now.
 
```
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main()  {
  //  concrete binding for applications based on the Widgets framewor
  WidgetsFlutterBinding.ensureInitialized();

  SystemChrome.setSystemUIOverlayStyle(
    SystemUiOverlayStyle.dark.copyWith(statusBarColor: Colors.black38),
  );

  runApp(const Home());
}

class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(child: Text("Home Page")),
      ),
    );
  }
}
``` 
Then split the main.dart file and move some of its content to the home.dart file. Let's remove the Home class from the main to home.dart file. Now our files look like this: 

__main.dart__

```
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:temple/screens/home/home.dart';

void main()  {
  //  concrete binding for applications based on the Widgets framewor
  WidgetsFlutterBinding.ensureInitialized();

  SystemChrome.setSystemUIOverlayStyle(
    SystemUiOverlayStyle.dark.copyWith(statusBarColor: Colors.black38),
  );

  runApp(const Home());
}

``` 

__home.dart__

```
import 'package:flutter/material.dart';

class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(child: Text("Home Page")),
      ),
    );
  }
}

```

### Router Utils
While routing we'll need to provide several properties like Router Path, Named Route, Page Title, and such. It will be efficient if these values can be outsourced from a module. Hence, we created __utils/router_utils.dart__ file.

__router_utils.dart__
```
// Create enum to represent different routes
enum APP_PAGE {
  onboard,
  auth,
  home,
}


extension AppPageExtension on APP_PAGE {
  // create path for routes
  String get routePath {
    switch (this) {
      case APP_PAGE.home:
        return "/";

      case APP_PAGE.onboard:
        return "/onboard";

      case APP_PAGE.auth:
        return "/auth";

      default:
        return "/";
    }
  }

// for named routes
  String get routeName {
    switch (this) {
      case APP_PAGE.home:
        return "HOME";

      case APP_PAGE.onboard:
        return "ONBOARD";

      case APP_PAGE.auth:
        return "AUTH";

      default:
        return "HOME";
    }
  }

// for page titles to use on appbar
  String get routePageTitle {
    switch (this) {
      case APP_PAGE.home:
        return "Astha";

      default:
        return "Astha";
    }
  }
}


``` 
### Go Router  

Finally, we can go to the router file where we'll create routes and redirect logic. So, on __app_router.dart__ file.

#### Create an [AppRouter](https://gorouter.dev/declarative-routing) class. 

```
import 'package:go_router/go_router.dart';
import 'package:temple/screens/home/home.dart';
import 'utils/router_utils.dart';

class AppRouter  {
  get router => _router;

  final _router = GoRouter(
      initialLocation: "/",
      routes: [
        GoRoute(
          path: APP_PAGE.home.routePath,
          name: APP_PAGE.home.routeName,
          builder: (context, state) => const Home(),
        ),
      ],
      redirect: (state) {});
}

``` 
The AppRouter is a router class we'll use as a [provider](https://pub.dev/documentation/provider/latest/provider/Provider-class.html). The router we just created now has one router __"/"__ which is the route to our home page. Likewise, the __initialLocation__  property tells the router to go to the homepage immediately after the app starts. But, if some conditions are met then, it can be redirected to somewhere else, which is done through __redirect__.  However, we have yet to implement our router. To do so let's head to the app.dart file.


#### Use Router instead of Navigator.

MaterialApp.Router creates a MaterialApp that uses the Router instead of a Navigator. Check out the differences [here](https://docs.flutter.dev/development/ui/navigation). We'll need to use the declarative way for our go_router.  

__app.dart__

```
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:temple/globals/settings/router/app_router.dart';

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider(create: (context) => AppRouter()),
      ],
      child: Builder(
        builder: ((context) {
          final GoRouter router = Provider.of<AppRouter>(context).router;

          return MaterialApp.router(
              routeInformationParser: router.routeInformationParser,
              routerDelegate: router.routerDelegate);
        }),
      ),
    );
  }
}

``` 

MyApp class will be the parent class for our app i.e class used in runApp(). Hence, this is where we'll use a router. Moreover, we are returning [MultiProvider](https://pub.dev/documentation/provider/latest/provider/MultiProvider-class.html), because as the app grows we'll use many other providers.

As mentioned before we need to pass the MyApp class in runApp() method in our main.dart file.

```
// Insde main() method
void main() {
............
// Only change this line 
  runApp(const MyApp());
//
}
``` 
Now save all the files and run the app in your emulator. You'll see a homepage that'll look like this.

![Home Page Initial](https://cdn.hashnode.com/res/hashnode/image/upload/v1648280485381/0wb2c6wiE.png)



### Provider

We'll be writing our logic about the onboard status on the provider class, and since, it's a global state we'll write it on the __app_state_provider.dart__ file inside the "__lib/globals/providers__" folder.

__app_state_provider.dart__

```
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';

class AppStateProvider with ChangeNotifier {

// lets define a method to check and manipulate onboard status
  void hasOnboarded() async {
    // Get the SharedPreferences instance
    SharedPreferences prefs = await SharedPreferences.getInstance();
    // set the onBoardCount to 1
    await prefs.setInt('onBoardCount', 1);
    // Notify listener provides converted value to all it listeneres
    notifyListeners();
  }
}


```
Inside hasOnboarded() function, we set the integer of onBoardCount to one or non-null value, like mentioned previously.



Now, do you know how to implement this provider in our app?. Yes, we'll need to add another provider to the app.dart's MultiProvider.

__app.dart__

```
import 'package:temple/globals/providers/app_state_provider.dart';

....
  .....
MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => AppStateProvider()),
        Provider(create: (context) => AppRouter())
 ]

``` 
Make sure to declare AppStateProvider before AppRouter, which we'll discuss later. For now, we'll make a very simple onboard screen for testing purposes. 

### Onboard Screen
__onboard_screen.dart__

```
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:temple/globals/providers/app_state_provider.dart';

class OnBoardScreen extends StatefulWidget {
  const OnBoardScreen({Key? key}) : super(key: key);

  @override
  State<OnBoardScreen> createState() => _OnBoardScreenState();
}

void onSubmitDone(AppStateProvider stateProvider, BuildContext context) {
  // When user pressed skip/done button we'll finally set onboardCount integer
  stateProvider.hasOnboarded();
  // After that onboard state is done we'll go to homepage.
  GoRouter.of(context).go("/");
}

class _OnBoardScreenState extends State<OnBoardScreen> {
  @override
  Widget build(BuildContext context) {
    final appStateProvider = Provider.of<AppStateProvider>(context);
    return Scaffold(
      body: Center(
          child: Column(
        children: [
          const Text("This is Onboard Screen"),
          ElevatedButton(
              onPressed: () => onSubmitDone(appStateProvider, context),
              child: const Text("Done/Skip"))
        ],
      )),
    );
  }
}

``` 
In this file, a stateful widget class was created. The main thing to notice here for now is __onSubmitDone()__ function. This function we'll be called when the user either pressed the *skip* button during onboarding or the *done* button when onboarding is done. Here, it calls the *hasOnboarded* method we defined earlier in the provider which sets things in motion. 
After that, our router will take us to the homepage. 

Now we're done!, or Are we? We still haven't introduced redirect instructions to our router. Hence, let's make some changes to our app router.

### Go-Router Redirect

__app_router.dart__

```
// Packages
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';
//Custom files
import 'package:temple/screens/home/home.dart';
import 'utils/router_utils.dart';
import 'package:temple/screens/onboard/onboard_screen.dart';
import 'package:temple/globals/providers/app_state_provider.dart';

class AppRouter {
  //=======================change #1 start ===========/
  AppRouter({
    required this.appStateProvider,
    required this.prefs,
  });

  AppStateProvider appStateProvider;
  late SharedPreferences prefs;
  //=======================change #1 end===========/
  get router => _router;

// change final to late final to use prefs inside redirect.
  late final _router = GoRouter(
      refreshListenable:
          appStateProvider, //=======================change #2===========/
      initialLocation: "/",
      routes: [
        GoRoute(
          path: APP_PAGE.home.routePath,
          name: APP_PAGE.home.routeName,
          builder: (context, state) => const Home(),
        ),
        // Add the onboard Screen
        //=======================change #3  start===========/

        GoRoute(
            path: APP_PAGE.onboard.routePath,
            name: APP_PAGE.onboard.routeName,
            builder: (context, state) => const OnBoardScreen()),
        //=======================change #3  end===========/
      ],
      redirect: (state) {
        //=======================change #4  start===========/

        // define the named path of onboard screen
        final String onboardPath =
            state.namedLocation(APP_PAGE.onboard.routeName); //#4.1

        // Checking if current path is onboarding or not
        bool isOnboarding = state.subloc == onboardPath; //#4.2

        // check if sharedPref as onBoardCount key or not
        //if is does then we won't onboard else we will
        bool toOnboard =
            prefs.containsKey('onBoardCount') ? false : true; //#4.3

        //#4.4
        if (toOnboard) {
          // return null if the current location is already OnboardScreen to prevent looping
          return isOnboarding ? null : onboardPath;
        }
        // returning null will tell router to don't mind redirect section
        return null; //#4.5
        //=======================change #4  end===========/
      });
}


``` 

Let's go through the changes we made. 
1. We created two class files: appStateRouter and prefs. The SharedPrefences instance *prefs* is needed to check whether we have already onboarded or not, based on the existence of the onboard count integer. The appStateProvider will provide all the changes that matter to the router. 

2. The router property [refreshListenableTo](https://gorouter.dev/redirection#top-level-redirection) is set to listen to the changes from appStateProvider.

3. We added the OnBoardScreen route to the routes list.

4.  Here we,
    1. First created a named location for the onboard screen. 

    2. Then *isOnboarding* checks whether the current route(state.subloc) is headed towards the onboard screen or not.

    3.  If local storage already has __onBoardCount__ integer then we'll not onboard else we will.

    4.  Based on the value of *toOnboard* we'll return either null or onBoardPath to redirect towards. We checked if the current route is going towards __onBoardScreen__ with *isOboarding* and if so returned null. We need to do this, if we don't then the router will enter a loop and cause an error.

    5. Lastly, if we don't have to redirect anywhere then return null which tells the router to ignore redirect for now.

(PS: I haven't mentioned changes in import.)

### Proxy Provider For Router 

Alright, so at this point, you probably have your linter screaming errors with red colors. It's the result of us declaring two fields in AppRouter, yet we aren't providing their values in our app.dart. So, let's fix it.

__app.dart__


```
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:temple/globals/providers/app_state_provider.dart';
import 'package:temple/globals/settings/router/app_router.dart';

class MyApp extends StatefulWidget {
// Declared fields prefs which we will pass to the router class
  //=======================change #1==========/
  SharedPreferences prefs; 
  MyApp({required this.prefs, Key? key}) : super(key: key);
  //=======================change #1 end===========/
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => AppStateProvider()),
        //=======================change #2==========/
        // Remove previous Provider call and create new proxyprovider that depends on AppStateProvider
        ProxyProvider<AppStateProvider, AppRouter>(
            update: (context, appStateProvider, _) => AppRouter(
                appStateProvider: appStateProvider, prefs: widget.prefs))
      ],
      //=======================change #2 end==========/
      child: Builder(
        builder: ((context) {
          final GoRouter router = Provider.of<AppRouter>(context).router;

          return MaterialApp.router(
              routeInformationParser: router.routeInformationParser,
              routerDelegate: router.routerDelegate);
        }),
      ),
    );
  }
}

``` 
1. First, create a field prefs, which needs to pass it as the value on our __main.dart__ file which is where we call MyApp class.
2. We'll create a [Proxy](https://pub.dev/documentation/provider/latest/provider/ProxyProvider-class.html) AppRouter provider, that'll depend on the AppStateProvider. The proxy provider is not the only way to pass a value. 

__main.dart__

Now, there is another red warning, because we have yet to pass our prefs field in the main.dart file. 


```
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:temple/app.dart';

//=======================change #1==========/
// make app an async funtion to instantiate shared preferences
void main() async {
  //  concrete binding for applications based on the Widgets framewor
  WidgetsFlutterBinding.ensureInitialized();

  //=======================change #2==========/
// Instantiate shared pref
  SharedPreferences prefs = await SharedPreferences.getInstance();
  SystemChrome.setSystemUIOverlayStyle(
    SystemUiOverlayStyle.dark.copyWith(statusBarColor: Colors.black38),
  );
  //=======================change #3==========/
// Pass prefs as value in MyApp
  runApp(MyApp(prefs: prefs));
}
``` 
Here we simply converted the __main()__ method to an async method. We did it to instantiate the shared preferences, which then is passed as value for MyApp class's prefs field. Now, when you run the app, it should work as intended.

## OnBoardScreen UI & Animation

Now, that we've made the functionality work. Let's do something about the onboard screen itself. You can download the following images or use your own. I created them at [canva](https://canva.com/) for free.
 

![Onboard Screen Image 1](https://cdn.hashnode.com/res/hashnode/image/upload/v1648354738966/21RIl6dY7.png)

![Onboard Screen Image2](https://cdn.hashnode.com/res/hashnode/image/upload/v1648354767226/7fRp6vnw1.png)

__onboard_screen.dart__


```
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:temple/globals/providers/app_state_provider.dart';

class OnBoardScreen extends StatefulWidget {
  const OnBoardScreen({Key? key}) : super(key: key);

  @override
  State<OnBoardScreen> createState() => _OnBoardScreenState();
}

void onSubmitDone(AppStateProvider stateProvider, BuildContext context) {
  // When user pressed skip/done button we'll finally set onboardCount integer
  stateProvider.hasOnboarded();
  // After that onboard state is done we'll go to homepage.
  GoRouter.of(context).go("/");
}

class _OnBoardScreenState extends State<OnBoardScreen> {
// Create a private index to track image index
  int _currentImgIndex = 0; // #1

// Create list with images to use while onboarding
  // #2
  final onBoardScreenImages = [
    "assets/onboard/FindTemples.png",
    "assets/onboard/FindVenues.png",
    "assets/onboard/FindTemples.png",
    "assets/onboard/FindVenues.png",
  ];

// Function to display next image in the list when next button  is clicked
  // #4
  void nextImage() {
    if (_currentImgIndex < onBoardScreenImages.length - 1) {
      setState(() => _currentImgIndex += 1);
    }
  }

// Function to display previous image in the list when previous button  is clicked
  // #3
  void prevImage() {
    if (_currentImgIndex > 0) {
      setState(() => _currentImgIndex -= 1);
    }
  }

  @override
  Widget build(BuildContext context) {
    final appStateProvider = Provider.of<AppStateProvider>(context);
    return Scaffold(
        body: SafeArea(
            child: Container(
                color: const Color.fromARGB(255, 255, 209, 166),
                padding: const EdgeInsets.all(10.0),
                child: Column(
                  children: [
                    // Animated switcher class to animated between images
                    // #4
                    AnimatedSwitcher(
                      switchInCurve: Curves.easeInOut,
                      switchOutCurve: Curves.easeOut,
                      transitionBuilder: ((child, animation) =>
                          ScaleTransition(scale: animation, child: child)),
                      duration: const Duration(milliseconds: 800),
                      child: Image.asset(
                        onBoardScreenImages[_currentImgIndex],
                        height: MediaQuery.of(context).size.height * 0.8,
                        width: double.infinity,
                        // Key is needed since widget type is same i.e Image
                        key: ValueKey<int>(_currentImgIndex),
                      ),
                    ),
                    // Container to that contains set butotns
                    // #5
                    Container(
                        color: Colors.black26,
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            IconButton(
                              // Change visibility by currentImgIndex
                              // #6
                              onPressed: prevImage,
                              icon: _currentImgIndex == 0
                                  ? const Icon(null)
                                  : const Icon(Icons.arrow_back),
                            ),
                            IconButton(
                              // Change visibility by currentImgIndex
                              // #7
                              onPressed: _currentImgIndex ==
                                      onBoardScreenImages.length - 1
                                  ? () =>
                                      onSubmitDone(appStateProvider, context)
                                  : nextImage,
                              icon: _currentImgIndex ==
                                      onBoardScreenImages.length - 1
                                  ? const Icon(Icons.done)
                                  : const Icon(Icons.arrow_forward),
                            )
                          ],
                        ))
                  ],
                ))));
  }
}

``` 

I know it's a bit too much code. So, let's go through them a chunk at a time.

```
// Create a private index to track image index
  int _currentImgIndex = 0; // #1

// Create list with images to use while onboarding
  // #2
  final onBoardScreenImages = [
    "assets/onboard/FindTemples.png",
    "assets/onboard/FindVenues.png",
    "assets/onboard/FindTemples.png",
    "assets/onboard/FindVenues.png",
  ];
``` 
1. A private variable **_currentIndex**  is created to keep track of images.
2. Images in the list __onBoardScreenImages__, will be shown on the screen based on _currentIndex.


```
// Function to display next image in the list when next button  is clicked
  // #1
  void nextImage() {
    if (_currentImgIndex < onBoardScreenImages.length - 1) {
      setState(() => _currentImgIndex += 1);
    }
  }

// Function to display previous image in the list when previous button  is clicked
  // #2
  void prevImage() {
    if (_currentImgIndex > 0) {
      setState(() => _currentImgIndex -= 1);
    }
  }
``` 
These functions will keep track of currentIndex by managing the local state properly.


```
// Animated switcher class to animated between images
               AnimatedSwitcher(
                      switchInCurve: Curves.easeInOut,
                      switchOutCurve: Curves.easeOut,
                      transitionBuilder: ((child, animation) =>
                          ScaleTransition(scale: animation, child: child)),
                      duration: const Duration(milliseconds: 800),
                      child: Image.asset(
                        onBoardScreenImages[_currentImgIndex],
                        height: MediaQuery.of(context).size.height * 0.8,
                        width: double.infinity,
                         // Key is needed since widget type is same i.e Image
                        key: ValueKey<int>(_currentImgIndex),
                      ),
                    ),
``` 
We're using [AnimatedSwitcher](https://api.flutter.dev/flutter/widgets/AnimatedSwitcher-class.html) to switch between our image widgets while using [ScaleTransition](https://api.flutter.dev/flutter/widgets/ScaleTransition-class.html). BTW, if you remove the __transitionBuilder__ property you'll get the default FadeTransition.


```
Container(
                        color: Colors.black26,
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            IconButton(
                              // Change visibility by currentImgIndex
                              // #1
                              onPressed: prevImage,
                              icon: _currentImgIndex == 0
                                  ? const Icon(null)
                                  : const Icon(Icons.arrow_back),
                            ),
                            IconButton(
                              // Change visibility by currentImgIndex
                              // #2
                              onPressed: _currentImgIndex ==
                                      onBoardScreenImages.length - 1
                                  ? () =>
                                      onSubmitDone(appStateProvider, context)
                                  : nextImage,
                              icon: _currentImgIndex ==
                                      onBoardScreenImages.length - 1
                                  ? const Icon(Icons.done)
                                  : const Icon(Icons.arrow_forward),
                            )
                          ],
                        ))
                  ],
                ))
``` 
This container is where we switch the button appearance based on the index.
1. The back arrow will disappear in case the image is the first on the list of images.
2. The front arrow will be replaced by the done icon if the image is the last one on the list of images. If it's the next button, the __nextImage()__ function will be triggered on click. Whereas, the done button will trigger the __submitButton()__.

## Homework

Test what you've learned so far, and how far can you go. I won't provide source code for the homework,       please escape the tutorial hell by making mistakes and fixing them by yourself.

1. When you save and restart the app, you'll likely encounter an error related to the image. Fix it by yourself, we have done this in the previous part of this series.

2. Create Skip Button.
  -    Skip Button should be a text button.
  -    Position it at the bottom Center but inside the container of other buttons.
  -    The text should be red in color.
  -    Implement the same logic as submit button.

If you've done this part then share the screenshot in the comment section. 


![KCl](https://cdn.hashnode.com/res/hashnode/image/upload/v1649833218687/mid27MiAE.png)

## Summary
In this blog, we created an onboard screen with GoRouter, Shared Preferences, and Provider packages. Here are the things we did in brief:  
1. We created router utils file for efficient work.
2. We also used __MaterialApp.Router__ to implement declarative routing using the __GoRouter__ package.
3. Then we used the __SharedPreferences__ package to store onboard information on local storage. 
4. With AppStateProvider class we wrote our onboard logic and redirected our routes inside AppRouter class.
6. We also created a simple onboard screen with a __ScaleTransition__ animation. 


Our hard work looks like this:

{% embed https://youtu.be/HW6za8BYW7M %}


## Show Support
In the upcoming blog, we'll define the theme of our app. If you want to know more about the series and expectations, check out the series [introduction](https://dev.to/kcl/flutter-app-development-tutorial-blog-series-1oaj) page. 

If you've any questions please feel free to comment. Please do like, bookmark and share the article with your friends. Thank you for your time and please keep on supporting us.  __Don't forget to follow__ to be notified immediately after the upload. 

This is Nibesh from Khadka's Coding Lounge, a freelancing agency that makes websites and mobile applications. 

![Like and Subscribe](https://cdn.hashnode.com/res/hashnode/image/upload/v1648366097884/rahAfHnjG.png)