I am using provider for the state management on Flutter. I'm making async await function and have warning that Do not use BuildContexts across async gaps. So I tried to put 'if(!mounted)' code and I got warning that Undefined name 'mounted'.
How can I fix this problem? Thank you!
Provider codes
signIn(BuildContext context) async{
try {
final navigator = Navigator.of(context);
!isSignupValid ? isSignupValid = true : null;
await authentication.signInWithEmailAndPassword(
email: userEmail.trim(), password: userPassword.trim()
);
navigator.pop();
} on FirebaseAuthException catch (errorCode) {
isSignupValid = false;
print('isSignupValid : $isSignupValid');
print('SignIn FirebaseAuthException : $errorCode');
ScaffoldMessenger.of(context).showSnackBar(
returnSnackBar(context, errorCode)
);
}
await Future.delayed(const Duration(seconds: 0));
if (!mounted) return;
context.watch<ProfileData>().profileImage = null;
notifyListeners();
}
The mounted property is only available in a StatefulWidget. If you are not in a stateful widget you have no way of knowing whether the context you are using still references the state of a widget which is still in the widget tree.
I'm not sure exactly what you do. You can either change your widget to a StatefulWidget or simply do a final profileData = context.read<ProfileData>() at the beginning of you method, and never access context after your first async call.
If you want to check whether the widget is mounted in a StatelessWidget, you can try using a GlobalKey to obtain a reference to the widget and then check its mounted property:
class MyStatelessWidget extends StatelessWidget {
final _globalKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Container(
key: _globalKey,
child: Text('Hello, world!'),
);
}
bool get isMounted => _globalKey.currentState != null && _globalKey.currentState!.mounted;
}
Related
I have to render screens condition wise. but conditions are not getting fulfilled.
My function is always returns the false boolean value even if it doesn't have to.
Here is my code
class _HomePageState extends State<HomePage> {
Future<bool> checkIsLoggedIn() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var data = await prefs.getString('isAuthenticated');
// the value of data is true here
print(data);
if (data == "true") {
return true;
} else {
return false;
}
}
#override
void initState() {
super.initState();
checkIsLoggedIn();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: checkIsLoggedIn() == true ? Profile() : LoginScreen());
//error : checkIsLoggedin () function returns false in my case.
}
}
Use then() to wait for the Future. Then, assign the result with setState().
class _HomePageState extends State<HomePage> {
Future<bool> checkIsLoggedIn() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var data = await prefs.getString('isAuthenticated');
// the value of data is true here
print(data);
if (data == "true") {
return true;
} else {
return false;
}
}
var isLoggedIn = false; // HERE
#override
void initState() {
super.initState();
checkIsLoggedIn().then((result) { // HERE
setState(() {
isLoggedIn = result;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: isLoggedIn == true // HERE
? Profile()
: LoginScreen());
}
}
When getting into problems, I do recommend you are first looking into the warnings you are getting around the problematic area. If you are using a default Flutter project, you should have the lint unrelated_type_equality_checks activated since it is part of the Core set of Dart Lints:
DON'T Compare references of unrelated types for equality.
Comparing references of a type where neither is a subtype of the other most likely will return false and might not reflect programmer's intent.
https://dart-lang.github.io/linter/lints/unrelated_type_equality_checks.html
This would give the following warning in your IDE with a link to the documentation I linked to previously:
The problem is that your checkIsLoggedIn() method have the following signature which means it returns an object of the type Future<bool>:
Future<bool> checkIsLoggedIn() async {
And you are then comparing this against true which are of the type bool. If you are doing an equality check against these two different types, it will ALWAYS end up returning false, which is also what you are observing.
A Future also means that you are handling a value that are potentially still not created. So when you are running your checkIsLoggedIn() == true it does not even know yet if the bool inside your Future<bool> are true or false.
The solution are to either make it so checkIsLoggedIn() returns bool, which cannot be done in this case since the method are handling asynchronously logic.
Another solution would then be to await the returned Future from checkIsLoggedIn(). But we cannot use await unless we mark the method, we are inside, as async. But for Flutter, we are not allowed to have build() marked as async.
So a solution would instead be to handle the asynchronously nature of checkIsLoggedIn() by using FutureBuilder:
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
Which are a Widget made for handling the building of the GUI when we don't know the value of the Future, but then triggers an automatic rebuild when the value of the Future are received.
You can use .getbool instead .getString
To wait for checkIsLoggedIn result you can use a FutureBuilder.
Here an example :D
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
Future<bool> checkIsLoggedIn() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final isAuthenticated = await prefs.getBool('isAuthenticated');
return isAuthenticated;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<bool>(
future: checkIsLoggedIn(),
builder: (context, snapshot) {
// SHOW LOADING WHILE PREFS IS GETTING DATA
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
// SHOW SCREEN ACCORDING DATA
return snapshot.data! //
? const Profile()
: const LoginScreen();
},
),
);
}
}
Here is my stateful widget and url is a property pass it to the widget from parent widget. I don't know where did I go wrong?? I created a future builder widget that has getData() as a future. But the print statement inside was not executed ever. Why is that and it returns me always null value, and this results me a red container appearing on screen and not the table widget.
class TimeTable extends StatefulWidget {
final url;
const TimeTable({Key? key,required this.url}) : super(key: key);
#override
_TimeTableState createState() => _TimeTableState();
}
class _TimeTableState extends State<TimeTable> {
Future<List<Train>> getData() async{
final list = await TrainClient(url: widget.url).getName();
print("this line not executed");
return list;
}
#override
Widget build(BuildContext context) {
return Scaffold(body: FutureBuilder(
future: getData(),
builder: (context,projectSnap){
if(projectSnap.connectionState == ConnectionState.none ||
projectSnap.data == null) {
return Container(color: Colors.red,);
}
return buildDataTable(trains: projectSnap.data);
}));
}
}
getData is a future method and it returns a list, The list gets printed when I call that object Train Client. I had my print statement inside TrainClient class to check whether the list is created successfully.
Here is the code of TrainClient
class TrainClient {
final String url;
TrainClient({required this.url});
Future<List<Train>> getName() async {
final uri = Uri.parse(url);
final response = await get(uri);
if (response.statusCode == 200) {
print("ulla");
final data = json.decode(response.body);
final result = data["RESULTS"]["directTrains"]["trainsList"];
final list = result.map((json) => Train.fromJson(json));
print(list);
return list;
}else{
throw Exception();
}
}
}
The TrainClient class has no error since it printed the list successfully as shown below
(Instance of 'Train', Instance of 'Train', Instance of 'Train', ..., Instance of 'Train', Instance of 'Train')
You should always obtain future earlier (in initState/didChangeDependencies).
Each time your build is executed, new future is created. So it never finishes, if your widget rebuilds often.
late final _dataFuture = getData();
...
FutureBuilder(
future: _dataFuture,
builder: (context,projectSnap){
...
}
);
I have noticed a new lint issue in my project.
Long story short:
I need to use BuildContext in my custom classes
flutter lint tool is not happy when this being used with aysnc method.
Example:
MyCustomClass{
final buildContext context;
const MyCustomClass({required this.context});
myAsyncMethod() async {
await someFuture();
# if (!mounted) return; << has no effect even if i pass state to constructor
Navigator.of(context).pop(); # << example
}
}
UPDATE: 17/September/2022
It appears that BuildContext will soon have a "mounted" property
So you can do:
if (context.mounted)
It basically allows StatelessWidgets to check "mounted" too.
Reference: Remi Rousselet Tweet
Update Flutter 3.7+ :
mounted property is now officially added to BuildContext, so you can check it from everywhere, whether it comes from a StatefulWidget State, or from a Stateless widget.
While storing context into external classes stays a bad practice, you can now check it safely after an async call like this :
class MyCustomClass {
const MyCustomClass();
Future<void> myAsyncMethod(BuildContext context) async {
Navigator.of(context).push(/*waiting dialog */);
await Future.delayed(const Duration(seconds: 2));
if (context.mounted) Navigator.of(context).pop();
}
}
// Into widget
#override
Widget build(BuildContext context) {
return IconButton(
onPressed: () => const MyCustomClass().myAsyncMethod(context),
icon: const Icon(Icons.bug_report),
);
}
// Into widget
Original answer
Don't stock context directly into custom classes, and don't use context after async if you're not sure your widget is mounted.
Do something like this:
class MyCustomClass {
const MyCustomClass();
Future<void> myAsyncMethod(BuildContext context, VoidCallback onSuccess) async {
await Future.delayed(const Duration(seconds: 2));
onSuccess.call();
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
#override
Widget build(BuildContext context) {
return IconButton(
onPressed: () => const MyCustomClass().myAsyncMethod(context, () {
if (!mounted) return;
Navigator.of(context).pop();
}),
icon: const Icon(Icons.bug_report),
);
}
}
Use context.mounted*
In StatefulWidget/StatelessWidget or in any class that has BuildContext:
void foo(BuildContext context) async {
await someFuture();
if (!context.mounted) return;
Navigator.pop(context); // No warnings now
}
* If you're in a StatefulWidget, you can also use just mounted instead of context.mounted
If your class can extend from StatefulWidget then adding
if (!mounted) return;
would work!
EDIT
I had this issue again and again and here's the trick - use or declare variables using context before using async methods like so:
MyCustomClass{
const MyCustomClass({ required this.context });
final buildContext context;
myAsyncMethod() async {
// Declare navigator instance (or other context using methods/classes)
// before async method is called to use it later in code
final navigator = Navigator.of(context);
await someFuture();
// Now use the navigator without the warning
navigator.pop();
}
}
EDIT END
As per Guildem's answer, he still uses
if (!mounted) return;
so what's the point of adding more spaghetti code with callbacks? What if this async method will have to pass some data to the methods you're also passing context? Then my friend, you will have even more spaghetti on the table and another extra issue.
The core concept is to not use context after async bloc is triggered ;)
If you want to use mounted check in a stateless widget its possible by making an extension on BuildContext
extension ContextExtensions on BuildContext {
bool get mounted {
try {
widget;
return true;
} catch (e) {
return false;
}
}
}
and then you can use it like this
if (context.mounted)
Inspiration taken from GitHub PR for this feature and it passes the same tests in the merged PR
you can use this approach
myAsyncMethod() async {
await someFuture().then((_){
if (!mounted) return;
Navigator.of(context).pop();
}
});
In Flutter 3.7.0 BuildContext has the property mounted. It can be used both in StatelessWidget and StatefulWidgets like this:
void bar(BuildContext context) async {
await yourFuture();
if (!context.mounted) return;
Navigator.pop(context);
}
Just simpliy creat a function to call the navigation
void onButtonTapped(BuildContext context) {
Navigator.of(context).pop();
}
To avoid this in StatelessWidget you can refer to this example
class ButtonWidget extends StatelessWidget {
final String title;
final Future<String>? onPressed;
final bool mounted;
const ButtonWidget({
super.key,
required this.title,
required this.mounted,
this.onPressed,
});
#override
Widget build(BuildContext context) {
return Row(
children: [
const SizedBox(height: 20),
Expanded(
child: ElevatedButton(
onPressed: () async {
final errorMessage = await onPressed;
if (errorMessage != null) {
// This to fix: 'Do not use BuildContexts across async gaps'
if (!mounted) return;
snackBar(context, errorMessage);
}
},
child: Text(title),
))
],
);
}
}
I handle it with converting the function become not async and using then
Future<void> myAsyncMethod(BuildContext context) {
Navigator.of(context).push(/*waiting dialog */);
Future.delayed(const Duration(seconds: 2)).then(_) {
Navigator.of(context).pop();
});
}
just save your navigator or whatever needs a context to a variable at the beginning of the function
myAsyncMethod() async {
final navigator = Navigator.of(context); // 1
await someFuture();
navigator.pop(); // 2
}
DO NOT use BuildContext across asynchronous gaps.
Storing BuildContext for later usage can easily lead to difficult to diagnose crashes. Asynchronous gaps are implicitly storing BuildContext and are some of the easiest to overlook when writing code.
When a BuildContext is used from a StatefulWidget, the mounted property must be checked after an asynchronous gap.
So, I think, you can use like this:
GOOD:
class _MyWidgetState extends State<MyWidget> {
...
void onButtonTapped() async {
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return;
Navigator.of(context).pop();
}
}
BAD:
void onButtonTapped(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1));
Navigator.of(context).pop();
}
I am using a future provider to display a login page on load and then a loading indicator on loading. Here is my future provider
final loginProvider = FutureProvider.family((ref, UserInput input) =>
ref.read(authRepositoryProvider).doLogin(input.email, input.password));
In my UI I have this....
class LoginScreen extends HookWidget {
final TextEditingController emailEditingController = TextEditingController();
final TextEditingController passwordEditingController =
TextEditingController();
#override
Widget build(BuildContext context) {
var userInput =
UserInput(emailEditingController.text, passwordEditingController.text);
final login = useProvider(loginProvider(userInput));
return login.when(
data: (user) => Login(emailEditingController, passwordEditingController),
loading: () => const ProgressIndication(),
error: (error, stack) {
if (error is DioError) {
return Login(emailEditingController, passwordEditingController);
} else {
return Login(emailEditingController, passwordEditingController);
}
},
);
}
}
here is my doLogin function.
#override
Future<dynamic> doLogin(String email, String password) async {
try {
final response = await _read(dioProvider)
.post('$baseUrl/login', data: {'email': email, 'password': password});
final data = Map<String, dynamic>.from(response.data);
return data;
} on DioError catch (e) {
return BadRequestException(e.error);
} on SocketException {
return 'No Internet Connection';
}
}
I would like to know why it's stuck in the loading state. Any help will be appreciated.
First off, family creates a new instance of the provider when given input. So in your implementation, any time your text fields change, you're generating a new provider and watching that new provider. This is bad.
In your case, keeping the UserInput around for the sake of accessing the login state doesn't make a lot of sense. That is to say, in this instance, a FamilyProvider isn't ideal.
The following is an example of how you could choose to write it. This is not the only way you could write it. It is probably easier to grasp than streaming without an API like Firebase that handles most of that for you.
First, a StateNotifierProvider:
enum LoginState { loggedOut, loading, loggedIn, error }
class LoginStateNotifier extends StateNotifier<LoginState> {
LoginStateNotifier(this._read) : super(LoginState.loggedOut);
final Reader _read;
late final Map<String, dynamic> _user;
static final provider =
StateNotifierProvider<LoginStateNotifier, LoginState>((ref) => LoginStateNotifier(ref.read));
Future<void> login(String email, String password) async {
state = LoginState.loading;
try {
_user = await _read(authRepositoryProvider).doLogin(email, password);
state = LoginState.loggedIn;
} catch (e) {
state = LoginState.error;
}
}
Map<String, dynamic> get user => _user;
}
This allows us to have manual control over the state of the login process. It's not the most elegant, but practically, it works.
Next, a login screen. This is as barebones as they get. Ignore the error parameter for now - it will be cleared up in a moment.
class LoginScreen extends HookWidget {
const LoginScreen({Key? key, this.error = false}) : super(key: key);
final bool error;
#override
Widget build(BuildContext context) {
final emailController = useTextEditingController();
final passwordController = useTextEditingController();
return Column(
children: [
TextField(
controller: emailController,
),
TextField(
controller: passwordController,
),
ElevatedButton(
onPressed: () async {
await context.read(LoginStateNotifier.provider.notifier).login(
emailController.text,
passwordController.text,
);
},
child: Text('Login'),
),
if (error) Text('Error signing in'),
],
);
}
}
You'll notice we can use the useTextEditingController hook which will handle disposing of those, as well. You can also see the call to login through the StateNotifier.
Last but not least, we need to do something with our fancy new state.
class AuthPage extends HookWidget {
const AuthPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final loginState = useProvider(LoginStateNotifier.provider);
switch (loginState) {
case LoginState.loggedOut:
return LoginScreen();
case LoginState.loading:
return LoadingPage();
case LoginState.loggedIn:
return HomePage();
case LoginState.error:
return LoginScreen(error: true);
}
}
}
In practice, you're going to want to wrap this in another widget with a Scaffold.
I know this isn't exactly what you asked, but thought it might be helpful to see another approach to the problem.
I am developing a Flutter application using Bloc pattern. After success authentication, UserSate has User object. In all other Blocs, I need to access User object in UserState. I tried with getting UserBloc on other Bloc's constructor parameters and accessing User object. But it shows that User object is null. Anyone have a better solution?
class SectorHomeBloc extends Bloc<SectorHomeEvent, SectorHomeState> {
final OutletRepository outletRepository;
UserBloc userBloc;
final ProductRepository productRepository;
final ProductSubCategoryRepository productSubCategoryRepository;
final PromotionRepository promotionRepository;
final ProductMainCategoryRepository mainCategoryRepository;
SectorHomeBloc({
#required this.outletRepository,
#required this.userBloc,
#required this.productSubCategoryRepository,
#required this.productRepository,
#required this.promotionRepository,
#required this.mainCategoryRepository,
});
#override
SectorHomeState get initialState => SectorHomeLoadingState();
#override
Stream<SectorHomeState> mapEventToState(SectorHomeEvent event) async* {
try {
print(userBloc.state.toString());
LatLng _location = LatLng(
userBloc.state.user.defaultLocation.coordinate.latitude,
userBloc.state.user.defaultLocation.coordinate.longitude);
String _token = userBloc.state.user.token;
if (event is GetAllDataEvent) {
yield SectorHomeLoadingState();
List<Outlet> _previousOrderedOutlets =
await outletRepository.getPreviousOrderedOutlets(
_token, _location, event.orderType, event.sectorId);
List<Outlet> _featuredOutlets =
await outletRepository.getFeaturedOutlets(
_token, _location, event.orderType, event.sectorId);
List<Outlet> _nearestOutlets = await outletRepository.getOutletsNearYou(
_token, _location, event.orderType, event.sectorId);
List<Product> _newProducts = await productRepository.getNewItems(
_token, _location, event.orderType, event.sectorId);
List<Product> _trendingProducts =
await productRepository.getTrendingItems(
_token, _location, event.orderType, event.sectorId);
List<Promotion> _promotions = await promotionRepository
.getVendorPromotions(_token, event.sectorId);
yield SectorHomeState(
previousOrderedOutlets: _previousOrderedOutlets,
featuredOutlets: _featuredOutlets,
nearByOutlets: _nearestOutlets,
newItems: _newProducts,
trendingItems: _trendingProducts,
promotions: _promotions,
);
}
} on SocketException {
yield SectorHomeLoadingErrorState('could not connect to server');
} catch (e) {
print(e);
yield SectorHomeLoadingErrorState('Error');
}
}
}
The print statement [print(userBloc.state.toString());] in mapEventToState method shows the initial state of UserSate.
But, at the time of this code executing UserState is in UserLoggedInState.
UPDATE (Best Practice):
please refer to the answer here enter link description here
so the best way for that is to hear the changes of another bloc inside the widget you are in, and fire the event based on that.
so what you will do is wrap your widget in a bloc listener and listen to the bloc you want.
class SecondPage extends StatelessWidget {
const SecondPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocListener<FirstBloc, FirstBlocState>(
listener: (context, state) {
if(state is StateFromFirstBloc){
BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());}//or whatever you want
},
child: ElevatedButton(
child: Text('THIS IS NEW SCREEN'),
onPressed: () {
BlocProvider.of<SecondBloC>(context).add(SecondBloCEvent());
},
),
);
}
}
the lovely thing about listener is that you can listen anywhere to any bloc and do whatever you want
here is the official documentation for it
OLD WAY (NOT Recommended)
there is an official way to do this as in the documentation, called Bloc-to-Bloc Communication
and here is the example for this as in the documentation
class MyBloc extends Bloc {
final OtherBloc otherBloc;
StreamSubscription otherBlocSubscription;
MyBloc(this.otherBloc) {
otherBlocSubscription = otherBloc.listen((state) {
// React to state changes here.
// Add events here to trigger changes in MyBloc.
});
}
#override
Future<void> close() {
otherBlocSubscription.cancel();
return super.close();
}
}
sorry for the late update for this answer and thanks to #MJ studio
The accepted answer actually has a comment in the above example in the official docs saying "No matter how much you are tempted to do this, you should not do this! Keep reading for better alternatives!"!!!
Here's the official doc link, ultimately one bloc should not know about any other blocs, add methods to update your bloc and these can be triggered from blocListeners which listen to changes in your other blocs: https://bloclibrary.dev/#/architecture?id=connecting-blocs-through-domain
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());
},
),
);
}
}