Skip to content

Commit 28e4a38

Browse files
authored
Improve app navigation. (#111)
* improve app navigation, and save navigation based on the auth state * improve auth state ref * imrpove go router * add deeplink support * add deeplink support
1 parent be28f8f commit 28e4a38

File tree

11 files changed

+227
-74
lines changed

11 files changed

+227
-74
lines changed

app/lib/main/app.dart

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,99 @@
1-
import 'package:common/core/resource.dart';
21
import 'package:flutter/material.dart';
32
import 'package:domain/bloc/app/app_cubit.dart';
43
import 'package:domain/bloc/app/app_state.dart';
54
import 'package:domain/bloc/auth/auth_cubit.dart';
6-
import 'package:domain/bloc/auth/auth_state.dart';
75
import 'package:app/presentation/navigation/routers.dart';
86
import 'package:app/presentation/resources/locale/generated/l10n.dart';
97
import 'package:app/presentation/themes/app_themes.dart';
108
import 'package:app/presentation/utils/lang_extensions.dart';
119
import 'package:flutter_bloc/flutter_bloc.dart';
1210
import 'package:flutter_localizations/flutter_localizations.dart';
11+
import 'package:flutter/foundation.dart' show kIsWeb;
12+
import 'package:app_links/app_links.dart';
1313
import 'package:go_router/go_router.dart';
14-
1514
import 'init.dart';
1615

17-
class App extends StatelessWidget {
18-
GoRouter get _goRouter => Routers.authRouter;
19-
16+
class App extends StatefulWidget {
2017
const App({super.key});
2118

19+
@override
20+
State<App> createState() => _AppState();
21+
}
22+
23+
class _AppState extends State<App> {
24+
late final GoRouter _router;
25+
bool _isRouterReady = false;
26+
27+
@override
28+
void initState() {
29+
super.initState();
30+
_initRouter();
31+
}
32+
33+
Future<void> _initRouter() async {
34+
String? initialLocation;
35+
if (kIsWeb) {
36+
final path = Uri.base.path;
37+
if (path.isNotEmpty && path != '/') {
38+
initialLocation = path;
39+
}
40+
} else {
41+
final appLinks = AppLinks();
42+
final initialUri = await appLinks.getInitialLink();
43+
if (initialUri != null) {
44+
initialLocation = initialUri.path;
45+
}
46+
}
47+
48+
debugPrint('App entry point: $initialLocation');
49+
50+
if (mounted) {
51+
setState(() {
52+
_router = Routes.init(
53+
context,
54+
initialLocation: initialLocation,
55+
);
56+
_isRouterReady = true;
57+
});
58+
}
59+
}
60+
2261
@override
2362
Widget build(BuildContext context) {
2463
return MultiBlocProvider(
2564
providers: [
2665
BlocProvider(create: (_) => getIt<AppCubit>()),
2766
BlocProvider(create: (_) => getIt<AuthCubit>()),
2867
],
29-
child: BlocBuilder<AppCubit, AppState>(
30-
builder: (context, state) {
31-
return MaterialApp.router(
32-
theme: AppThemes.getAppTheme(state.themeType).data,
33-
locale: LangExtensions.langLocale[state.appLang],
34-
supportedLocales: LangExtensions.supportedLang,
35-
localizationsDelegates: const [
36-
S.delegate,
37-
GlobalMaterialLocalizations.delegate,
38-
GlobalWidgetsLocalizations.delegate,
39-
GlobalCupertinoLocalizations.delegate,
40-
],
41-
builder: (context, child) {
42-
return BlocListener<AuthCubit, Resource>(
43-
listener: (_, state) {
44-
if (state is RSuccess<AuthState>) {
45-
switch (state.data) {
46-
case AuthStateAuthenticated _:
47-
_goRouter.go('/home');
48-
case AuthStateUnauthenticated _:
49-
_goRouter.go('/login');
50-
case _:
51-
}
52-
}
53-
},
54-
child: child,
55-
);
56-
},
57-
routerConfig: _goRouter,
58-
);
59-
},
60-
),
68+
child: !_isRouterReady
69+
? const Material(
70+
child: Center(
71+
child: CircularProgressIndicator(),
72+
),
73+
)
74+
: BlocBuilder<AppCubit, AppState>(
75+
builder: (context, state) {
76+
return MaterialApp.router(
77+
theme: AppThemes.getAppTheme(state.themeType).data,
78+
locale: LangExtensions.langLocale[state.appLang],
79+
supportedLocales: LangExtensions.supportedLang,
80+
localizationsDelegates: const [
81+
S.delegate,
82+
GlobalMaterialLocalizations.delegate,
83+
GlobalWidgetsLocalizations.delegate,
84+
GlobalCupertinoLocalizations.delegate,
85+
],
86+
builder: (context, child) =>
87+
child ??
88+
const Material(
89+
child: Center(
90+
child: CircularProgressIndicator(),
91+
),
92+
),
93+
routerConfig: _router,
94+
);
95+
},
96+
),
6197
);
6298
}
6399
}

app/lib/main/init.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import 'package:data/init.dart';
66
import 'package:domain/init.dart';
77
import 'package:flutter/material.dart';
88
import 'package:get_it/get_it.dart';
9-
import 'package:url_strategy/url_strategy.dart';
9+
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
10+
1011

1112
void init() async {
1213
WidgetsFlutterBinding.ensureInitialized();
14+
usePathUrlStrategy();
1315
await initialize();
14-
setHashUrlStrategy();
1516
runApp(const App());
1617
}
1718

Lines changed: 129 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,137 @@
1+
import 'package:app/main/init.dart';
12
import 'package:app/presentation/ui/pages/home/home_page.dart';
23
import 'package:app/presentation/ui/pages/login/login_page.dart';
34
import 'package:app/presentation/ui/pages/sign_up/sign_up_page.dart';
45
import 'package:app/presentation/ui/pages/splash/splash_page.dart';
6+
import 'package:common/core/resource.dart';
7+
import 'package:domain/bloc/auth/auth_cubit.dart';
8+
import 'package:domain/bloc/auth/auth_state.dart';
9+
import 'package:flutter/widgets.dart';
10+
import 'package:flutter_bloc/flutter_bloc.dart';
511
import 'package:go_router/go_router.dart';
612

13+
enum Routes {
14+
auth,
15+
login,
16+
signup,
17+
app,
18+
home,
19+
placeholder;
20+
21+
String get path => '/$name';
22+
String get subPath => name;
23+
24+
void nav(BuildContext context, {Object? extra}) {
25+
context.router.goNamed(
26+
name,
27+
extra: extra,
28+
);
29+
}
30+
31+
static GoRouter init(BuildContext context, {String? initialLocation}) =>
32+
Routers.appRouter(context, initialLocation: initialLocation);
33+
}
34+
35+
extension ContextOnRouter on BuildContext {
36+
GoRouter get router => GoRouter.of(this);
37+
}
38+
739
class Routers {
8-
static GoRouter authRouter = GoRouter(
9-
initialLocation: "/splash",
10-
routes: [
11-
GoRoute(
12-
name: "login",
13-
path: "/login",
14-
builder: (context, state) => const LoginPage(),
15-
),
16-
GoRoute(
17-
name: "splash",
18-
path: "/splash",
19-
builder: (context, state) => const SplashPage(),
20-
),
21-
GoRoute(
22-
name: "signUp",
23-
path: "/signUp",
24-
builder: (context, state) => const SignUpPage(),
25-
),
26-
GoRoute(
27-
name: "home",
28-
path: "/home",
29-
builder: (context, state) => const HomePage(),
30-
),
31-
],
32-
);
40+
static GoRouter appRouter(
41+
BuildContext context, {
42+
String? initialLocation,
43+
}) =>
44+
GoRouter(
45+
initialLocation: initialLocation ??
46+
(getIt<AuthCubit>().isLoggedIn()
47+
? Routes.app.path
48+
: Routes.auth.path),
49+
routes: [
50+
GoRoute(
51+
path: '/',
52+
builder: (context, state) {
53+
return BlocListener<AuthCubit, Resource>(
54+
listenWhen: (previous, current) => current is RSuccess,
55+
listener: (_, appState) {
56+
if (appState is RSuccess) {
57+
switch (appState.data) {
58+
case AuthStateAuthenticated _:
59+
debugPrint('User is authenticated: ${state.fullPath}');
60+
if (state.fullPath?.startsWith(Routes.app.path) ??
61+
false) {
62+
// Already navigating to app, do nothing
63+
return;
64+
}
65+
debugPrint('Navigating to app route');
66+
Routes.app.nav(context);
67+
break;
68+
case AuthStateUnauthenticated _:
69+
debugPrint(
70+
'User is unauthenticated: ${state.fullPath}');
71+
if (state.fullPath?.startsWith(Routes.auth.path) ??
72+
false) {
73+
// Already navigating to auth, do nothing
74+
return;
75+
}
76+
debugPrint('Navigating to auth route');
77+
Routes.auth.nav(context);
78+
break;
79+
case _:
80+
}
81+
}
82+
},
83+
child: const SplashPage(),
84+
);
85+
},
86+
routes: [
87+
ShellRoute(
88+
builder: (context, state, child) => child,
89+
routes: [
90+
GoRoute(
91+
name: Routes.auth.name,
92+
path: Routes.auth.path,
93+
redirect: (context, state) {
94+
if (getIt<AuthCubit>().isLoggedIn()) {
95+
return Routes.app.path;
96+
}
97+
return null;
98+
},
99+
builder: (context, state) => const LoginPage(),
100+
routes: [
101+
GoRoute(
102+
name: Routes.signup.name,
103+
path: Routes.signup.subPath,
104+
builder: (context, state) => const SignUpPage(),
105+
),
106+
],
107+
),
108+
],
109+
),
110+
ShellRoute(
111+
builder: (context, state, child) => child,
112+
routes: [
113+
GoRoute(
114+
name: Routes.app.name,
115+
path: Routes.app.path,
116+
redirect: (context, state) {
117+
if (!getIt<AuthCubit>().isLoggedIn()) {
118+
return Routes.auth.path;
119+
}
120+
return null;
121+
},
122+
builder: (context, state) => const HomePage(),
123+
routes: [
124+
GoRoute(
125+
name: Routes.placeholder.name,
126+
path: Routes.placeholder.subPath,
127+
builder: (context, state) => const Placeholder(),
128+
),
129+
],
130+
),
131+
],
132+
),
133+
],
134+
),
135+
],
136+
);
33137
}

app/lib/presentation/ui/pages/home/home_view.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class HomeView extends StatelessWidget {
1414
Widget build(BuildContext context) {
1515
return Scaffold(
1616
appBar: AppBar(
17+
title: const Text('Home'),
18+
automaticallyImplyLeading: false,
1719
actions: [
1820
IconButton(
1921
onPressed: () => _authCubit.logOut(),

app/lib/presentation/ui/pages/login/login_page.dart

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'package:app/main/init.dart';
2+
import 'package:app/presentation/navigation/routers.dart';
23
import 'package:app/presentation/resources/resources.dart';
3-
import 'package:app/presentation/themes/app_themes.dart';
44
import 'package:app/presentation/ui/custom/app_theme_switch.dart';
55
import 'package:app/presentation/ui/custom/loading_screen.dart';
66
import 'package:common/core/resource.dart';
@@ -33,10 +33,7 @@ class LoginPage extends StatelessWidget {
3333
child: ElevatedButton(
3434
child: const Text('Login'),
3535
onPressed: () {
36-
_authCubit.login(
37-
'Rootstrap',
38-
'12345678',
39-
);
36+
_authCubit.login('Rootstrap', '12345678');
4037
},
4138
),
4239
),

app/lib/presentation/ui/pages/splash/splash_page.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import 'package:domain/bloc/auth/auth_cubit.dart';
33
import 'package:flutter/material.dart';
44

55
class SplashPage extends StatefulWidget {
6-
const SplashPage({super.key});
6+
final bool instant;
7+
const SplashPage({super.key, this.instant = true});
78

89
@override
910
State<SplashPage> createState() => _SplashPageState();
@@ -20,7 +21,7 @@ class _SplashPageState extends State<SplashPage> {
2021

2122
/// Add post frame callback to avoid calling bloc methods during build
2223
WidgetsBinding.instance.addPostFrameCallback((_) async {
23-
await Future.delayed(const Duration(seconds: 1));
24+
await Future.delayed(Duration(seconds: widget.instant ? 0 : 2));
2425
_authCubit.onValidate();
2526
});
2627
}

app/linux/flutter/generated_plugin_registrant.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
#include "generated_plugin_registrant.h"
88

9+
#include <gtk/gtk_plugin.h>
910

1011
void fl_register_plugins(FlPluginRegistry* registry) {
12+
g_autoptr(FlPluginRegistrar) gtk_registrar =
13+
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
14+
gtk_plugin_register_with_registrar(gtk_registrar);
1115
}

app/linux/flutter/generated_plugins.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44

55
list(APPEND FLUTTER_PLUGIN_LIST
6+
gtk
67
)
78

89
list(APPEND FLUTTER_FFI_PLUGIN_LIST

0 commit comments

Comments
 (0)