bloc
bloc copied to clipboard
Navigation with authentication and "deep link" routes
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!
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
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. π€¦
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 π
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.
@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
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
?
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.
@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 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.
Ah, good idea. Thanks!
How would you feel about a PR outlining this approach as an example in the examples directory?
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 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 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.
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)
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();
},
);
}
}`
@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!
I decided against deep linking for now.
@djensen47 do you think this can be closed or is there anything I can do to help? I know it's been a while π
Well, it would be nice to have an example of how to use Bloc using the new recommended approaches. π€
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?
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
This is long outstanding issue ! @felangel Does flowbuilder fall under this type of scenario?
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.
test this line code for have specific status context.read<AuthenticationBloc>().state.status
I think you forget to close this issue :)