Flutter: build method runs before initState finishes

Issue

I’m trying to make a weather app with Flutter. But for some reason, the build() method runs before the initState() method finishes. The thing is, all the state variables are initialized using the setState() method inside the initState(), whose variables are to be used in build() method. I guess the problem is that Flutter is trying to access those state variables before the setState() method, which keeps throwing me the error: A non-null String must be provided to a Text widget. I know these codes are too long to read. But I’d appreciate it if you could help me with this.

import 'package:flutter/material.dart';

import "package:climat/screens/LoadingScreen.dart";
import "package:climat/screens/MainScreen.dart";
import "package:climat/screens/SearchScreen.dart";

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(
        fontFamily: "Open Sans",
      ),
      title: "Climat",
      initialRoute: "/",
      onGenerateRoute: (RouteSettings routeSettings) {
        dynamic routes = <String, WidgetBuilder>{
          "/": (context) => LoadingScreen(),
          "/index": (context) => MainScreen(),
          "/search": (context) => SearchScreen(),
        };
        WidgetBuilder builder = routes[routeSettings.name];
        return MaterialPageRoute(builder: (context) => builder(context));
      },
    ),
  );
}
import "package:flutter/material.dart";
import "package:flutter_spinkit/flutter_spinkit.dart";

import "package:climat/services/GeolocatorHelper.dart";
import "package:climat/services/NetworkHelper.dart";

import "package:climat/utilities/constants.dart";

class LoadingScreen extends StatefulWidget {
  @override
  _LoadingScreenState createState() => _LoadingScreenState();
}

class _LoadingScreenState extends State<LoadingScreen> {
  Future<void> getWeatherData() async {
    Map<String, double> coordinates = await GeolocatorHelper().getGeoData();
    final NetworkHelper networkHelper = NetworkHelper(
        uri:
            "$endPoint&lat=${coordinates["latitude"]}&lon=${coordinates["longitude"]}");
    final dynamic data = await networkHelper.getData();
    return await Navigator.pushNamed(context, "/index", arguments: data);
  }

  @override
  void initState() {
    this.getWeatherData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Climat"),
      ),
      body: SafeArea(
        child: Center(
          child: SpinKitRing(
            color: Colors.redAccent,
          ),
        ),
      ),
    );
  }
}
import "package:flutter/material.dart";

import "package:climat/services/WeatherHelper.dart";
import "package:climat/services/NetworkHelper.dart";

import "package:climat/utilities/constants.dart";

class MainScreen extends StatefulWidget {
  @override
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  Color backgroundColor;
  String cityName;
  int temperature;
  String status;
  String image;
  int minTemperature;
  int maxTemperature;
  int humidity;

  Future<void> updateUI({String userInput = null}) async {
    dynamic weatherData;
    if (userInput == null) {
      weatherData = ModalRoute.of(context).settings.arguments;
    } else {
      final NetworkHelper networkHelper =
          NetworkHelper(uri: "$endPoint&q=$userInput");
      weatherData = await networkHelper.getData();
    }
    final int weatherCode = weatherData["weather"][0]["id"];
    final WeatherHelper weatherHelper = WeatherHelper(
      weatherCode: weatherCode,
    );
    setState(() {
      this.backgroundColor = weatherHelper.getBackgroundColor();
      this.cityName = weatherData["name"];
      this.temperature = weatherData["main"]["temp"].toInt();
      this.status = weatherHelper.getWeatherStatus();
      this.minTemperature = weatherData["main"]["temp_min"].toInt();
      this.maxTemperature = weatherData["main"]["temp_max"].toInt();
      this.humidity = weatherData["main"]["humidity"];
      this.image = weatherHelper.getWeatherImage();
    });
  }

  @override
  void initState() {
    this.updateUI();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: this.backgroundColor,
      appBar: AppBar(
        title: Text("Climat"),
      ),
      body: SafeArea(
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                this.cityName,
                style: TextStyle(
                  fontSize: 24.0,
                  color: Colors.white,
                ),
              ),
              SizedBox(
                height: 30.0,
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Image.asset(
                    "./assets/images/${this.image}",
                    scale: 4.0,
                  ),
                  SizedBox(
                    width: 30.0,
                  ),
                  Text(
                    "${this.temperature}°C",
                    style: TextStyle(
                      fontSize: 96.0,
                      color: Colors.white,
                    ),
                  ),
                ],
              ),
              SizedBox(
                height: 30.0,
              ),
              Text(
                this.status.toUpperCase(),
                style: TextStyle(
                  fontSize: 24.0,
                  color: Colors.white,
                ),
              ),
              SizedBox(
                height: 10.0,
              ),
              Text(
                "MIN / MAX : ${this.minTemperature.toString()} / ${this.maxTemperature.toString()}",
                style: TextStyle(
                  fontSize: 24.0,
                  color: Colors.white,
                ),
              ),
              SizedBox(
                height: 10.0,
              ),
              Text(
                "HUMIDITY : ${this.humidity}%",
                style: TextStyle(
                  fontSize: 24.0,
                  color: Colors.white,
                ),
              ),
              SizedBox(
                height: 30.0,
              ),
              ElevatedButton(
                onPressed: () async {
                  dynamic userInput =
                      await Navigator.pushNamed(context, "/search");
                  this.updateUI(
                    userInput: userInput.toString(),
                  );
                },
                style: ElevatedButton.styleFrom(
                  padding: EdgeInsets.symmetric(
                    vertical: 8.0,
                    horizontal: 16.0,
                  ),
                ),
                child: Text(
                  "Search by city name",
                  style: TextStyle(
                    fontSize: 20.0,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Solution

if you want to use Future function in initState and want it run once and complete before build, please consider to use WidgetsBinding.instance.addPostFrameCallback, like

@override
void initState() {
    WidgetsBinding.instance.addPostFrameCallback((_) async {
        await this.getWeatherData();
        setState(() { });
    });
}

and

@override
void initState() {
  WidgetsBinding.instance.addPostFrameCallback((_) async {
    await this.updateUI();
    setState(() { });        
  });
}

setState should be used for rebuild the widget

Answered By – Sam Chan

Answer Checked By – Marilyn (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *