How to handle routes changes in flutter bloc depending on state - flutter

I have a question about how to handle the authentication state in flutter bloc.
Here is my login screen code.
class LoginScreen extends StatelessWidget {
const LoginScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
_getToken(context);
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is Authenticated) {
return const HomeScreen();
}
if (state is UnAuthenticated) {
return const _SignInPage();
}
return Container(
color: const Color(0xFFF39360),
child: const Center(child: CircularProgressIndicator(color: Colors.white)),
);
},
);
}
As you can see I'm rendering Homescreen and SignInPage depending on the state.
I am also calling a _getToken(context); function during build of the screen. This functions executes a loading and I get an ugly CircularProgressIndicator while it is reading the storage. If i have the token i set the state as Authenticated.
Is there a better way to handle this so I don't get the progress indicator which is not spinning when I open the application?
Thanks in advance I hope you can help me!

There is nothing wrong with your bloc builder code. It will render a default progress indicator till bloc emit any new state. So if you want to show indicator while your function doing some work then you should emit a loading state and handle the same in the bloc builder and if you don't want to show anything at all for default case then you can use any other widget like an empty container or Sizedbox.shrink().
Hope it helps

Related

How ChangeNotifier notify the state?

Can someone give me idea how provider notify the state?
I don't want to use ChangeNotifierProvider, Can you give me a suggestion without library?
I just need better explanation with example.
How provider combine InheritedWidget.
What do you think about the following example (inspired by an answer here) with an AnimatedBuilder:
import 'package:flutter/material.dart';
class MyChangeNotifier extends ChangeNotifier {
int count = 0;
void addOne() {
count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
final MyChangeNotifier myChangeNotifier = MyChangeNotifier();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ExampleButton(myChangeNotifier),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: myChangeNotifier.addOne,
),
),
);
}
}
class ExampleButton extends StatelessWidget {
final MyChangeNotifier myChangeNotifier;
const ExampleButton(this.myChangeNotifier, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: myChangeNotifier,
builder: (context, child) {
return OutlinedButton(
onPressed: myChangeNotifier.addOne,
child: Text(
'Tap me - or the floating button\n\n${myChangeNotifier.count}',
textAlign: TextAlign.center,
));
});
}
}
void main() => runApp(MyApp());
The ChangeNotifier implements the Listenable class. You can see here how to listen to that Listenable, for example with an AnimatedBuilder (what my code does).
A ChangeNotifyProvider (I know, you don't want that) would also implement that for you and notify your widgets lower in the widget tree about changes.
Here is some idea for you :
widgets listen to changes and notify each other if there is a rebuild. As soon as the state changes, that particular widget rebuilds without affecting other widgets in the tree.
Three major components make all of this possible: the ChangeNotifier class in Flutter, the ChangeNotifierProvider (primarily used in our sample app), and the Consumer widgets.
Whatever change in the state observed from the ChangeNotifier class causes the listening widget to rebuild. The Provider package offers different types of providers – listed below are some of them:
The Provider class takes a value and exposes it, regardless of the value type
ListenableProvider is the specific provider used for listenable objects. It will listen, then ask widgets depending on it and affected by the state change to rebuild any time the listener is called
ChangeNotifierProvider is similar to ListenableProvider but for ChangeNotifier objects, and calls ChangeNotifier.dispose automatically when needed
ValueListenableProvider listens to a ValueListenable and exposes the value
StreamProvider listens to a stream, exposes the latest value emitted,
and asks widgets dependent on the stream to rebuild FutureProvider
takes a Future class and updates the widgets depending on it when the
future is completed
As a suggestion to learn provider from this article-
https://medium.com/flutter-community/making-sense-all-of-those-flutter-providers-e842e18f45dd

Flutter App: Orientation Builder change causes Stream to have a ConnectionState of waiting instead of active

I have a simple app that pulls data from Firestore. I wanted to make it more user-friendly on web, so I added an OrientationBuilder that will place the Drawer next to the body when the orientation is landscape.
Here is my "Responsive Scaffold" widget:
import 'package:flutter/material.dart';
class ResponsiveScaffold extends StatefulWidget {
final AppBar? appBar;
final Widget body;
final Widget? drawer;
const ResponsiveScaffold({
Key? key,
this.appBar,
required this.body,
this.drawer,
}) : super(key: key);
#override
_ResponsiveScaffoldState createState() => _ResponsiveScaffoldState();
}
class _ResponsiveScaffoldState extends State<ResponsiveScaffold> {
#override
Widget build(BuildContext context) {
return OrientationBuilder(builder: (context, orientation) {
if (orientation == Orientation.portrait) {
return portraitBuilder();
} else {
return landscapeBuilder();
}
});
}
Widget portraitBuilder() {
return Scaffold(
appBar: widget.appBar,
drawer: widget.drawer,
body: widget.body,
);
}
Widget landscapeBuilder() {
return Scaffold(
appBar: widget.appBar,
body: Row(
children: [
if (widget.drawer != null) widget.drawer!,
Flexible(
child: widget.body,
),
],
),
);
}
}
Inside of the body that is pulling from Firestore is just a simple StreamBuilder that gives a ListView of all of the items.
I don't have any errors shown in the debug console but after some playing around and checking the connection state, I see that when I change from landscape to portrait orientation, or vice versa, the ConnectionState changes from active to waiting. Only when something new is added to the stream does it turn back to active.
If anybody can please help with this issue, I'd appreciate it!
If more information is needed as well please let me know.
Thank you.
Looks like, after much looking around, I have found the reason and solution.
The OrientationBuilder only builds ones and doesn't listen to any changes itself so when the orientation would change, it would not rebuild and would not try to pull the list correctly anymore.
The solution was to use MediaQuery.of(context).orientation inside of a Builder widget. This listens for any adjustments and then rebuilds fully when the orientation would switch.

How to make an object from a bloc available for all other bloc in Flutter

I am using Bloc for my Flutter project. I have created three blocs. These are AuthenticationBloc, FirebaseDatabaseBloc, and ChatMessagesBloc. When the user gets authenticated, AuthenticationBloc emits a state called authenticated with a user object.
I want to make this user object available inside FirebaseDatabaseBloc and ChatMessagesBloc. What is the clean way of doing this?
Well, This is year 2022 and a lot has changed. Bloc to Bloc to communication via the constructor is now considered a bad practice. Nobody said it won't work though but trust me, you'd end up tightly coupling your code.
Generally, sibling dependencies between two entities in the same architectural layer should be avoided at all costs, as it creates tight-coupling which is hard to maintain. Since blocs reside in the business logic architectural layer, no bloc should know about any other bloc.
documentation.
You should rather try this:
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocListener<WeatherCubit, WeatherState>(
listener: (context, state) {
// When the first bloc's state changes, this will be called.
//
// Now we can add an event to the second bloc without it having
// to know about the first bloc.
BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());
},
child: TextButton(
child: const Text('Hello'),
onPressed: () {
BlocProvider.of<FirstBloc>(context).add(FirstBlocEvent());
},
),
);
}
}
I hope it helps!
This is achievable by BLoC-to-BLoC communication. The simplest way is to pass your BLoC reference by the other's constructor and subscribe to BLoC changes:
#override
Widget build(BuildContext context) {
final authenticationBloc = AuthenticationBloc();
return MultiBlocProvider(
providers: [
BlocProvider<AuthenticationBloc>.value(value: authenticationBloc),
BlocProvider<FirebaseDatabaseBloc>(
create: (_) => FirebaseDatabaseBloc(
authenticationBloc: authenticationBloc,
),
),
],
child: ...,
);
}
Then, inside the FirebaseDatabaseBloc you can subscribe to changes:
class FirebaseDatabaseBloc extends Bloc<FirebaseDatabaseEvent, FirebaseDatabaseBloc> {
final AuthenticationBloc authenticationBloc;
StreamSubscription<AuthenticationState> _authenticationStateStreamSubscription;
FirebaseDatabaseBloc({
#required this.authenticationBloc,
}) : super(...) {
_authenticationStateStreamSubscription = authenticationBloc.listen(_onAuthenticationBlocStateChange);
}
#override
Future<void> close() async {
_authenticationStateStreamSubscription.cancel();
return super.close();
}
void _onAuthenticationBlocStateChange(AuthenticationState authState) {
// Do whatever you want with the auth state
}
}
For more info, you can check this video: https://www.youtube.com/watch?v=ricBLKHeubM

Navigator.pop from a FutureBuilder

I have a first screen which ask the user to enter to input, then when the users clicks on a button, the app goes on a second screen which uses a FutureBuilder to call an API.
If the API returns an error, I would like to go back to the previous screen with Navigator.pop. When I try to do that in the builder of the FutureBuilder, I get an error because I modify the tree while I am building it...
setState() or markNeedsBuild() called during build. This Overlay
widget cannot be marked as needing to build because the framework is
already in the process of building widgets
What is the proper way to go to the previous screen if an error occur?
class Stackoverflow extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FutureBuilder<Flight>(
future: fetchData(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ScreenBody(snapshot.data);
} else if (snapshot.hasError) {
Navigator.pop(context, "an error");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
)
),
);
}
}
PS: I tried to use addPostFrameCallback and use the Navigator.pop inside, but for some unknown reason, it is called multiple times
You can not directly navigate when build method is running, so it better to show some error screen and give use chance to go back to last screen.
However if you want to do so then you can use following statement to do so.
Future.microtask(() => Navigator.pop(context));
I'd prefer to convert class into StateFullWidget and get rid of FutureBuilder
class Stackoverflow extends StatefulWidget {
#override
_StackoverflowState createState() => _StackoverflowState();
}
class _StackoverflowState extends State<Stackoverflow> {
Flight flight;
#override
void initState() {
super.initState();
fetchData().then((data) {
setState(() {
flight = data;
});
}).catchError((e) {
Navigator.pop(context, "an error");
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: flight != null ? ScreenBody(flight) : CircularProgressIndicator(),
),
);
}
}
and of cause pass context somewhere outside class is not good approach

How to create Login-Wall Views in Flutter

I am working on an app in Flutter and I'm pretty new to it/Dart. I already created the login, signup etc and everything works perfectly fine. Now I want to create a "Login-Wall" Template for every View that needs the user to be logged in. If the user is not logged in, he should be returned to the LoginView, if the api-call is still loading, it should not show anything but a loading screen called LoadingView(). I started by creating a Stateful Widget called AuthorizedLayout:
class AuthorizedLayout extends StatefulWidget {
final Widget view;
AuthorizedLayout({this.view});
_AuthorizedLayoutState createState() => new _AuthorizedLayoutState();
}
The state utilizes a Future Builder as follows:
Widget build(BuildContext context) {
return FutureBuilder<User>(
future: futureToken,
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return NoConnectionView();
case ConnectionState.active:
case ConnectionState.waiting:
return LoadingView();
case ConnectionState.done:
if(snapshot.data != null) {
print("User Data loaded");
return widget.view;
} else
return LoginView();
}
},
);
}
As you can see, it should load the userdata, and when it's finished it should return the view. The futureToken represents the Future that will return the User-Object from the server after an api-request. In any other case it should show the Loading/Error/Login Page.
I'm calling it like this:
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: AuthorizedLayout(
view: DashboardView(),
),
);
}
In the Build method of the Dashboard view I have a "print('Dashboard View');". The problem I have is that in the output the 'Dashboard View' is printed before the 'User Data Loaded'. That means I can't access the loaded user data in that view. This means that this solution does not work the way I intended it to.
Now for my question: Is there any way I can build this "Login-Wall" and pass the user data to every view that is inside the login wall? I hope the code I posted explains the idea I'm trying to go for.
Is there any way I can build this "Login-Wall" and pass the user data to every view that is inside the login wall?
Absolutely! At a basic level, you're talking about state management. Once a user logs into your app, you want to store that user data so that it's accessible to any widget within the widget tree.
State management in Flutter is a hotly-debated topic and while there are a ton of options, there is no defacto state management technique that fits every app. That said, I'd start simple. One of the simplest and most popular options is the scoped_model package.
You can read all of the details here, but the gist is that it provides utilities to pass a data model from a parent widget to its descendants.
First, install the package.
Second, you'll want to create a model that can hold the user data that you want to be accessible to any widget in the tree. Here's a trivial example of what that might look like:
// user_model.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
class UserModel extends Model {
dynamic _userData;
void setUserData(dynamic userData) {
_userData = userData;
}
String getFirstName() {
return _userData['firstName'];
}
static UserModel of(BuildContext context) =>
ScopedModel.of<UserModel>(context);
}
Next, we'll need to make an instance of this UserModel available to all widgets. A contrived way of doing this would be to wrap your entire app in a ScopedModel. Example below:
// main.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'login_view.dart';
import 'user_model.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ScopedModel<UserModel>(
model: UserModel(),
child: MaterialApp(
theme: ThemeData.light(),
home: LoginView(),
),
);
}
}
In the above code, we're wrapping our entire instance of MaterialApp in a ScopedModel<UserModel>, which will give every widget in the application access to the User model.
In your login code, you could then do something like the following when your login button is pressed:
onPressed() async {
// authenticate your user...
var userData = await someApiCall();
// set the user data in our model
UserModel.of(context).setUserData(userData);
// go to the dashboard
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DashboardView(),
),
);
}
Last but not least, you can then access that user data through the UserModel like so:
// dashboard_view.dart
import 'package:flutter/material.dart';
import 'package:scoped_model_example/user_model.dart';
class DashboardView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Text(
UserModel.of(context).getFirstName(),
),
),
],
);
}
}
Check out the docs on scoped_model for more details. If you need something more advanced, there are a number of other state management patterns in Flutter such as BloC, Redux, Mobx, Provider and more.
So I just got what was happening. I was passing the already-built widget to the AuthorizedView. What I actually had to pass was a Builder instead of a Widget.
class AuthorizedLayout extends StatefulWidget {
final Builder viewBuilder;
AuthorizedLayout({this.viewBuilder});
_AuthorizedLayoutState createState() => new _AuthorizedLayoutState();
}
Calling it like this:
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: AuthorizedLayout(
viewBuilder: Builder(builder: (context) => DashboardLayout()),
),
);
}
Note that I recalled the final variable to viewBuilder instead of view, compared to the example above.
This will actually build the widget AFTER the userdata is loaded.