I'm watching a simple authentication provider created using Riverpod in a Flutter app, that will then use NavigatorSate to either navigate to the home page, or the sign in page, depending on whether the user is authenticated. I've create a simple provider that returns 'false' by default, and my App class looks as follows:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:workbench/app_pods.dart';
import 'package:workbench/app_router.dart';
import 'package:workbench/app_routes.dart';
class App extends StatelessWidget {
App({Key? key}) : super(key: key);
final _navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => _navigatorKey.currentState!;
#override
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
final authenticated = ref.watch(authProvider);
return MaterialApp(
navigatorKey: _navigatorKey,
debugShowCheckedModeBanner: false,
initialRoute: AppRoutes.splash,
onGenerateRoute: AppRouter.generateRoute,
theme: ThemeData(
colorSchemeSeed: const Color(0xFF243EA5),
),
builder: (context, child) {
if (authenticated) {
//_navigatorKey.currentState?.pushNamedAndRemoveUntil<void>(
_navigator.pushNamedAndRemoveUntil<void>(
AppRoutes.home,
(route) => false,
);
} else {
// _navigatorKey.currentState?.pushNamedAndRemoveUntil<void>(
_navigator.pushNamedAndRemoveUntil<void>(
AppRoutes.signin,
(route) => false,
);
}
return Container(child: child);
}
);
}
);
}
}
However, _navigatorKey.currentState is always null, and so I'm unable to navigate.
This may not be a Riverpod question, as much as a NavigatorState question - although I'd be super grateful for any tips or suggestions. I was able to get this working with Bloc and a BlocListener but I am trying to convert this 'test' app to Riverpod as a learning exercise.
Related
In my Flutter app, I am using Firebase auth for the authentication of users. I am successfully able to direct the user to a specific page based on the user's auth state (Signup page if they have not signed up before, and home page if they are authenticated). However, I have an onboarding form I want users to go through after signing up and before using the app. Currently, I have a hasOnBoarded property in my database and if Firebase auth has successfully authentication the user, I make a call to the database for user data. Once the comes back I check the hasOnBoarded property to determine if they should be directed to the onboarding screen or the Home Screen. The problem I'm having is that while the user data has been successfully retrieved, they app doesn't rebuild and thus they are sent to the wrong screen (If I hot reload it does update to the correct screen). I am using a FutureBuilder to accomplish this, and I'm wondering if there is a better way. I'm also using riverpod to manage global state (which is where the user is stored).
main.dart
class AppRoot extends ConsumerWidget {
const AppRoot({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
...
initialRoute: '/loading',
routes: {
'/loading': (context) => const LoadingScreen(),
'/welcome': (context) => const WelcomeScreen(),
'/onboarding': (context) => const OnBoardingScreen(),
'/home': (context) => const HomeScreen(),
...
},
);
}
}
loading_screen.dart
class LoadingScreen extends ConsumerStatefulWidget {
const LoadingScreen({Key? key}) : super(key: key);
#override
ConsumerState<LoadingScreen> createState() => _LoadingScreenState();
}
class _LoadingScreenState extends ConsumerState<LoadingScreen> {
#override
Widget build(BuildContext context) {
final firebaseUser = ref.watch(authRepostoryProvider).getCurrentUser();
final userExists = ref.watch(userProvider).initialize(firebaseUser?.uid);
return FutureBuilder(
future: userExists,
builder: ((context, snapshot) {
print("FutureBuilder: ${snapshot.data}");
switch (snapshot.data) {
case "no_user":
return const WelcomeScreen();
case "has_not_on_boarded":
return const OnBoardingScreen();
case "has_on_boarded":
return const HomeScreen();
default:
return Container(
color: Colors.white,
child: const Center(
child: Text("Loading..."),
),
);
}
}),
);
}
}
I trying to using the auto_route and flutter_bloc libraries to navigate page, but BlocListener is not triggered.
I'm using print(SplashRoute == NavigationState.initial().routeType); to check the trigger condition with BlocListener, it's return true.
However, the BlocListener still not triggered.
How do I fix my code problem :(? This is the sample code of my app. Thanks.
main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const AppWidget());
}
class AppWidget extends StatelessWidget {
const AppWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final rootRouter = RootRouter();
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => NavigationCubit()..nav(SplashRoute),
),
// ... Other blocProvider
],
child: BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, state) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
theme: state.themeData,
routerDelegate: rootRouter.delegate(),
routeInformationParser: rootRouter.defaultRouteParser(),
);
},
));
}
}
class SplashPage extends StatelessWidget {
const SplashPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
print(SplashRoute == NavigationState.initial().routeType); // <------ return ture
return MultiBlocListener(
listeners: [
BlocListener<NavigationCubit, NavigationState>( // <------ Not working here
listenWhen: (p, c) => c.routeType is SplashRoute,
listener: (context, state) {
LoggerService.simple.i('NavigationCubit page listening!!');
context.read<NavigationCubit>().nav(HomeRoute);
context.pushRoute(const HomeRoute());
},
// ... Other blocListener
),
],
child: const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
);
}
}
navigation_state.dart
part of 'navigation_cubit.dart';
#freezed
abstract class NavigationState with _$NavigationState {
const factory NavigationState({
required Type routeType,
}) = _NavigationState;
factory NavigationState.initial() => const NavigationState(
routeType: SplashRoute,
);
}
navigation_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../presentation/routes/router.gr.dart';
part 'navigation_cubit.freezed.dart';
part 'navigation_state.dart';
class NavigationCubit extends Cubit<NavigationState> {
NavigationCubit() : super(NavigationState.initial());
void nav(Type routeType) {
emit(
state.copyWith(
routeType: routeType,
),
);
}
#override
Future<void> close() async {
return super.close();
}
}
There are two things I can interpret from your code. Either way it will not work as you had hoped.
My understanding from your question is that you are trying to navigate using the BlocListener on initial state, which doesn't work.
The reason is that BlocListener is not triggered on initial state as it is not a state change, but rather something that is defined by the bloc.
The second thing I see is that you call the nav method when providing the bloc, which is a good thing: NavigationCubit()..nav(SplashRoute). However, it will set the same value for the parameter routeType, which will not trigger a state change as it is the same value. Meaning that the BlocListener will not be triggered.
Set routeType to something else initially, perhaps set it to null, so that your bloc can identify a state change, then your BlocListener will be triggered.
EDIT:
Also, c.routeType is SplashRoute doesn't seem right. try changing to c.routeType == SplashRoute in your listenWhen property, otherwise your function in the listener property will not trigger
I've setup my project with a Scaffold wrapper so all the routes have the AppBar and SideDrawer widgets.
I'm quite annoyed having to pass around my _navigatorKey all the time and running into this error: "Navigator operation requested with a context that does not include a Navigator".
Also when trying to do very specific things like open a dialog I have to pass navigatorKey.currentContext which throws this error: "The argument type 'BuildContext?' can't be assigned to the parameter type 'BuildContext'".
Also when using any widget requiring an overlay like TextFormField or DropdownButton I'm running into this error: "No Overlay widget found.".
I've been able to fix all these issues with roundabout methods that I don't like, but more and more issues keep coming up because of this GlobalKey usage.
Is there a better way to wrap my routes and avoid using GlobalKey?
import 'package:flutter/material.dart';
import 'package:get_it_mixin/get_it_mixin.dart';
import 'package:provider/provider.dart';
import 'pages/blog_page.dart';
import 'pages/landing_page/landing_page.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget with GetItMixin {
MyApp({Key? key}) : super(key: key);
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MenuProvider>(
create: (_) => MenuProvider(),
),
ChangeNotifierProvider<MarkdownProvider>(
create: (_) => MarkdownProvider(),
),
],
builder: (context, child) {
return MaterialApp(
title: "test",
navigatorKey: _navigatorKey,
debugShowCheckedModeBanner: false,
builder: (context, child) {
return Overlay(
initialEntries: [
OverlayEntry(
builder: (context) =>
MyScaffold(child: child!, navigatorKey: _navigatorKey),
),
],
);
},
home: const LandingPage(),
routes: {
LandingPage.routeName: (context) => const LandingPage(),
BlogPage.routeName: (context) => const BlogPage(),
MarkdownPage.routeName: (context) => const MarkdownPage(),
},
);
},
);
}
}
still a beginner in flutter. below is a sample chat apps i tried to redirect user depending on their login status.
so far tested with emulator, the outputs is what i expected. my questions are:
1.is this the correct approach for user redirect, or is there a better way as in better refactored code?
2.any refactoring can be done for the 'return materialApp', as it is very repetitive. (only changing initialRoute)
3.any implication to runApp a StatefulWidget? because all tutorial normally starts runApp a StatelessWidget
import 'package:flutter/material.dart';
import 'package:chatting/screens/login_screen.dart';
import 'package:chatting/screens/registration_screen.dart';
import 'package:chatting/screens/chat_screen.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:async';
void main() => runApp(LoadPage());
class LoadPage extends StatefulWidget {
#override
_LoadPageState createState() => _LoadPageState();
}
class _LoadPageState extends State<LoadPage> {
Future checkIfLoggedIn;
#override
void initState() {
super.initState();
checkIfLoggedIn = FirebaseAuth.instance.currentUser();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<FirebaseUser>(
future: checkIfLoggedIn,
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(
backgroundColor: Colors.lightBlueAccent,
),
);
default:
if (snapshot.hasData)
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: ChatScreen.id,
routes: {
ChatScreen.id: (context) => ChatScreen(),
LoginScreen.id: (context) => LoginScreen(),
RegistrationScreen.id: (context) => RegistrationScreen(),
},
);
else
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: LoginScreen.id,
routes: {
ChatScreen.id: (context) => ChatScreen(),
LoginScreen.id: (context) => LoginScreen(),
RegistrationScreen.id: (context) => RegistrationScreen(),
},
);
}
});
}
}
Yeah your code looks good to me. There's no problem using a StatefulWidget in runApp.
The only additional tip I'd give is that, typically, for larger applications, you'll want to use the BLoC pattern to manage state. If you added that pattern to this code sample, it would abstract the logic you're doing away from this component, and you could manage the future in the bloc. You could then use a stateless widget for your loading screen. The Flutter Bloc library provides useful, straightforward abstractions that show how to implement the bloc pattern.
I have two streams:
Stream<FirebaseUser> FirebaseAuth.instance.onAuthStateChanged
Stream<User> userService.streamUser(String uid)
My userService requires the uid of the authenticated FirebaseUser as a parameter.
Since I will probably need to access the streamUser() stream in multiple parts of my app, I would like it to be a provider at the root of my project.
This is what my main.dart looks like:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
var auth = FirebaseAuth.instance;
var userService = new UserService();
return MultiProvider(
providers: [
Provider<UserService>.value(
value: userService,
),
],
child: MaterialApp(
home: StreamBuilder<FirebaseUser>(
stream: auth.onAuthStateChanged,
builder: (context, snapshot) {
if (!snapshot.hasData) return LoginPage();
return StreamProvider<User>.value(
value: userService.streamUser(snapshot.data.uid),
child: HomePage(),
);
}),
),
);
}
}
The issue is that when I navigate to a different page, everything below the MaterialApp is changed out and I lose the context with the StreamProvider.
Is there a way to add the StreamProvider to the MultiProvider providers-list?
Because when I try, I also have to create another onAuthStateChanged stream for the FirebaseUser and I don't know how to combine them into one Provider.
So this seems to work fine:
StreamProvider<User>.value(
value: auth.onAuthStateChanged.transform(
FlatMapStreamTransformer<FirebaseUser, User>(
(firebaseUser) => userService.streamUser(firebaseUser.uid),
),
),
),
If anybody has doubts about this in certain edge cases, please let me know.
Thanks to pskink for the hint about flatMap.
Maybe you can try this approach:
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<FirebaseUser>(
builder: (_) => FirebaseUser(),
),
],
child: AuthWidgetBuilder(builder: (context, userSnapshot) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.indigo),
home: AuthWidget(userSnapshot: userSnapshot),
);
}),
);
}
}
AuthWidgetBuilder.dart
Used to create user-dependant objects that need to be accessible by
all widgets. This widget should live above the [MaterialApp]. See
[AuthWidget], a descendant widget that consumes the snapshot generated
by this builder.
class AuthWidgetBuilder extends StatelessWidget {
const AuthWidgetBuilder({Key key, #required this.builder}) : super(key: key);
final Widget Function(BuildContext, AsyncSnapshot<User>) builder;
#override
Widget build(BuildContext context) {
final authService =
Provider.of<FirebaseUser>(context, listen: false);
return StreamBuilder<User>(
stream: authService.onAuthStateChanged,
builder: (context, snapshot) {
final User user = snapshot.data;
if (user != null) {
return MultiProvider(
providers: [
Provider<User>.value(value: user),
Provider<UserService>(
builder: (_) => UserService(uid: user.uid),
),
],
child: builder(context, snapshot),
);
}
return builder(context, snapshot);
},
);
}
}
AuthWidget.dart
Builds the signed-in or non signed-in UI, depending on the user
snapshot. This widget should be below the [MaterialApp]. An
[AuthWidgetBuilder] ancestor is required for this widget to work.
class AuthWidget extends StatelessWidget {
const AuthWidget({Key key, #required this.userSnapshot}) : super(key: key);
final AsyncSnapshot<User> userSnapshot;
#override
Widget build(BuildContext context) {
if (userSnapshot.connectionState == ConnectionState.active) {
return userSnapshot.hasData ? HomePage() : SignInPage();
}
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
This is originally from the tutorial of advance provider from Andrea Bizotto.
But I tailored some the code according to your your code above.
Hope this works, good luck!
Reference:
https://www.youtube.com/watch?v=B0QX2woHxaU&list=PLNnAcB93JKV-IarNvMKJv85nmr5nyZis8&index=5