6

My main objective is to show a CircularProgressIndicator before I get a location address placename but I keep getting this error The getter 'placeName' was called on null..I did try to check the null value in futureBuilder but I believe my implementation is wrong. Could you please take a look ? This is my AppData class

class AppData extends ChangeNotifier {
 Address pickUpLocation;
 void updatePickUpLocationAddress(Address pickUpAddress) {
    pickUpLocation = pickUpAddress;
    notifyListeners();
  }
}

and this is the Address class

class Address {
  String placeFormattedAddress;
  dynamic placeName;
  String placeId;
  double latitude;
  double longitude;

  Address(
      {this.placeFormattedAddress,
      this.placeName,
      this.placeId,
      this.latitude,
      this.longitude});
}

Now in my MainScreen I am using it like this but the error persisting.

 @override
  Widget build(BuildContext context) {
\\\
body: Stack( 
\\\
Flexible(
    child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
            FutureBuilder (
                future: Provider.of < AppData > (context)
                .pickUpLocation
                .placeName,
                builder: (context, snapshot) {
                    if (snapshot.data == null) {
                        return CircularProgressIndicator();
                    } else {
                        return Text(
                            Provider.of < AppData > (context)
                            .pickUpLocation
                            .placeName,

                            style: TextStyle(fontSize: 12.0),
                            overflow: TextOverflow.ellipsis,
                            
                        );
                    }
                }),
          
        ],
    ),
)
Pannam T
  • 389
  • 7
  • 20
  • have a look at how I combined the features of FutureProvider & ChangeNotifierProvider: https://stackoverflow.com/questions/72533330/how-to-combine-features-of-futureprovider-futurebuilder-waiting-for-async-data – Cedric Jun 07 '22 at 15:05

3 Answers3

14

There are a few things to be aware of.

  1. Your Future... isn't really a Future. You're just evaluating a synchronous property of your AppData Object provided by your Provider... and that's really it. Therefore the FutureBuilder should immediately evaluate its expression without giving that "loading" experience (it should render the "loaded" widget after a first rebuild).
  2. Say you're using a Provider to return the Future you're looking for (and that's ok), which is something close to your implementation: something like that is erroneous since it would "stress" your widget tree with handling two state changes (the provider changing and/or the future being completed). Furthermore, your future gets fired every time you rebuild this Widget: that's not good (usually, firing a Future multiple times means asking your backend for the same data multiple times).

To fix your situation you have to:

  1. Let your getter properly return and handle a Future;
  2. Initialize the call to your getter (i.e. the Future call) before you call the build method.

Here's how I'd change your provider model/class:

class AppData extends ChangeNotifier {
 Address? pickUpLocation;

 Future<void> updatePickUpLocationAddress() async {
    // TODO: Maybe add error handling?
    var pickUpLocation = await Future.delayed(Duration(seconds: 5));  // Request mock
    
    notifyListeners();
  }
}

Now, to initialize the Future you either do so in the above Provider (in the Constructor of AppData), or you have to change your Widget to be Stateful so that we can access to the initState() method, in which we can initialize the Future without worrying about multiple calls. In your (now Stateful) Widget:

var myFuture;
// ...
void initState() {
  myFuture = Provider.of<AppData>(context).updatePickUpLocationAddress();
}
// ...
Widget build (BuildContext context) {
  return // ...
         FutureBuilder(
           future: myFuture, // already initialized, won't re-initalize when build() is called
           builder: (ctx, snapshot) => // ...
}

That should be it for this Provider + Future pattern, although a final consideration should be mentioned.

Take this made up example and look what happens (i.e. read the console) when you fire the tap some times: if ChildWidget1 is disposed, then myFuture will be fired again when it gets re-loaded in the tree. This happens because ChildWidget1 is entirely removed from the tree and then re-added again to it, i.e. it's disposed and re-initialized.

This is useful to remember because having a StatefulWidget doesn't mean we are "immune" from side effects like this one.

venir
  • 1,809
  • 7
  • 19
1
Flexible(
        child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
                if (Provider.of < AppData > (context)
                    .pickUpLocation ==
                    null)
                    CircularProgressIndicator(),

                    if (Provider.of < AppData > (context)
                        .pickUpLocation !=
                        null)
                        Text(
                            Provider.of < AppData > (context)
                            .pickUpLocation
                            .placeName,
                            style: TextStyle(fontSize: 12.0),
                            overflow: TextOverflow.ellipsis,
                        ),
                      ),  
                     )  
Pannam T
  • 389
  • 7
  • 20
0

For anyone who is facing this problem You can easily click on Options+enter on Mac and click on "wrap with Builder" then just pass the context to your future function

 child: Builder(
                builder: (context) {
                  return FutureBuilder(
                      future: futureFunctionHere(context),
                      builder: (context, AsyncSnapshot snapshot) {
Shalabyer
  • 555
  • 7
  • 23