Initial screen in Flutter app based on user's database property value - flutter

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..."),
),
);
}
}),
);
}
}

Related

Flutter Bloc: log out from any place when using Navigator + Bloc

I am new to Fluter and trying to tackle the navigation part using Bloc.
I would like to add the possibility to log out from any place in the application.
Details:
I am working on the Notes application (just for learning), and have wrapped MaterialApp with BlocProvider, so I can access AuthBloc from any place. MaterialApp 'home' widget is HomePage, which returns BlocConsumer, thus it can return a state depending on the event.
So, if the user has logged in - I emit AuthStateLoggedIn and return a ViewNotePage.
From the ViewNotePage (by clicking on a concrete Note) I go to NotePage, using MaterialPageRoute. And if I emit AuthEventLogOut (by press on the LogOut button) from the NotesListPage - nothing happens, until I invoke Navigator.of(context).popUntil(ModalRoute.withName('/')).
Here is the code
void main() {
runApp(const MyApp());
}
// App
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider<AuthBloc>(
create: (context) => AuthBloc(FirebaseAuthProvider()),
child: MaterialApp(
title: 'Notes',
theme: Colors.blue,
home: const HomePage(),
routes: {
viewNotePage: (context) => const ViewNotePage(),
}
),
);
}
}
// HomePage
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
context
.read<AuthBloc>()
.add(const AuthEventInitialize());
return BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state.isLoading) {
// loading is handled here
}
},
builder: (context, state) {
if (state is AuthStateLoggedOut) {
return const LoginView();
} else if (state is AuthStateRegistering) {
return const RegisterView();
} else if (state is AuthStateNeedsVerification) {
return const VerifyEmailView();
} else if (state is AuthStateLoggedIn) {
return const NotesPage();
} else if (state is AuthStateForgotPassword) {
return const ForgotPasswordView();
}
return const Scaffold(body: CircularProgressIndicator());
},
);
}
}
// NotesPage
To shorten the code, basically, I do here:
await Navigator.of(context).pushNamed(viewNotePage, arguments: someArgs);
// ViewNotePage
context.read<AuthBloc>().add(const AuthEventLogOut());
Navigator.of(context).popUntil(ModalRoute.withName('/')); // without popUntil - I stay on the ViewNotePage
So from what I understand, when I use MaterialPageRoute - it creates a new tree under the MaterialApp and now it can not access BlocConsumer.
Can someone please point out, what is the correct way to handle this case?
Is it a good idea to mix Bloc + Navigator features?
you can listen for the logout event using bloc listener and then call the navigator function
Yes, you can embed navigation into the flutter bloc (which handles login/logout navigation for you within the bloc). A solution for this already exists on flutter_bloc's official website! https://bloclibrary.dev/#/flutterlogintutorial

Using the auto_route and flutter_bloc libraries to navigate page not working

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

To read from one State to Another in flutter_bloc

I have been working on an app, here the basic structure looks like.
Having a MultiblocProvider. With two routes.
Route generateRoute(RouteSettings routeSettings) {
switch (routeSettings.name) {
case BASE_ROUTE:
return MaterialPageRoute(
builder: (_) => BlocProvider(
create: (context) => SignupCubit(),
child: SignUp(),
),
);
case OTP_VERIFY:
return MaterialPageRoute(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => VerifyCubit(),
),
BlocProvider(
create: (context) => SignupCubit(),
),
],
child: Verify(),
),
);
default:
return MaterialPageRoute(builder: (_) => Broken());
}
}
In OTP_Verify route I am giving access to two Cubit, VerifyCubit() and SignupCubit().
Now, what i am doing is,
There is two Screen, one is SignUp and the other is Verify. In SignUp Screen, if the state is SignUpSuccess, I am navigating to verify OTP screen.
class SignUp extends StatelessWidget {
const SignUp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
double deviceHeight = MediaQuery.of(context).size.height;
return Scaffold(
body: BlocListener<SignupCubit, SignupState>(
listener: (context, state) {
if (state is SignUpError) {
showToast("Please try again");
} else if (state is SignupSuccess) {
print(state.email);
Navigator.pushNamed(context, OTP_VERIFY); <--- Here
} else if (state is EmailValidationError) {
showToast("Not valid email");
}
},
child: SafeArea(
bottom: false,
child: CustomScrollView(
slivers: [
.... rest of code....
In VerifyOTP screen, i am trying to read state of current SignUpCubit
....other code....
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(45),
primary: Theme.of(context).primaryColor),
onPressed: () {
final signUpState = BlocProvider.of<SignupCubit>(context).state; <--- Here
if (signUpState is SignupSuccess) {
print(signUpState.email);
}
BlocProvider.of<VerifyCubit>(context).setOtp(otp);
},
child: const Text('Verify'),
),
.....other code.....
This is my SignUpState
part of 'signup_cubit.dart';
#immutable
abstract class SignupState {}
class SignupIntial extends SignupState {}
class SignUpError extends SignupState {}
class SignupSuccess extends SignupState {
final String email;
SignupSuccess({required this.email});
}
class EmailValidationError extends SignupState {}
Now what I am assuming is I already emitted SignupSuccess in first page and I could read it in second page if I have provided that state by MultiBlocProvider.
But its not happening. Insted I am getting SignUpIntial.
Can someone please help, what i could be doing wrong, or is my method even valid ?
that's because you provide a new instance of the SignupCubit while routing to Verify Screen. thus BlocProvider.of<SignupCubit>(context).state will return the state of the cubit above it which is still in the initial state.
I don't know why you need to check the state of the SignupCubit in the Verify Since you only navigate to it when it's SignupSuccess but anyway, a quick workaround is that you declare and initialize an instance of SignupCubit and use it in the provider around the SignUp and Verify Screens.

Flutter 2.0 Navigator w/ Provider Gives Null Value Errors when Popping

I am currently having some data race bugs when using the Flutter 2.0 Navigator API. My store is implemented with MobX and passed down via Provider. After that, I pull an Observer over the global store and then re-update the Navigator (for the routes) every time my global store updates. However, everything works fine until I hit the top back arrow on the WinnerPage. It shows the following error on the screen and flashes back to being fine instantly later:
Unexpected null value.
The relevant error-causing widget was WinnerView
Therefore, to try and debug further, I learned that the value of winner is null for an instant (from onPopPage in Navigator) when pressing the back button, which pops the state. Does anyone know if there is a fix to this problem? Here is all of my code:
Store
class GlobalStore extends Store {
#observable
String? winner;
#action
void setWinner(String? newWinner) => winner = newWinner;
}
App Setup
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "MyApp",
home: Provider<GlobalStore>(
create: (_) => GlobalStore(),
child: Observer(builder: (context) {
final store = Provider.of<GlobalStore>(context);
return Navigator(
pages: [
FooPage(),
if (store.winner != null)
WinnerPage()
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
store.setWinner(null);
return true;
}
);
}),
)
);
}
}
Foo Page (default)
class FooPage extends Page {
const FooPage() : super(key: const ValueKey('Foo Page'));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this, builder: (BuildContext context) => FooView());
}
}
class FooView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final store = Provider.of<GlobalStore>(context);
return Scaffold(
body: TextButton(
child: const Text('Hello, world!'),
onPressed: () {
store.setWinner("You!");
}
)
);
}
}
Winner Page
class WinnerPage extends Page {
const WinnerPage() : super(key: const ValueKey('Winner Page'));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this, builder: (BuildContext context) => WinnerView());
}
}
class WinnerView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final store = Provider.of<GlobalStore>(context);
final winner = store.winner!; // error here!
return Scaffold(
appBar: AppBar(),
body: Column(children: [Text('${winner} is the winner!')]));
}
}
I actually solved this myself, although I cannot find a complete explanation for why the problem was happening so if anyone else has a better answer please write it. Anyways, It looks like for some reason my way of extending Page was not working correctly. Therefore, I changed each of my Navigator.pages to look like this:
[
// note: I am using the stateless widget views not pages
MaterialPage(child: const FooView(), key: const ValueKey("FooPage")),
if (store.winner != null)
MaterialPage(child: const WinnerView(), key: const ValueKey("WinnerPage"))
]
I also have a hunch that the problem was that I was only extending Page, not MaterialPage, which may not have the same behavior. However, I still don't know why it wasn't working the other way after ~1 hr of searching.

How to navigate to another page on load in Flutter

I want to navigate to the login page if there is no logged in user, otherwise display the homepage. I thought of calling Navigator.of(context).push() conditionally inside the build method but that triggers an exception. Is there some method I'm missing that I can override?
Update to add the Homepage widget
class HomePage extends StatelessWidget {
final AppUser user;
const HomePage({Key key, this.user}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Rera Farm'),
actions: <Widget>[
PopupMenuButton(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry>[
PopupMenuItem(
child: ListTile(
title: Text('Settings'),
onTap: () {
Navigator.pop(context);
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context)
=> SettingsPage()
));
},
),
),
];
},
)
],
),
body: _buildBody(context));
}
And the container
class HomePageContainer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (BuildContext context, _ViewModel vm) {
return HomePage(
user: vm.user,
);
},
);
}
}
You need to either use a ternary in the onTap if you're using the settings button or, if you just want it to automatically send the user to the correct page when the app starts, you can put the ternary in the MyApp build method.
If you are using the settings button and just want it to pop back to the previous page if the person is not logged in then you can change NotLoggedIn() to a pop.
For some strange reason SO is refusing to post the code when it is properly formatted with four spaces, exactly as it asks, so I'm just going to make a gist.
https://gist.github.com/ScottS2017/3288c7e7e9a014430e56dd6be4c259ab
Here's how I end up doing it. I do the checks in the main method, so the user sees the splash screen set in manifest while those weird checks are made:
void main() {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences.getInstance().then((instance) {
_token = instance.getString("token");
final _loggedIn = _token != null && token != "";
runApp(MyApp(loggedIn: _loggedIn));
});
}
Then in your app add the parameters to switch:
class MyApp extends StatelessWidget {
final bool loggedIn;
MyApp({this.key, this.loggedIn});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: loggedIn ? HomePage() : LoginPage(),
);
}
}
You can also use Navigator.pushReplacement() if you need to do it below MyApp(). Just posting it here for future generations.