How to check if Widget has mounted in flutter - dart

So in my app, I want to make an Ajax request as soon as the widget is mounted, not in initState(). Similar to ComponentWillMount() in react

if the Widget has not mounted then return. Do it before the setState method
if (!mounted) return;
setState(() {});
or
if (mounted) {
//Do something
};
setState(() {});

If you want to execute some code as soon as the widget loaded , you could simply put this code in the initstate like the following ;
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => yourFunction(context));
}
In this way , the yourFunction will be exectued as soon as the first frame of the widget loaded on the screen.

I don't think it's currently possible.
Here's the mounted property: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/framework.dart#L974
bool get mounted => _element != null;
And here's when _element is set: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/framework.dart#L3816
_state._element = this
And I don't see any hook around this code that would inform us.
Why not use initState anyway? It's probably what you want. Here's the comment above the mounted property: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/framework.dart#L967
/// After creating a [State] object and before calling [initState], the
/// framework "mounts" the [State] object by associating it with a
/// [BuildContext]. The [State] object remains mounted until the framework

Simply do this as follows.
if (this.mounted) {
setState(() {
//Your code
});
}

I know this answer comes a little bit late but...
Inside your method, you should have something like this:
if(mounted){
setState(() {});
}
This would help, to rebuild the UI only if something changes.
I use this myself inside a method, where I fill my list with users from firestore.
The mounted property helps to avoid the error, when you trying to call setState before build.

Related

Navigating back to page from FutureBuilder

I have a submission form for my app where I have some data the user fills out in a form. I need to GET from an external API in the process, and use that data to create an entry in the database. All this happens once a Submit button is pressed, then after that I want to be able to go back to my homepage route.
I'm not sure how to get data from a Future function without using FutureBuilder, even though I don't need to build a widget, I just need the data.
This is what I have currently:
_populateDB() {
return new FutureBuilder(
future: fetchPost(latitude, longitude),
builder: (context, snapshot) {
if (snapshot.hasData) {
_createJson(snapshot.data);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomeScreen()
),
);
} else if (snapshot.hasError) {
return new Text("${snapshot.error}");
}
return new CircularProgressIndicator();
},
);
}
The _populateDB() function is being called when a button is pressed on the screen. What I would like to do is get data from fetchPost(latitude, longitude), use that data in the function _createJson(snapshot.data), and finally go back to the HomeScreen().
I haven't implemented _createJson(snapshot.data) yet, but currently when I call this method with onPressed, it does not go back to the HomeScreen(), and I'm not sure why.
You can get data from a Future function in asynchronous way or in synchronous way.
1 Asynchronous way
It's simple, you can use Native Future API from dart. The method then is a callback method that is called when your Future is completed. You can also use catchError method if your future was completed with some error.
fetchPost(latitude, longitude).then(
(fetchPostResultsData) {
if (fetchPostResultsData != null)
print (fetchPostResultsData);
} ).catchError(
(errorFromFetchPostResults){
print(errorFromFetchPostResults);
}
);
With this Approach your UI isn't blocked waiting results from network.
2 Synchronous way
You can use Dart key words async and await to keep your calls synchronized. In your case you have to transform your _populateDB method in an async method and await from fetchPost results.
_populateDB() async {
var data = await fetchPost(latitude, longitude);
// just execute next lines after fetchPost returns something.
if (data !=null ){
_createJson(snapshot.data);
//... do your things
}
else {
//... your handle way
}
}
With this approach your _populateDB function will wait the results from fetchPost blocking the UI Isolete and just after getting the results will execute the next instructions.
About Navigation if your HomeScreen is the previous the previous widget on stack you just need Navigator.pop(context) call but if there are others widgets in the Stack above of your HomeScreen is a better choice use Navigator.pushReplacement call.
This article shows in details with illustrations how the effects of Navigator methods. I hope it helps.
Use the below code snippet to solve it.
Future.delayed(const Duration(milliseconds: 0))
.then((value) => Navigator.pushNamed(context, '/routeName'));

How do we remove the inline code listener?

Suppose we have added listener to a changeNotifier object in a view like following:
someChangeNotifierObject.addListener((){ if (this.mounted) setState(){}});
Do we need to remove the listener explicitly when the view is getting disposed?
What would be the impact if not removing it?
How to remove it since it is an inline code?
If you add a listener to a super object (an object located outside of current state) and you use setState inside of the listener - in that case you should remove it explicitly on current state dispose.
Otherwise, setState will throw an exception when the object notifies its listeners, as the state inside of which you added a listener would have been disposed to that moment.
Do not use inline functions in addListener. If you need mounted - it can be accessed anywhere throughout the State, except for static methods. Hence, simply create a new function inside of the current State class.
e.g.
#override
void initState() {
super.initState();
someChangeNotifierObject.addListener(myListenerFunc);
}
#override
void dispose() {
someChangeNotifierObject.removeListener(myListenerFunc);
super.dispose();
}
void myListenerFunc() {
print("Heya the object has changed!");
setState(() {
// re-render current stateful widget.
});
}
However, it is simply a good practice to remove listeners on dispose - no matter whether you use State's methods inside of a listener or not.

Flutter image preload

Is it possible to preload somehow the image on the app start? Like I have an background image in my drawer but for the first time when I open the drawer I can see the image blinks like it is fetched from assets and then displayed and it gives bad experience to me once I see it for the first time other openings of the drawer are behaving as expected because it is cached. I would like to prefetch it on the app load so there is no such effect.
Use the precacheImage function to start loading an image before your drawer is built. For example, in the widget that contains your drawer:
class MyWidgetState extends State<MyWidget> {
#override
void initState() {
// Adjust the provider based on the image type
precacheImage(new AssetImage('...'));
super.initState();
}
}
I had problems with the upper solution which uses precacheImage() inside initState. The code below resolved them. Also note that you might not see expected results in the debug mode but only in the release mode.
Image myImage;
#override
void initState() {
super.initState();
myImage= Image.asset(path);
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(myImage.image, context);
}
There is a good article on this subject : https://alex.domenici.net/archive/preload-images-in-a-stateful-widget-on-flutter
It should look something like this
class _SampleWidgetState extends State<SampleWidget> {
Image image1;
Image image2;
Image image3;
Image image4;
#override
void initState() {
super.initState();
image1 = Image.asset("assets/image1.png");
image2 = Image.asset("assets/image2.png");
image3 = Image.asset("assets/image3.png");
image4 = Image.asset("assets/image4.png");
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(image1.image, context);
precacheImage(image2.image, context);
precacheImage(image3.image, context);
precacheImage(image4.image, context);
}
#override
Widget build(BuildContext context) {
return Container(
child: Stack(
children: <Widget>[
image1,
image2,
image3,
image4,
],
),
);
}
}
To get rid of the "blink", you can simply use the FadeInImage class in combination with transparent_image, which will fade instead of appearing instantly. Usage, in your case, looks as follow:
// you need to add transparent_image to your pubspec and import it
// as it is required to have the actual image fade in from nothing
import 'package:transparent_image/transparent_image.dart';
import 'package:flutter/material.dart';
...
FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image: AssetImage('image.png'),
)
The precacheImage() function (as used in most of the answers) returns a Future, and for certain use case scenarios it can be really useful. For instance, in my app I needed to load an external image and like everyone else did not want to experience the flickering. So I ended up with something like this:
// show some sort of loading indicator
...
precacheImage(
NetworkImage(),
context,
).then((_) {
// replace the loading indicator and show the image
// (may be with some soothing fade in effect etc.)
...
});
Please note that, in the above example I wanted to illustrate how the Future can be potentially used. The comments are just to help express the idea. Actual implementation has to be done in a Fluttery way.
I had the following problem: image requires some space. So after loading, it pushed UI down which is not good for UX. I decided to create a builder to display an empty (or loading) container until the image is loaded and after that display my UI.
Looks like precacheImage returns future which is resolved. The tricky part for me was FutureBuilder and snapshot.hasData which was always false because future is resolved with null. So I added future transformation to fix snapshot.hasData:
precacheImage(this.widget.imageProvider, context).then((value) => true)
I'm not sure is it ok to call precacheImage multiple times so I decided to wrap it into StatefulWidget.
You can check the final builder here
Use builder the following way:
return PreloadingImageBuilder(
imageProvider: AssetImage("assets/images/logo-main.png"),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Scaffold(
...
);
} else {
return Container();
}
},
);
Depending on your logic and UX, something like this can work too (when pushing a new route with images):
Future.wait([
precacheImage(AssetImage("assets/imageA.png"), context),
]).then((value) {
Navigator.pop(context);
Navigator.pushReplacement(
context,
YourRouteRoute()
);
});
late AssetImage assetImage;
#override
void initState() {
assetImage = const AssetImage("$kImagePath/bkg2.png");
super.initState();
}
#override
void didChangeDependencies() {
precacheImage(assetImage, context);
super.didChangeDependencies();
}

Flutter BLoC pattern - How can I navigate to another screen after a stream event?

My question is about navigation used with the BLoC pattern.
In my LoginScreen widget I have a button that adds an event into the EventSink of the bloc. The bloc calls the API and authenticates the user.
Where in the LoginScreen Widget do I have to listen to the stream, and how do I navigate to another screen after it returns a success status?
Use BlockListener.
BlocListener(
bloc: _yourBloc,
listener: (BuildContext context, YourState state) {
if(state is NavigateToSecondScreen){
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {return SecondScreen();}));
}
},
child: childWidget
)
The navigator is not working in blocBuilder, because in blocBuilder, you can only return a widget
But BlocListener solved it for me.
Add this code:
BlocListener(
bloc: _yourBloc,
listener: (BuildContext context, YourState state) {
if(state is NavigateToSecondScreen){
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {return SecondScreen();}));
}
},
child: childWidget
)
First of all: if there isn't any business logic, then there isn't any need to go to YourBloc class.
But from time to time some user's activity is required to perform some logic in Bloc class and then the Bloc class has to decide what to do next: just rebuild widgets or show dialog or even navigate to next route. In such a case, you have to send some State to UI to finish the action.
Then another problem appears: what shall I do with widgets when Bloc sends State to show a toast?
And this is the main issue with all of this story.
A lot of answers and articles recommend to use flutter_block. This library has BlocBuilder and BlocListener. With those classes you can solve some issues, but not 100% of them.
In my case I used BlocConsumer which manages BlocBuilder and BlocListener and provides a brilliant way to manage states.
From the documentation:
BlocConsumer<BlocA, BlocAState>(
listenWhen: (previous, current) {
// Return true/false to determine whether or not
// to invoke listener with state
},
listener: (context, state) {
// Do stuff here based on BlocA's state
},
buildWhen: (previous, current) {
// Return true/false to determine whether or not
// to rebuild the widget with state
},
builder: (context, state) {
// Return widget here based on BlocA's state
}
)
As you can see with BlocConsumer, you can filter states: you can easily define states to rebuild widgets and states to show some popups or navigate to the next screen.
Something like this:
if (state is PhoneLoginCodeSent) {
// Dispatch here to reset PhoneLoginFormState
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return VerifyCodeForm(phoneLoginBloc: _phoneLoginBloc);
},
),
);
return;
});
}

Flutter: How to prevent banner ad from showing if view is already disposed

I am currently experimenting with banner ads from the firebase_admob plugin. The process to show and dispose them is pretty straightforward, I do it in initState() and dispose().
The code to create and display the add looks like this:
_bannerAd = createBannerAd();
_bannerAd
..load().then((loaded) {
if (loaded) {
_bannerAd..show();
}
});
However, as I am calling show() asynchronously, it is possible that the view was already closed when the ad is being shown (i.e. by clicking back button really fast). In that case, the dispose() method will never be called and the ad will be "stuck" on the bottom of the screen.
How can I solve this problem? Am I using the banner ad wrong or is it possible to detect if the view was already changed? I tried using the "mounted" property of the state but it didn't seem to work.
Just check "this.mounted" property of state class before showing the add.
_bannerAd = createBannerAd();
_bannerAd
..load().then((loaded) {
if (loaded && this.mounted) {
_bannerAd..show();
}
});
From https://github.com/flutter/flutter/issues/21474#issuecomment-535188820, that's a little hack but it works for me.
You can add a little delay in your dispose method like this:
static void hideBannerAd() {
Future.delayed(Duration(milliseconds: 500), () {
if (_bannerAd != null) _bannerAd.dispose();
_bannerAd = null;
});
}
500 milliseconds is enough.

Resources