Milad-Akarie / auto_route_library

Flutter route generator
MIT License
1.58k stars 402 forks source link

QUESTION : initialDeepLink #1458

Closed wer-mathurin closed 1 year ago

wer-mathurin commented 1 year ago

@Milad-Akarie

Use Case for Web As a user, I want to copy and pate a URL in the browser address bar I want to go to that route. If I'm not authenticated, redirect me to the login page and once authenticated redirect me to the initial URL pasted initially.

I successfully did this, but I have a question to why I needed to use an UniqueKey on the MaterialApp to make it work ?

Here is the code I skip the import to make is more readable, and the Auth mechanisims is just a dummy persisting state

void main() {
  usePathUrlStrategy();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => AuthController()..add(AuthCheck()),
      child: Builder(builder: (context) {
        final state = context.watch<AuthController>().state;

        if (state is Initial) return _waiting();
        if (state is Working) return _waiting();

        final router = AppRouter(state is Authenticated);
        String? initialDeepLink;
        if (state is Authenticated) {
          initialDeepLink = state.initialDeepLink;
        }

        return MaterialApp.router(
          key: UniqueKey(),  // Commenting this will take me to the home page, even if I have a deeplink
          title: 'Flutter Demo',
          routerConfig: router.config(initialDeepLink: initialDeepLink),
        );
      }),
    );
  }

  Widget _waiting() {
    return const Material(
      child: Center(child: CircularProgressIndicator()),
    );
  }
}
@AutoRouterConfig()
class AppRouter extends _$AppRouter {
  AppRouter(this.authenticated);
  final bool authenticated;

  @override
  late final List<AutoRoute> routes = [
    AutoRoute(page: LoginRoute.page, path: '/login', keepHistory: false),

    /// routes go here
    AutoRoute(
      page: RootRoute.page,
      path: '/',
      guards: [
        AutoRouteGuard.simple((resolver, router) async {
          if (authenticated) {
            resolver.next();
          } else {
            navigate(LoginRoute(initialDeepLink: resolver.route.fullPath));
          }
        })
      ],
      children: [
        AutoRoute(page: HomeRoute.page, path: ''),
        AutoRoute(page: ProfilRoute.page, path: 'profile'),
        AutoRoute(page: ProfilRouteDetail.page, path: 'profile/:id')
      ],
    ),
  ];
}
part 'auth_event.dart';
part 'auth_state.dart';

class AuthController extends Bloc<AuthEvent, AuthState> with SafeEmitter {
  AuthController() : super(Initial()) {
    on<AuthCheck>(_authCheck);
    on<Logout>(_logout);
    on<Login>(_login);
  }

  void _authCheck(AuthCheck event, Emitter<AuthState> emitter) async {
    final shared = await SharedPreferences.getInstance();
    if (shared.getBool('auth.temp') ?? false) {
      safeEmit(emitter, Authenticated(initialDeepLink: event.initialDeepLink));
    } else {
      safeEmit(emitter, NoAuthenticated());
    }
  }

  void _login(Login event, Emitter<AuthState> emitter) async {
    final shared = await SharedPreferences.getInstance();
    shared.setBool('auth.temp', true);
    add(AuthCheck(initialDeepLink: event.initialDeepLink));
  }

  void _logout(Logout event, Emitter<AuthState> emitter) async {
    final shared = await SharedPreferences.getInstance();
    shared.setBool('auth.temp', false);
    add(AuthCheck());
  }
}

mixin SafeEmitter<E, S> on Bloc<E, S> {
  void safeEmit(Emitter<S> emitter, S state) {
    if (isClosed) {
      return;
    } else {
      emitter(state);
    }
  }
}
part of 'auth_controller.dart';

abstract class AuthEvent extends Equatable {
  @override
  List<Object?> get props => [];
}

class AuthCheck extends AuthEvent {
  AuthCheck({this.initialDeepLink});
  final String? initialDeepLink;

  @override
  List<Object?> get props => [initialDeepLink];
}

class Logout extends AuthEvent {}

class Login extends AuthEvent {
  Login({this.initialDeepLink});
  final String? initialDeepLink;

  @override
  List<Object?> get props => [initialDeepLink];
}
part of 'auth_controller.dart';

abstract class AuthState extends Equatable {
  @override
  List<Object?> get props => [];
}

class Initial extends AuthState {}

class Working extends AuthState {}

class Authenticated extends AuthState {
  Authenticated({this.initialDeepLink});
  final String? initialDeepLink;

  @override
  List<Object?> get props => [];
}

class NoAuthenticated extends AuthState {}

class Error extends AuthState {}
Milad-Akarie commented 1 year ago

Hey @wer-mathurin not sure what the problem is but first of all you're initialising AppRouter inside of a build method which leads to state loss, secondly you shouldn't carry around the initial deep-link. let's try something like this

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
   final authController = AuthController()..add(AuthCheck());
   late final router = AppRouter(authController);
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => authController,
      child: MaterialApp.router(
          title: 'Flutter Demo',
          routerConfig: router.config(placeHolder:(_)=> _waiting()),
        );
      })
  }

  Widget _waiting() {
    return const Material(
      child: Center(child: CircularProgressIndicator()),
    );
  }
}
@AutoRouterConfig()
class AppRouter extends _$AppRouter {
  AppRouter(this.authController);
  final AuthController authController;

  @override
  late final List<AutoRoute> routes = [
    AutoRoute(page: LoginRoute.page, path: '/login', keepHistory: false),

    /// routes go here
    AutoRoute(
      page: RootRoute.page,
      path: '/',
      guards: [
        AutoRouteGuard.simple((resolver, router) async {

          if (await authController.isAuthenticated) {
            resolver.next();
          } else {
            navigate(LoginRoute(onLoginSuccess:(){
                 // user is now logged in we continue with our deep-link navigation
                  resolver.next(true); 
              }),);
          }
        })
      ],
       ...
    ),
  ];
}
class AuthController extends Bloc<AuthEvent, AuthState> with SafeEmitter {
  AuthController() : super(Initial()) {
    on<AuthCheck>(_authCheck);
    on<Logout>(_logout);
    on<Login>(_login);
  }

// haven't. worked with bloc in a while but I think you can do something like this because it's a stream
 // we need an await-able auth-status-check ... maybe you can do it in a better way
Future<bool> isAuthenticated = firstWhere((s)=> s is Authenticated, orElse:()=> NotAuthenticated()).then(s)=> s is Authenticated); 
  ...
}
wer-mathurin commented 1 year ago

@Milad-Akarie Thnaks for taking the time to respond. I know about the router state...but seems ok to restart from the begining for this usecase. The only time the build method will be call, will be when the AuthState change.

The propose solution will not work, the await will wait forever.... a new "yield" must be done, otherwise the code will wait there forever. Also we need to reevaluate gaurd when the authstate change

Maybe a new kind of Guard that is reevaluating base on state change

Milad-Akarie commented 1 year ago

@wer-mathurin there's actually this kind of guards already but I still think it's experimental. and I haven't worked on it in a while maybe you can try it and share some feedback

class AuthGuard extends AutoRedirectGuard {
  final AuthController authController;

  AuthGuardX(this.authController){
    authController.addListener((state) { 
     // don't evaluate when state does not effect auth-status
       if(state is! Inital && state is! Working){
         reevaluate();
       }
    });
  };

  @override
  Future<void> onNavigation(NavigationResolver resolver, StackRouter router) async {
    if (await canNavigate(resolver.route)) {
      resolver.next();
    } else {
      redirect(LoginRoute(), resolver: resolver);
    }
  }

  @override
  Future<bool> canNavigate(RouteMatch<dynamic> route) async {
    return authController.state is Authenticated;
  }
}
wer-mathurin commented 1 year ago

@Milad-Akarie :

I did a test and with a sligh modification to make it work with bloc, you will find the working code bellow.

Why you thnink this is still experimental?

I really like this implementation scenario wtih Blocs...with this structure, this is really simple to follow the clean architecture principle (this is not done in this example), and this will work as a charm with get_it and Injectable :-)

I could write a blog about it to show a complete(auth only) real word scenario with:

void main() {
  usePathUrlStrategy();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({super.key});

  final _authProvider = AuthController();
  late final router = AppRouter(_authProvider.stream);
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => _authProvider..add(AuthCheck()),
      child: MaterialApp.router(
        title: 'Flutter Demo',
        routerConfig: router.config(
          placeholder: (_) => _waiting(),
        ),
      ),
    );
  }

  Widget _waiting() {
    return const Material(
      child: Center(child: CircularProgressIndicator()),
    );
  }
}
@AutoRouterConfig()
class AppRouter extends _$AppRouter {
  AppRouter(this.stream);
  final Stream<AuthState> stream;

  @override
  late final List<AutoRoute> routes = [
    AutoRoute(page: LoginRoute.page, path: '/login', keepHistory: false),

    /// routes go here
    AutoRoute(
      page: RootRoute.page,
      path: '/',
      guards: [AuthGuard(stream, ValidateAuth())],
      children: [
        AutoRoute(page: HomeRoute.page, path: ''),
        AutoRoute(page: ProfilRoute.page, path: 'profile'),
        AutoRoute(page: ProfilRouteDetail.page, path: 'profile/:id')
      ],
    ),
  ];
}

abstract class IValidateAuth {
  Future<bool> validate();
}

class ValidateAuth implements IValidateAuth {
  @override
  Future<bool> validate() async {
    final shared = await SharedPreferences.getInstance();

    return shared.getBool('auth.temp') ?? false;
  }
}

class AuthGuard extends AutoRedirectGuard {
  AuthGuard(Stream<AuthState> stream, this._validate) {
    _subscription = stream.listen((state) {
      // don't evaluate when state does not effect auth-status
      if (state is! Initial && state is! Working) {
        reevaluate();
      }
    });
  }

  late StreamSubscription<AuthState> _subscription;
  final IValidateAuth _validate;

  @override
  Future<void> onNavigation(
      NavigationResolver resolver, StackRouter router) async {
    if (await canNavigate(resolver.route)) {
      resolver.next();
    } else {
      redirect(const LoginRoute(), resolver: resolver);
    }
  }

  @override
  Future<bool> canNavigate(RouteMatch route) async {
    return _validate.validate();
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
}
abstract class AuthState {}

class Initial extends AuthState {}

class Working extends AuthState {}

class Authenticated extends AuthState {}

class NoAuthenticated extends AuthState {}

class Error extends AuthState {}
abstract class AuthEvent {}

class AuthCheck extends AuthEvent {}

class Logout extends AuthEvent {}

class Login extends AuthEvent {}
Milad-Akarie commented 1 year ago

@wer-mathurin I faced some issues a refreshable auth-state, I don't remember the whole thing but it needed some fixes. anyways you can try it fully and share your thoughts maybe we can enhance this and make it solid.

github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions