bloc icon indicating copy to clipboard operation
bloc copied to clipboard

Navigation with authentication and "deep link" routes

Open djensen47 opened this issue 4 years ago β€’ 24 comments

Is your feature request related to a problem? Please describe. I'm building an app that runs on mobile and web. As such my routing needs to be able to handle URL-style routes like /customers/2. I was struggling with how to do this with authentication thrown into the mix. It seems, however, that for deep linking, I should be using the Flutter "Navigation 2.0" APIs. Mentioned here: https://flutter.dev/docs/development/ui/navigation with the tutorial on the link on that page.

Describe the solution you'd like What I'm trying to accomplish is:

  • Routing for public routes
  • Routing for authenticated routes (very plural here, I'm talking dozens)
  • Routing that supports "deep links" and web links like /customers/2
  • Use Navigation 2.0 if that helps accomplish this goal

Additional context I've already implemented login like this tutorial but quickly got stuck routing other private routes unless it required a ton of boilerplate.

  • https://bloclibrary.dev/#/flutterlogintutorial

I've read through:

  • https://github.com/felangel/bloc/tree/master/examples/flutter_firestore_todos
  • https://bloclibrary.dev/#/recipesflutternavigation
  • https://bloclibrary.dev/#/recipesflutterblocaccess

And I'm still trying to figure it out.

I'm going to spend the weekend reading this (https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade) while seeing if I can make it work with Bloc.


Thanks in advance for any help. Thanks for the great library!

djensen47 avatar Nov 14 '20 06:11 djensen47

I'm leaning towards a Generated Route but haven't quite figured out the ideal way to do it. ~the new stuff seems like it should be the way to go~.


I'm also thinking I should split this into two issues?

  • Guidance on Generated Routes
  • Guidance on declarative Navigation 2.0

djensen47 avatar Nov 14 '20 07:11 djensen47

After a bunch of re-rereading, thinking, trial, error, and more thinking, I believe I found a solution. In order to use state in Generated Routes, I needed to wrap my entire MaterialApp in a Block Builder. Now my AppRouter can know whether we're authenticated or not.

This works, but it feels like there's a gotcha in here somewhere.

class _AppViewState extends State<AppView> {
  // ...

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<AuthenticationBloc, AuthenticationState>(
      builder: (context, state) {
        return MaterialApp(
          navigatorKey: _navigatorKey,
          onGenerateRoute: (settings) => _router.onGenerateRoute(
            settings,
            state,
          ),
          // initialRoute: '/',
        );
      },
    );
  }

  // ...
}

Update: I spoke too soon. This does not work. After I authenticate it just goes back to login. 🀦

djensen47 avatar Nov 15 '20 01:11 djensen47

Hi @djensen47 πŸ‘‹ Thanks for opening and issue and sorry for the delayed response.

You should be able to accomplish the desired behavior with either onGeneratedRoute or nested Navigators and the new pages api.

If you have a sample app you can share that illustrates the problem you’re facing, I’m more than happy to take a look πŸ‘

felangel avatar Nov 15 '20 03:11 felangel

Awesome, thanks. I thought I was making progress but then I wasn't. I'll post the code later today, when I'm in front of my computer. In the meantime I'm basically taking the login example from this repo and converting it to use generated routes instead. I'm able to block private routes but where I failed was allowing the user through after login/authentication.

djensen47 avatar Nov 15 '20 18:11 djensen47

@felangel In terms of the code, I started with this: https://github.com/felangel/bloc/tree/master/examples/flutter_login

Everything in the subfolders login, authentication, home, splash is the same. The only difference is that I'm actually using a different dependency injection library called Milad-Akarie/injectable for injecting my service and repositories. This was the first thing I got working and it worked like a charm so no issues there. I'm 100% confident that if I started all over with flutter_login without injectable I would get the same issue. I mention this because my main and app will differ in that regard.

void main() {
  configureInjection();
  runApp(App());
}

class App extends StatelessWidget {
  const App({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => AuthenticationBloc(),
      child: AppView(),
      lazy: false,
    );
  }
}

class AppView extends StatefulWidget {
  @override
  _AppViewState createState() => _AppViewState();
}

class _AppViewState extends State<AppView> {
  final _navigatorKey = GlobalKey<NavigatorState>();
  final _router = AppRouter();

  NavigatorState get _navigator => _navigatorKey.currentState;

  @override
  Widget build(BuildContext context) {
    print('AppViewState.build');
    return BlocConsumer<AuthenticationBloc, AuthenticationState>(
      listenWhen: (previous, current) =>
          previous.status != AuthenticationStatus.unknown &&
          current.status != AuthenticationStatus.unknown,
      listener: (context, state) {
        print('listener ${state.status}');
        switch (state.status) {
          case AuthenticationStatus.authenticated:
            _navigator.pushNamedAndRemoveUntil<void>(
              '/home',
              (route) => false,
            );
            break;
          case AuthenticationStatus.unauthenticated:
            _navigator.pushNamedAndRemoveUntil<void>(
              '/login',
              (route) => false,
            );
            break;
          default:
            break;
        }
      },
      builder: (context, state) {
        print('builder ${state.status}');
        return MaterialApp(
          navigatorKey: _navigatorKey,
          onGenerateRoute: (settings) => _router.onGenerateRoute(
            settings,
            state,
          ),
          // initialRoute: '/',
        );
      },
    );
  }

  @override
  void dispose() {
    _router.dispose();
    super.dispose();
  }
}

class AppRouter {
  Route onGenerateRoute(
    RouteSettings settings,
    AuthenticationState state,
  ) {
    print('${settings.name},  ${state.status}');

    final loginRoute = MaterialPageRoute<void>(
      builder: (context) => LoginPage(),
      settings: RouteSettings(name: '/login'),
    );

    if (settings.name == '/login') {
      return loginRoute;
    }

    if (settings.name == '/splash') {
      return MaterialPageRoute<void>(builder: (context) => SplashPage());
    }

    if (settings.name == '/' || settings.name == '/home') {
      if (state.status != AuthenticationStatus.authenticated) {
        return loginRoute;
      }
      return MaterialPageRoute<void>(
        settings: RouteSettings(name: '/'),
        //settings: RouteSettings(name: '/home'),
        builder: (context) => HomePage(),
      );
    }

    return MaterialPageRoute<void>(builder: (_) => NotFoundPage());
  }

  void dispose() {}
}

What happens (I'm running this all as a web app at the moment):

  • Load the page, it redirects to login πŸ‘
  • Try to access /home, it redirects to login πŸ‘
  • Enter the correct login information, it redirects to login πŸ‘Ž
  • After login/authentication is successful, if I manually go to /home it lets me through πŸ‘
  • If I hit the logout button, I'm redirected to the login page πŸ‘

The only issue is going from login to any other page after auth is successful. It appears I have a race condition and/or a lack of understanding when what gets fired in the lifecycle.

After I hit the login button, those print statements in the code execute as follows:

listener AuthenticationStatus.authenticated
/home,  AuthenticationStatus.unauthenticated
builder AuthenticationStatus.authenticated

djensen47 avatar Nov 15 '20 19:11 djensen47

Ah, I think I see what is happening. The listener navigates using the previous _router.onGenerateRoute which has the previous state.

So my question is, how do I get the current state into _router.onGenerateRoute?

djensen47 avatar Nov 15 '20 19:11 djensen47

I considered putting a BlocBuilder in the AppRouter in the MaterialPageRouter(s) but I have a bit of a chicken-and-egg problem. I also need to set the RouteSettings so that the url gets updated.

djensen47 avatar Nov 15 '20 21:11 djensen47

@felangel I think I have it!

Modifying the above code, I pass context to the AppRouter instead of the actual state. Then I use the context to get the AuthenticationBloc and then read state. My question now is, are there any issues in this technique?

class _AppViewState extends State<AppView> {
// ...
      builder: (context, state) {
        return MaterialApp(
          navigatorKey: _navigatorKey,
          onGenerateRoute: (settings) => _router.onGenerateRoute(
            settings,
            context,
          ),
        );
      },
// ...
}

class AppRouter {
  Route onGenerateRoute(
    RouteSettings settings,
    BuildContext context,
  ) {
    final state = context.bloc<AuthenticationBloc>().state;
// ...
}

djensen47 avatar Nov 15 '20 21:11 djensen47

@djensen47 glad you managed to get it working! I would recommend injecting the AuthenticationBloc instance rather than BuildContext because BuildContext can become outdated and cause problems with lookups.

felangel avatar Nov 16 '20 16:11 felangel

Ah, good idea. Thanks!

How would you feel about a PR outlining this approach as an example in the examples directory?

djensen47 avatar Nov 16 '20 17:11 djensen47

I would recommend injecting the AuthenticationBloc instance

I had a question about this. Since I'm using injectable as my DI container, is there any reason not to use it for all of my blocs? Are there situations where we need two distinct instances of a Bloc? The AuthenticationBloc is definitely global so that would make sense but what about a LoginBloc, etc.?

Thanks again!

djensen47 avatar Nov 16 '20 17:11 djensen47

@djensen47 I typically just use BlocProvider to manage DI for blocs since it handles closing the bloc when it is unmounted from the widget tree. I think a flutter_deep_links example would be great πŸ‘

felangel avatar Nov 16 '20 18:11 felangel

@felangel regarding this comment

I would recommend injecting the AuthenticationBloc instance rather than BuildContext because BuildContext can become outdated and cause problems with lookups.

Does that mean something like this?

class _AppViewState extends State<AppView> {
// ...
      builder: (context, state) {
        // final authenticationBloc = context.bloc<AuthenticationBloc>();
        final authenticationBloc = BlocProvider.of<AuthenticationBloc>(context);
        return MaterialApp(
          navigatorKey: _navigatorKey,
          onGenerateRoute: (settings) => _router.onGenerateRoute(
            settings,
            authenticationBloc,
          ),
        );
      },
// ...
}

If not, I'm a bit confused as how I should use BlocProvider to inject into the AppRouter.

djensen47 avatar Nov 17 '20 02:11 djensen47

I believe it'll be better to inject it through the router's constructor (I'm currently trying to change my app structure to something similar)

JoseGeorges8 avatar Nov 27 '20 19:11 JoseGeorges8

Bloc Listener dont listen to state changes.
please help, what I'm doing wrong.

`void main() {
  WidgetsFlutterBinding.ensureInitialized();
  configureInjection(Environment.prod);
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<AuthBloc>(
          create: (context) =>
              getIt<AuthBloc>()..add(const AuthEvent.authCheckRequested()),
          child: BlocListener<AuthBloc, AuthState>(
            listener: (context, state) => state.map(
              initial: (_) =>
                  ExtendedNavigator.of(context).replace(r.Routes.pinSetupPage),
              unauthenticated: (_) => ExtendedNavigator.of(context)
                  .replace(r.Routes.avatarSelectionPage),
              phoneAuthenticated: (_) => ExtendedNavigator.of(context)
                  .replace(r.Routes.cnicVerificationPage),
              cnicAuthenticated: (_) => ExtendedNavigator.of(context)
                  .replace(r.Routes.avatarSelectionPage),
              profileUploaded: (_) =>
                  ExtendedNavigator.of(context).replace(Routes.pinSetupPage),
              pinSetUp: (_) =>
                  ExtendedNavigator.of(context).replace(Routes.signInPage),
              authenticated: (_) =>
                  ExtendedNavigator.of(context).replace(r.Routes.mainPage),
            ),
          ),
        ),
        BlocProvider<AuthFieldsManagerBloc>(
          create: (context) => getIt<AuthFieldsManagerBloc>(),
          child: BlocListener<AuthFieldsManagerBloc, AuthFieldsManagerState>(
            listener: (context, state) => state.failureOrSuccessOption.fold(
              () => null,
              (either) => either.fold(
                (failure) => Flushbar(
                  duration: const Duration(seconds: 4),
                  message: failure.map(
                    cancelledByUser: (_) => 'Cancelled by user',
                    serverError: (_) => 'Server error',
                    phoneNumberAlreadyInUse: (_) => 'Phone Already in use',
                    cnicNumberAlreadyInUse: (_) => 'Cnic Already in use',
                    invalidPin: (_) => 'Invalid pin',
                    couldNotLoadPhoneNumberFromCache: (_) =>
                        'Could Not Load Phone Number From Cache',
                    cacheError: (_) => 'Cache error occurred',
                  ),
                  title: 'Rizirf Info Point',
                  flushbarPosition: FlushbarPosition.TOP,
                  flushbarStyle: FlushbarStyle.FLOATING,
                ).show(context),
                (_) => {},
              ),
            ),
          ),
        ),
      ],
      child: MaterialApp(
        title: appName,
        theme: ThemeData(
          primaryColor: const Color.fromRGBO(29, 53, 87, 1),
          accentColor: const Color.fromRGBO(29, 53, 87, 1),
          inputDecorationTheme: Theme.of(context).inputDecorationTheme.copyWith(
                alignLabelWithHint: true,
                border: border,
                focusedBorder: border,
                enabledBorder: border,
                labelStyle: Theme.of(context).textTheme.headline6.apply(
                      color: const Color.fromRGBO(241, 250, 238, 0.7),
                    ),
                hintStyle: Theme.of(context).textTheme.headline6.apply(
                      color: const Color.fromRGBO(241, 250, 238, 0.7),
                    ),
              ),
          bottomNavigationBarTheme:
              Theme.of(context).bottomNavigationBarTheme.copyWith(
                    backgroundColor: const Color.fromRGBO(230, 57, 70, 1),
                    elevation: 5.0,
                    unselectedItemColor: const Color.fromRGBO(241, 250, 238, 1),
                    selectedItemColor: const Color.fromRGBO(29, 53, 87, 1),
                    unselectedIconTheme: Theme.of(context).iconTheme.copyWith(
                          color: const Color.fromRGBO(241, 250, 238, 1),
                          opacity: 1,
                          size: 24,
                        ),
                    selectedIconTheme: Theme.of(context).iconTheme.copyWith(
                          color: const Color.fromRGBO(29, 53, 87, 1),
                          opacity: 1,
                          size: 24,
                        ),
                    type: BottomNavigationBarType.fixed,
                  ),
          scaffoldBackgroundColor: const Color.fromRGBO(230, 57, 70, 1),
          primarySwatch: Colors.blue,
          textTheme: Theme.of(context).textTheme.apply(
                bodyColor: const Color.fromRGBO(241, 250, 238, 1),
                fontFamily: 'Redrose',
              ),
          appBarTheme: Theme.of(context).appBarTheme.copyWith(
                centerTitle: true,
                elevation: 0,
                color: Colors.transparent,
                textTheme: Theme.of(context).textTheme.apply(
                      fontFamily: 'Castoro',
                      bodyColor: const Color.fromRGBO(241, 250, 238, 1.0),
                    ),
              ),
        ),
        builder: ExtendedNavigator(router: r.Router()),
      ),
    );
  }
`}``

Here is my Auth_Bloc:

`@injectable
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  AuthBloc(this._authFacade) : super(const AuthState.initial());

  final IAuthFacade _authFacade;

  @override
  Stream<AuthState> mapEventToState(
    AuthEvent event,
  ) async* {
    yield* event.map(
      authCheckRequested: (e) async* {
        final userOption = await _authFacade.getLocalUser();

        userOption.fold(
          () => const AuthState.cnicAuthenticated(),
          (user) {
            if (user.phoneNumberConfirmed)
              const AuthState.phoneAuthenticated();
            else if (user.cnicNumberConfirmed)
              const AuthState.cnicAuthenticated();
            else if (user.pin.isNotEmpty)
              const AuthState.pinSetUp();
            else
              const AuthState.unauthenticated();
          },
        );
      },
      signedOut: (e) async* {
        await _authFacade.signOut();
        yield const AuthState.unauthenticated();
      },
    );
  }
}`

rizirf-connect avatar Dec 08 '20 11:12 rizirf-connect

@djensen47 Hi! I am also currently trying to apply deep linking routing with the bloc package. I was trying to apply this on the Firebase login tutorial but I can't find myself to a solution. Do you have any problems with your implementations or does it work fine? Also I would like to know if you have any source that I can reference on on this topic and your code example :) (if that's possible) Thx and Happy New Year!

ComputelessComputer avatar Jan 07 '21 06:01 ComputelessComputer

I decided against deep linking for now.

djensen47 avatar Jan 07 '21 22:01 djensen47

@djensen47 do you think this can be closed or is there anything I can do to help? I know it's been a while πŸ˜…

felangel avatar Mar 02 '21 05:03 felangel

Well, it would be nice to have an example of how to use Bloc using the new recommended approaches. πŸ€”

djensen47 avatar Mar 09 '21 18:03 djensen47

Hi @djensen47, after planning to move all navigation to onGenerateRoute. would you mind sharing how is your latest change on handling auth state with the onGenerateRoute approach?

yk-theapps avatar Mar 31 '21 14:03 yk-theapps

Hello @djensen47 , I stumbled upon the same need you describe. In my case to support deep linking after authentification I did 3 things:

  • I created a DeepLinkBloc with the logic to initiate deep linking listening, receiving them and consuming them. I instantiated this bloc at the top of the widget tree alongside the AuthBloc to be accessible everywhere.
  • I created an abstract DeepLinkRepository that implements the method used by the DeepLinkBloc. For simplicity in my case I implemented such repository with FirebaseDynamicLinks, but I guess it would work with another implementation too.
  • Finally I instantiated the blocListener only in a UserScreenPage which is pushed only when successfully authenticated. Thus, my DeepLinkBloc will hold a pendingLink to be consumed when my UI is ready so that I can push my routes as needed, when needed. After consumption I have an event to let my DeepLinkBloc know it can dispose of the pendingLink.

Would this approach works in your case ? More on what I did here.

PS notice that for the convinience of working with blocs I kind of transformed the listeners from the repository to a stream of links to be consumed by the DeepLinkBloc

gbaccetta avatar Aug 06 '21 15:08 gbaccetta

This is long outstanding issue ! @felangel Does flowbuilder fall under this type of scenario?

Gene-Dana avatar Oct 27 '21 17:10 Gene-Dana

This is long outstanding issue ! @felangel Does flowbuilder fall under this type of scenario?

We're currently experimenting with deep link support and url based routing in flow_builder. See proposal and https://github.com/felangel/flow_builder/tree/feat/url-routing-and-deep-linking for the current progress.

felangel avatar Oct 27 '21 19:10 felangel

test this line code for have specific status context.read<AuthenticationBloc>().state.status

Arawipacha avatar Apr 07 '22 15:04 Arawipacha

I think you forget to close this issue :)

rashedswen avatar Feb 18 '23 13:02 rashedswen