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
Related
Using a normal setup for handling Theme with a ChangeNotifier that notifies the whole app / everything below it in the three - that something should be redrawn.
This approach seems general and there's multiple "guides" doing it this way. And this works works well when clicking a Button to change it. However, if the data for a Theme is coming from an API - where can we safely update the same value before rendering a Widget?
This is an example code where the ThemeData is somehow "downloaded" and supposed to be updated before rendering the view once the StreamBuilder is done. This, of course, causes the same Widget that's downloading something being redrawn while building so I'm getting a warning for that.
How can this be solved? The Theme can just be a single color that is downloaded and changed dynamically. And so far I haven't seen themes being changed inside one single widget while the "main one" is unchanged. Not sure what's the best approach to this (or similiar) issue - since it can't be uncommon in an mostly online based world.
Edit #1: Just to clarify - the Theme might change depending on the Widget / Page / Screen being loaded and it's not a "one time thing" where you initialize it at the beginning but with each screen being loaded - to customize that particular page based on online API data.
Example code:
void main() {
runApp(ChangeNotifierProvider(
create: (context) => ThemeConfig(),
child: MyApp()
));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<ThemeConfig>(builder: (context, state, child)
{
return MaterialApp(
theme: state.getTheme()
)
});
}
}
class _MyScreen extends State<MyScreen>
{
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Api.downloadTheme(),
builder: (context, snapshot)
{
// If OK render screen - But where to safely set the "Theme" from API?
return MyWidget(context.data)
});
)
}
}
class _MyWidget extends State<MyWidget>
{
#override
void initState() {
super.initState();
// This will cause the Widget tree to be redrawn while it's drawing and not work at all
// So when I've downloaded the data - where can this safely be changed?
Provider.of<ThemeConfig>(context).setTheme(widget.data.theme);
}
#override
Widget build(BuildContext context) {
return Container();
}
}
I'm not sure if I understood you correctly, but if you wander how to update the theme by fetching if from an api, here is the example of simulating an api call which updates the theme:
void main() {
runApp(
ChangeNotifierProvider(
lazy: false, // triggering ThemeConfig constructor
create: (context) => ThemeConfig(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<ThemeConfig>(builder: (context, state, child) {
return MaterialApp(
theme: state.theme,
home: MyScreen(),
);
});
}
}
class ThemeConfig with ChangeNotifier {
ThemeConfig() {
// trigger theme fetch
getTheme();
}
ThemeData theme = ThemeData(primarySwatch: Colors.blue); // initial theme
Future<void> getTheme() async {
// TODO: fetch your theme data here and then update it like below
await Future.delayed(Duration(seconds: 3)); // simulating waiting for response
theme = ThemeData(primarySwatch: Colors.red);
notifyListeners();
}
}
class MyScreen extends StatelessWidget {
const MyScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(), // notice how the colors change
body: Center(
child: Container(
height: 200,
width: 200,
color: Theme.of(context).primaryColor,
),
),
);
}
}
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 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.
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.