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:
/customers/2Additional context
I've already implemented login like this tutorial but quickly got stuck routing other private routes unless it required a ton of boilerplate.
I've read through:
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?
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鈥檙e facing, I鈥檓 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):
/home, it redirects to login 馃憤 /home it lets me through 馃憤 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)
Most helpful comment
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鈥檙e facing, I鈥檓 more than happy to take a look 馃憤