Getting NULL value for async function (after using await) then updating to the new value
Asked Answered
I

2

8

When I run my app it throws a lot of errors, and the red/yellow error screen on my device, which refreshes automatically and shows me the expected output then.

From the log I can see that first my object is return as null, which somehow later updates and I get the output.

I have recently started Android Dev (Flutter)

I tried to follow a number of online guides and also read related questions on async responses but no troubleshooting has been helping me. My biggest problem being that I can't figure out what exactly is the problem.

In my _AppState class:

  void initState() {
    super.initState();
    fetchData();
  }

  fetchData() async {
    var cityUrl = "http://ip-api.com/json/";
    var cityRes = await http.get(cityUrl);
    var cityDecodedJson = jsonDecode(cityRes.body);
    weatherCity = WeatherCity.fromJson(cityDecodedJson);
    print(weatherCity.city);
    var weatherUrl = "https://api.openweathermap.org/data/2.5/weather?q=" +
        weatherCity.city +
        "," +
        weatherCity.countryCode +
        "&appid=" +
        //Calling open weather map's API key from apikey.dart
        weatherKey;
    var res = await http.get(weatherUrl);
    var decodedJson = jsonDecode(res.body);
    weatherData = WeatherData.fromJson(decodedJson);
    print(weatherData.weather[0].main);
    setState(() {});
  }

Expected output (Terminal):

Mumbai
Rain

Actual output (Terminal): https://gist.github.com/Purukitto/99ffe63666471e2bf1705cb357c2ea32 (Actual error was crossing the body limit of StackOverflow)

ScreenShots: At run initial After a few seconds

Impartial answered 19/8, 2019 at 6:29 Comment(2)
Could you please print content of decodedJson and content of weatherData to make sure the data exists?Fornax
the problem is straightforward...the fetchData function is async so it doesn't stop the widget from building. And when it builds first, the weatherData is null. But after some time the API call gives some response and then you call setState and this time it works because now weatherData is not null.Belak
H
8

The async and await is a mechanism for handling Asynchronous programming in Dart.

Asynchronous operations let your program complete work while waiting for another operation to finish.

So whenever a method is marked as async, your program does not pause for completion of the method and just assumes that it will complete at some point in the future.

Example: Incorrectly using an asynchronous function

The following example shows the wrong way to use an asynchronous function getUserOrder().

String createOrderMessage () {
  var order = getUserOrder(); 
  return 'Your order is: $order';
}

Future<String> getUserOrder() {
  // Imagine that this function is more complex and slow
  return Future.delayed(Duration(seconds: 4), () => 'Large Latte'); 
}

main () {
  print(createOrderMessage());
}

If you run the above program it will produce the below output -

Your order is: Instance of '_Future<String>'

This is because, since the return type of the method is marked as Future, the program will treat is as an asynchronous method.

To get the user’s order, createOrderMessage() should call getUserOrder() and wait for it to finish. Because createOrderMessage() does not wait for getUserOrder() to finish, createOrderMessage() fails to get the string value that getUserOrder() eventually provides.


Async and await

The async and await keywords provide a declarative way to define asynchronous functions and use their results.

So whenever you declare a function to be async, you can use the keyword await before any method call which will force the program to not proceed further until the method has completed.


Case in point

In your case, the fetchData() function is marked as async and you are using await to wait for the network calls to complete.

But here fetchData() has a return type of Future<void> and hence when you call the method inside initState() you have to do so without using async/ await since initState() cannot be marked async.

So the program does not wait for completion of the fetchData() method as a whole and tries to display data which is essentially null. And since you call setState() after the data is loaded inside fetchData(), the screen refreshes and you can see the details after some time.

Hence the red and yellow screen error.


Solution

The solution to this problem is you can show a loading indicator on the screen until the data is loaded completely.

You can use a bool variable and change the UI depending the value of that variable.

Example -

class _MyHomePageState extends State<MyHomePage> {
  bool isLoading = false;

  void initState() {
    super.initState();
    fetchData();
 }

 fetchData() async {
   setState(() {
     isLoading = true; //Data is loading
   });
   var cityUrl = "http://ip-api.com/json/";
   var cityRes = await http.get(cityUrl);
   var cityDecodedJson = jsonDecode(cityRes.body);
   weatherCity = WeatherCity.fromJson(cityDecodedJson);
   print(weatherCity.city);
   var weatherUrl = "https://api.openweathermap.org/data/2.5/weather?q=" + weatherCity.city + "," +
    weatherCity.countryCode +
    "&appid=" +
    //Calling open weather map's API key from apikey.dart
    weatherKey;
    var res = await http.get(weatherUrl);
    var decodedJson = jsonDecode(res.body);
    weatherData = WeatherData.fromJson(decodedJson);
    print(weatherData.weather[0].main);
    setState(() {
      isLoading = false; //Data has loaded
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: isLoading ? Center(child : CircularProgressIndicator())
      : Container(), //Replace this line with your actual UI code
    );
  }
}

Hope this helps!

Heyerdahl answered 19/8, 2019 at 7:34 Comment(1)
But I chose to put the condition while returning the Scaffold instead of the body as I was also using the value in the AppBarImpartial
Z
0

You have to wait until the data from api arrive. The quick fix is to place a null-coalescing operator on the variable that hold the future data like this:

String text;

fetchData() async {
//...
  text = weatherData.weather[0].main ?? 'Waiting api response...';
//...
}

// in your build method
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Container(
      child: Text(text), //this will render "Waiting api response" first, and when the api result arrive, it will change
    ),
  );
}

Otherwise, you could use the futureBuilder widget to achieve that. But you must place each api to different function, and change that to Future, so it has a return value.

Future fetchDataCity() async {
  // your code
  weatherCity = WeatherCity.fromJson(cityDecodedJson);
  return weatherCity;
}

Future fetchDataWeather() async {
  // your code
  weatherData = WeatherData.fromJson(decodedJson);
  return weatherData;
}

// in your build method
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Container(
      child: FutureBuilder(
        future: fetchDataWeather(), // a previously-obtained Future or null
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          switch (snapshot.connectionState)
            case ConnectionState.active:
            case ConnectionState.waiting:
              return Text('Awaiting result...'); //or a placeholder
            case ConnectionState.done:
              if (snapshot.hasError){
                return Text('Error: ${snapshot.error}');
              } else {
                return Text('Error: ${snapshot.data}');
            }
         },
      ) //FutureBuilder
    ),
  );
}
Zannini answered 19/8, 2019 at 7:33 Comment(1)
Thanks for the insight , though this seems like it would work I found @siddharth-patankar 's answer easier to understandImpartial

© 2022 - 2024 — McMap. All rights reserved.