autologin

This commit is contained in:
Stein Milder 2022-09-21 14:58:46 +02:00
parent 9d6529538d
commit 7abc81593b
14 changed files with 222 additions and 432 deletions

View file

@ -1,42 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart';
import '../login_config.dart'; import '../login_config.dart';
import '../model/login_confirmation_result.dart'; import '../model/login_confirmation_result.dart';
import '../model/login_user.dart'; import '../model/login_user.dart';
abstract class LoginRepository with ChangeNotifier { abstract class LoginRepository {
String? _loggedIn = ''; bool loggedIn = false;
String loginError = ''; String loginError = '';
bool _initialized = false;
bool isInitialized() => _initialized;
/// This function returns true if the user is logged in.
bool isLoggedIn() => _loggedIn != null && _loggedIn != '';
String get user => _loggedIn!;
/// This function sets the logged in user.
void setLoggedIn(String user) => _loggedIn = user;
String getLoginError() => loginError; String getLoginError() => loginError;
Future<bool> login(String username, String password); Future<bool> login(String username, String password);
Future<void> logout() async {
_loggedIn = null;
}
/// This function returns a map with the username.
Map<String, dynamic> getUser() => {
'username': _loggedIn,
};
@mustCallSuper
Future<void> init() async {
// Auto login here
_initialized = true;
}
Future<LoginUser?> signInWithSocial(SocialLoginBundle bundle); Future<LoginUser?> signInWithSocial(SocialLoginBundle bundle);
Future<bool?> userprofileExists(); Future<bool?> userprofileExists();
Future sendLoginEmail(String input); Future sendLoginEmail(String input);
@ -59,6 +32,6 @@ abstract class LoginRepository with ChangeNotifier {
}); });
Future<bool> forgotPassword(String email); Future<bool> forgotPassword(String email);
Future<bool> isRegistrationRequired(LoginUser user); Future<bool> isRegistrationRequired(LoginUser user);
Future<void> reLogin(); Future<void> reLogin({required VoidCallback onLoggedIn});
Future<LoginUser?> signInAnonymous(); Future<LoginUser?> signInAnonymous();
} }

View file

@ -1,17 +0,0 @@
import 'sdk/screen.dart';
import 'sdk/user.dart';
mixin FlutterLoginSdk {
static final UserService _userService = UserService();
static final ScreenService _screenService = ScreenService();
UserService get users => _userService;
ScreenService get screens => _screenService;
static UserService get userService => _userService;
static ScreenService get screenService => _screenService;
void dispose() {
_userService.dispose();
}
}

View file

@ -2,10 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_login/backend/login_repository.dart'; import 'package:flutter_login/backend/login_repository.dart';
import '../default_translation.dart'; import '../default_translation.dart';
import '../plugins/login/login_email_password.dart'; import '../plugins/login/login_email_password.dart';
import 'flutter_login_sdk.dart';
import 'login_config.dart'; import 'login_config.dart';
import 'plugins/login/choose_login.dart'; import 'plugins/login/choose_login.dart';
import 'widgets/custom_navigator.dart';
export '../plugins/form/form.dart'; export '../plugins/form/form.dart';
export '../plugins/login/email_password_form.dart'; export '../plugins/login/email_password_form.dart';
export '../plugins/login/login_email_password.dart'; export '../plugins/login/login_email_password.dart';
@ -15,8 +13,8 @@ export 'model/login_confirmation_result.dart';
export 'model/login_user.dart'; export 'model/login_user.dart';
export 'plugins/settings/control.dart' show Control; export 'plugins/settings/control.dart' show Control;
class FlutterLogin extends InheritedWidget with FlutterLoginSdk { class FlutterLogin extends InheritedWidget {
FlutterLogin({ const FlutterLogin({
required this.config, required this.config,
required this.repository, required this.repository,
required Widget child, required Widget child,
@ -24,14 +22,6 @@ class FlutterLogin extends InheritedWidget with FlutterLoginSdk {
Key? key, Key? key,
}) : super(key: key, child: child); }) : super(key: key, child: child);
FlutterLogin.from({
required FlutterLogin appShell,
required Widget child,
Key? key,
}) : config = appShell.config,
repository = appShell.repository,
app = appShell.app,
super(child: child, key: key);
static Function(Object?) logError = (error) {}; static Function(Object?) logError = (error) {};
static Map<String, Map<String, String>> get defaultTranslations => static Map<String, Map<String, String>> get defaultTranslations =>
defaultTranslation; defaultTranslation;
@ -41,21 +31,21 @@ class FlutterLogin extends InheritedWidget with FlutterLoginSdk {
final Widget app; final Widget app;
static FlutterLogin of(BuildContext context) { static FlutterLogin of(BuildContext context) {
var inheritedAppshell = var inheritedLogin =
context.dependOnInheritedWidgetOfExactType<FlutterLogin>(); context.dependOnInheritedWidgetOfExactType<FlutterLogin>();
if (inheritedAppshell == null) { if (inheritedLogin == null) {
throw FlutterError( throw FlutterError(
'You are retrieving an flutter login from a context that does not contain an flutter login. Make sure you keep the flutter login in your inheritence tree', 'You are retrieving an flutter login from a context that does not contain an flutter login. Make sure you keep the flutter login in your inheritence tree',
); );
} }
return inheritedAppshell; return inheritedLogin;
} }
@override @override
bool updateShouldNotify(FlutterLogin oldWidget) => config != oldWidget.config; bool updateShouldNotify(FlutterLogin oldWidget) => config != oldWidget.config;
} }
extension AppShellRetrieval on BuildContext { extension LoginRetrieval on BuildContext {
static LoginRepository? _cachedBackend; static LoginRepository? _cachedBackend;
FlutterLogin login() => FlutterLogin.of(this); FlutterLogin login() => FlutterLogin.of(this);
LoginRepository loginRepository() { LoginRepository loginRepository() {
@ -77,7 +67,7 @@ extension StringFormat on String {
String format(List<dynamic> params) => _interpolate(this, params); String format(List<dynamic> params) => _interpolate(this, params);
} }
extension AppShellTranslate on BuildContext { extension LoginTranslate on BuildContext {
String? _getDefaultTranslation(String key, List arguments) { String? _getDefaultTranslation(String key, List arguments) {
var locale = Localizations.localeOf(this); var locale = Localizations.localeOf(this);
var code = locale.countryCode ?? 'nl'; var code = locale.countryCode ?? 'nl';
@ -86,8 +76,11 @@ extension AppShellTranslate on BuildContext {
return translationMap?[key]?.toString().format(arguments); return translationMap?[key]?.toString().format(arguments);
} }
String translate(String key, String translate(
{String? defaultValue, List<dynamic> arguments = const []}) { String key, {
String? defaultValue,
List<dynamic> arguments = const [],
}) {
dynamic translateFunction = login().config.translate; dynamic translateFunction = login().config.translate;
if (translateFunction == null) { if (translateFunction == null) {
return _getDefaultTranslation(key, arguments) ?? defaultValue ?? key; return _getDefaultTranslation(key, arguments) ?? defaultValue ?? key;
@ -100,53 +93,64 @@ extension AppShellTranslate on BuildContext {
} }
} }
class LoginMain extends StatelessWidget { class LoginMain extends StatefulWidget {
LoginMain({ const LoginMain({
required this.child, required this.child,
super.key, super.key,
}); });
final Widget child; final Widget child;
Widget _login(context) {
return Builder(
builder: (context) {
if (context.login().users.isLoggedIn(context)) {
return child;
}
return FlutterLogin.of(context)
.config
.loginOptions
.loginMethod
.contains(LoginMethod.LoginInteractiveWithSocial) ||
FlutterLogin.of(context)
.config
.loginOptions
.loginMethod
.contains(LoginMethod.LoginInteractiveWithPhoneNumber)
? ChooseLogin(
child: child,
)
: EmailPasswordLogin(
onPressedForgotPassword: FlutterLogin.of(context)
.config
.loginOptions
.onPressForgotPassword,
child: child,
);
},
);
}
@override @override
CustomNavigator build(BuildContext context) => CustomNavigator( State<LoginMain> createState() => _LoginMainState();
pageRoute: PageRoutes.materialPageRoute,
home: _login(context),
);
} }
class AppShellException implements Exception { class _LoginMainState extends State<LoginMain> {
AppShellException( bool _checkedIfLoggedIn = false;
bool _isLoggedIn = false;
@override
Widget build(BuildContext context) {
if (!_checkedIfLoggedIn) {
context.loginRepository().reLogin(
onLoggedIn: () => setState(
() {
_isLoggedIn = true;
},
),
);
_checkedIfLoggedIn = true;
}
return _isLoggedIn
? widget.child
: Builder(
builder: (context) => FlutterLogin.of(context)
.config
.loginOptions
.loginMethod
.contains(LoginMethod.LoginInteractiveWithSocial) ||
FlutterLogin.of(context)
.config
.loginOptions
.loginMethod
.contains(LoginMethod.LoginInteractiveWithPhoneNumber)
? ChooseLogin(
child: widget.child,
)
: EmailPasswordLogin(
onPressedForgotPassword: FlutterLogin.of(context)
.config
.loginOptions
.onPressForgotPassword,
child: widget.child,
),
);
}
}
class LoginException implements Exception {
LoginException(
this.error, [ this.error, [
this.stackTrace = StackTrace.empty, this.stackTrace = StackTrace.empty,
]); ]);
@ -158,7 +162,7 @@ class AppShellException implements Exception {
@override @override
String toString() { String toString() {
return 'Unhandled error occurred in Appshell: $error\n' return 'Unhandled error occurred in login: $error\n'
'$stackTrace'; '$stackTrace';
} }
} }

View file

@ -337,7 +337,7 @@ class LoginConfig extends StatefulWidget {
} }
class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver { class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver {
FlutterLogin? appShell; FlutterLogin? login;
late final ConfigData configData; late final ConfigData configData;
late LoginRepository repository; late LoginRepository repository;
@ -352,7 +352,6 @@ class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver {
if (widget.repository != null) { if (widget.repository != null) {
repository = widget.repository!; repository = widget.repository!;
repository.init();
} }
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
@ -383,7 +382,7 @@ class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver {
!configData.loginOptions.socialOptions.forceAppleSignin))) { !configData.loginOptions.socialOptions.forceAppleSignin))) {
// check if apple login is removed by developer // check if apple login is removed by developer
if (configData.loginOptions.socialOptions.socialLogins.isEmpty) { if (configData.loginOptions.socialOptions.socialLogins.isEmpty) {
throw AppShellException( throw LoginException(
'If you enable LoginMethod.LoginInteractiveWithSocial you must provide atleast 1 social login option!'); 'If you enable LoginMethod.LoginInteractiveWithSocial you must provide atleast 1 social login option!');
} }
} }
@ -391,7 +390,7 @@ class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
appShell = FlutterLogin( login = FlutterLogin(
config: widget.config ?? ConfigData.example(), config: widget.config ?? ConfigData.example(),
repository: repository, repository: repository,
app: widget.child, app: widget.child,
@ -406,21 +405,10 @@ class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver {
if (isFlutterDefaultTheme(context)) { if (isFlutterDefaultTheme(context)) {
return Theme( return Theme(
data: defaultTheme, data: defaultTheme,
child: appShell!, child: login!,
); );
} }
return appShell!; return login!;
}
@override
void dispose() {
super.dispose();
repository.dispose();
} }
} }
class AppshellNoDisplayError {
AppshellNoDisplayError(this.error);
Object error;
}

View file

@ -14,7 +14,8 @@ class ChooseLogin extends Login {
static String? finalEmail; static String? finalEmail;
static String? finalPassword; static String? finalPassword;
createState() => ChooseLoginState(); @override
ChooseLoginState createState() => ChooseLoginState();
} }
class ChooseLoginState extends LoginState<ChooseLogin> { class ChooseLoginState extends LoginState<ChooseLogin> {

View file

@ -47,139 +47,122 @@ class ForgotPasswordState extends State<ForgotPassword> {
context.translate('forgot_password.text.title'), context.translate('forgot_password.text.title'),
), ),
), ),
body: context.login().screens.getAppshellScreenWrapper( body: Column(
context, children: [
backgroundImg: Expanded(
context.login().config.loginOptions.backgroundImage, child: ListView(
child: Column(
children: [ children: [
Expanded( Container(
child: ListView( alignment: Alignment.topLeft,
padding: const EdgeInsets.only(
top: 27,
left: 5,
),
child: context.login().config.appTheme.buttons.backButton(
context: context,
),
),
Padding(
padding: const EdgeInsets.all(30.0),
child: Text(
context.translate('forgot_password.text.body'),
style: Theme.of(context).textTheme.subtitle1,
textAlign: TextAlign.left,
),
),
Padding(
padding: const EdgeInsets.only(left: 30, right: 30),
child: Column(
children: [ children: [
Container( const Padding(
alignment: Alignment.topLeft, padding: EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.only(
top: 27,
left: 5,
),
child: context
.login()
.config
.appTheme
.buttons
.backButton(
context: context,
),
), ),
Padding( context.login().config.appTheme.inputs.textField(
padding: const EdgeInsets.all(30.0), title: context.translate(
child: Text( 'forgot_password.input.email',
context.translate('forgot_password.text.body'),
style: Theme.of(context).textTheme.subtitle1,
textAlign: TextAlign.left,
),
),
Padding(
padding: const EdgeInsets.only(left: 30, right: 30),
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(bottom: 20),
), ),
context.login().config.appTheme.inputs.textField( validators: [
title: context.translate( EmailValidator(
'forgot_password.input.email', errorMessage: context.translate(
), 'forgot_password.error.invalid_email',
validators: [
EmailValidator(
errorMessage: context.translate(
'forgot_password.error.invalid_email',
),
)
],
onChange: (value, valid) {
setState(() {
showError = false;
});
if (valid) {
email = value;
}
},
), ),
if (showError) ...[ )
Text(
context.translate(
'forgot_password.error.email_does_not_exist',
),
style: Theme.of(context)
.textTheme
.bodyText2!
.copyWith(
color: Theme.of(context).errorColor,
),
),
], ],
const Padding( onChange: (value, valid) {
padding: EdgeInsets.only(bottom: 30), setState(() {
), showError = false;
], });
if (valid) {
email = value;
}
},
),
if (showError) ...[
Text(
context.translate(
'forgot_password.error.email_does_not_exist',
),
style:
Theme.of(context).textTheme.bodyText2!.copyWith(
color: Theme.of(context).errorColor,
),
), ),
],
const Padding(
padding: EdgeInsets.only(bottom: 30),
), ),
], ],
), ),
), ),
Container(
margin: const EdgeInsets.symmetric(horizontal: 30),
padding: const EdgeInsets.only(bottom: 40.0),
child: context
.login()
.config
.appTheme
.buttons
.primaryButton(
context: context,
child: Text(
context.translate('forgot_password.button.submit'),
style: Theme.of(context).textTheme.button,
),
onPressed: () async {
if (email != null) {
setState(() {
showError = false;
});
var result = await context
.loginRepository()
.forgotPassword(email!);
if (result) {
showAlert(
title: context.translate(
'forgot_password.dialog.text.title',
),
text: context.translate(
'forgot_password.dialog.text.body',
arguments: [email],
),
buttonTitle: context.translate(
'forgot_password.dialog.text.button',
),
buttonAction: () {
Navigator.pop(context);
},
);
} else {
setState(() {
showError = true;
});
}
}
},
),
),
], ],
), ),
), ),
Container(
margin: const EdgeInsets.symmetric(horizontal: 30),
padding: const EdgeInsets.only(bottom: 40.0),
child: context.login().config.appTheme.buttons.primaryButton(
context: context,
child: Text(
context.translate('forgot_password.button.submit'),
style: Theme.of(context).textTheme.button,
),
onPressed: () async {
if (email != null) {
setState(() {
showError = false;
});
var result = await context
.loginRepository()
.forgotPassword(email!);
if (result) {
showAlert(
title: context.translate(
'forgot_password.dialog.text.title',
),
text: context.translate(
'forgot_password.dialog.text.body',
arguments: [email],
),
buttonTitle: context.translate(
'forgot_password.dialog.text.button',
),
buttonAction: () {
Navigator.pop(context);
},
);
} else {
setState(() {
showError = true;
});
}
}
},
),
),
],
),
), ),
); );
} }

View file

@ -15,30 +15,16 @@ class LoginAwaitEmailScreen extends StatefulWidget {
final Widget? child; final Widget? child;
@override @override
_LoginAwaitEmailScreenState createState() => _LoginAwaitEmailScreenState(); LoginAwaitEmailScreenState createState() => LoginAwaitEmailScreenState();
} }
class _LoginAwaitEmailScreenState extends State<LoginAwaitEmailScreen> class LoginAwaitEmailScreenState extends State<LoginAwaitEmailScreen>
with NavigateWidgetMixin { with NavigateWidgetMixin {
@override @override
void initState() { void initState() {
context.loginRepository().addListener(registrateOrMainScreen);
super.initState(); super.initState();
} }
Future<void> registrateOrMainScreen() async {
var data =
await (context.loginRepository().userprofileExists() as FutureOr<bool>);
if (context.login().config.registrationOptions.registrationMode ==
RegistrationMode.Disabled ||
data) {
context.loginRepository().setLoggedIn(EmailPasswordLogin.finalEmail!);
widget.loginComplete!();
} else {
debugPrint('Register');
}
}
void navigateToEmailPage(BuildContext context) { void navigateToEmailPage(BuildContext context) {
navigateFadeTo( navigateFadeTo(
context, context,

View file

@ -87,7 +87,7 @@ class EmailLoginState extends LoginState<EmailPasswordLogin> {
_loading = false; _loading = false;
}); });
} else { } else {
context.loginRepository().setLoggedIn(EmailPasswordLogin.finalEmail!); context.loginRepository().loggedIn = true;
navigateFadeToReplace( navigateFadeToReplace(
context, context,
(context) => widget.child, (context) => widget.child,

View file

@ -14,7 +14,6 @@ class LoginImage extends StatelessWidget {
if (config.loginImage == '') { if (config.loginImage == '') {
image = const AssetImage( image = const AssetImage(
'assets/images/login.png', 'assets/images/login.png',
package: 'appshell',
); );
} else if (split.length < 2) { } else if (split.length < 2) {
image = AssetImage(config.loginImage); image = AssetImage(config.loginImage);

View file

@ -36,43 +36,38 @@ abstract class LoginState<L extends Login> extends State<L>
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
backgroundColor: Theme.of(context).backgroundColor, backgroundColor: Theme.of(context).backgroundColor,
body: context.login().screens.getAppshellScreenWrapper( body: SizedBox(
context, height: MediaQuery.of(context).size.height,
backgroundImg: child: Stack(
context.login().config.loginOptions.backgroundImage, children: [
child: SizedBox( SingleChildScrollView(
height: MediaQuery.of(context).size.height, child: buildLoginPage(context),
child: Stack(
children: [
SingleChildScrollView(
child: buildLoginPage(context),
),
if (widget.allowExit) ...[
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 30, right: 10),
child: SizedBox(
height: 48,
width: 48,
child: GestureDetector(
key: const Key('navigateToSettings'),
child: const Icon(
Icons.close,
size: 24,
),
onTap: () {
Navigator.of(context).pop();
},
),
),
),
),
],
],
),
), ),
), if (widget.allowExit) ...[
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 30, right: 10),
child: SizedBox(
height: 48,
width: 48,
child: GestureDetector(
key: const Key('navigateToSettings'),
child: const Icon(
Icons.close,
size: 24,
),
onTap: () {
Navigator.of(context).pop();
},
),
),
),
),
],
],
),
),
); );
Widget buildLoginPage(BuildContext context); Widget buildLoginPage(BuildContext context);

View file

@ -5,10 +5,10 @@ class Resend extends StatefulWidget {
const Resend({super.key}); const Resend({super.key});
@override @override
_ConfirmationState createState() => _ConfirmationState(); ConfirmationState createState() => ConfirmationState();
} }
class _ConfirmationState extends State<Resend> { class ConfirmationState extends State<Resend> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();

View file

@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import '../flutter_login_view.dart';
class ScreenService {
late bool shouldShowIntroductionScreen;
late bool shouldShowPolicyPage;
Widget getAppshellScreenWrapper(
BuildContext context, {
required Widget child,
String? backgroundImg,
}) {
var bgImage =
backgroundImg ?? context.login().config.appOptions.backgroundImage;
if (bgImage.isNotEmpty) {
late AssetImage image;
var split = bgImage.split(';');
image = split.length < 2
? AssetImage(bgImage)
: AssetImage(
split.first,
package: split.last,
);
return Container(
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
image: DecorationImage(
image: image,
fit: BoxFit.cover,
),
),
child: child,
);
} else {
return child;
}
}
}

View file

@ -1,84 +0,0 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../flutter_login_view.dart';
class UserService extends ChangeNotifier {
late Map<String, dynamic>? _currentProfile = {};
set profile(Map<String, dynamic> profile) {
_currentProfile = profile;
notifyListeners();
}
Future<void> checkAutoLogin(FlutterLogin login) async {
debugPrint('checking autologin');
if (login.config.loginOptions.loginMode != LoginMode.NoLogin) {
await Future.delayed(const Duration(milliseconds: 100), () async {
if (login.config.loginOptions.loginMode == LoginMode.LoginAutomatic) {
if (login.config.loginOptions.loginEmail == null ||
login.config.loginOptions.loginEmail == '') {
throw Exception('No login account for automatic login provided!');
}
if (login.config.loginOptions.loginPassword == null ||
login.config.loginOptions.loginPassword == '') {
throw Exception(
'No login password for automatic login provided!',
);
}
await login.repository.login(login.config.loginOptions.loginEmail!,
login.config.loginOptions.loginPassword!);
} else if (login.config.loginOptions.loginMode ==
LoginMode.LoginAnonymous) {
await login.repository.signInAnonymous();
} else {
var prefs = await SharedPreferences.getInstance();
var autoLoginMode = login.config.loginOptions.autoLoginMode;
if ((autoLoginMode != AutoLoginMode.alwaysOff &&
(prefs.getBool('autoLogin') ?? false) == true) ||
autoLoginMode == AutoLoginMode.alwaysOn) {
await login.repository.reLogin();
}
}
});
}
}
void addProfileListener(
void Function(Map<String, dynamic>) onProfileChanged,
) {
addListener(() {
onProfileChanged.call(_currentProfile!);
});
}
bool isLoggedIn(BuildContext context) =>
context.loginRepository().isLoggedIn();
Future<void> logout(BuildContext context) =>
SharedPreferences.getInstance().then(
(value) {
value
.setBool('autoLogin', false)
.then((value) => context.loginRepository().logout());
},
);
}
class UserProfile {
late Map<String, dynamic> rawFields;
String? photoUrl;
@mustCallSuper
void init(Map<String, dynamic> raw) {
rawFields = raw;
photoUrl = raw['photo'];
}
bool isProfileComplete({List<String> requiredFields = const []}) {
return !requiredFields.any((element) => rawFields[element] == null);
}
dynamic getValue(String key) {
return rawFields[key];
}
}

View file

@ -24,10 +24,10 @@ class CustomNavigator extends StatefulWidget {
final List<NavigatorObserver> navigatorObservers; final List<NavigatorObserver> navigatorObservers;
@override @override
_CustomNavigatorState createState() => _CustomNavigatorState(); CustomNavigatorState createState() => CustomNavigatorState();
} }
class _CustomNavigatorState extends State<CustomNavigator> class CustomNavigatorState extends State<CustomNavigator>
implements WidgetsBindingObserver { implements WidgetsBindingObserver {
GlobalKey<NavigatorState>? _navigator; GlobalKey<NavigatorState>? _navigator;
@ -134,9 +134,11 @@ class _CustomNavigatorState extends State<CustomNavigator>
return result; return result;
} }
didChangeAppLifecycleState(AppLifecycleState state) {} @override
void didChangeAppLifecycleState(AppLifecycleState state) {}
noSuchMethod(Invocation invocation) { @override
void noSuchMethod(Invocation invocation) {
var name = invocation.memberName.toString(); var name = invocation.memberName.toString();
debugPrint( debugPrint(
'Expected a method to be called with name $name, ' 'Expected a method to be called with name $name, '
@ -147,9 +149,9 @@ class _CustomNavigatorState extends State<CustomNavigator>
class PageRoutes { class PageRoutes {
static final materialPageRoute = static final materialPageRoute =
(<T>(RouteSettings settings, WidgetBuilder builder) => <T>(RouteSettings settings, WidgetBuilder builder) =>
MaterialPageRoute<T>(settings: settings, builder: builder)); MaterialPageRoute<T>(settings: settings, builder: builder);
static final cupertinoPageRoute = static final cupertinoPageRoute =
(<T>(RouteSettings settings, WidgetBuilder builder) => <T>(RouteSettings settings, WidgetBuilder builder) =>
CupertinoPageRoute<T>(settings: settings, builder: builder)); CupertinoPageRoute<T>(settings: settings, builder: builder);
} }