Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to rebuild widget in Flutter when a change occurs

Tags:

flutter

dart

Edit: I've edited the code below to feature the method that fetches the data along with the widgets that build the train estimates (replacing any API information along the way with "API_URL" and "API_STOP_ID"). I hope this even better helps us figure out the problem! I really appreciate any information anyone can give -- I've been working very hard on this project! Thank you all again!

Original post: I have a ListView of ListTiles that each have a trailing widget which builds train arrival estimates in a new Text widget. These trailing widgets are updated every five seconds (proven by print statements). As a filler for when the app is fetching data from the train's API, it displays a "no data" Text widget which is built by _buildEstimatesNull().

However, the problem is that "no data" is still being shown even when the app has finished fetching data and _isLoading = false (proven by print statements). Still, even if that was solved, the train estimates would become quickly outdated, as the trailing widgets are updating every five seconds on their own but this would not be reflected in the actual app as the widgets were built on page load. Thus, I need a way to rebuild those trailing widgets whenever they fetch new information.

Is there a way to have Flutter automatically rebuild the ListTile's trailing widget every five seconds as well (or whenever _buildEstimatesS1 is updated / the internals of the trailing widget is updated)?

class ShuttleApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new ShuttleState();
  }
}

class ShuttleState extends State<ShuttleApp> {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new HomeState();
  }
}

class HomeState extends State<HomeScreen> {

  var _isLoading = true;

  void initState() {
    super.initState();
    _fetchData();
    const fiveSec = const Duration(seconds: 5);
    new Timer.periodic(fiveSec, (Timer t) {
      _fetchData();
    });
  }

  var arrivalsList = new List<ArrivalEstimates>();

  _fetchData() async {
    arrivalsList.clear();
    stopsList.clear();
    final url = "API_URL";
    print("Fetching: " + url);
    final response = await http.get(url);
    final busesJson = json.decode(response.body);
    if (busesJson["service_id"] == null) {
      globals.serviceActive = false;
    } else {
      busesJson["ResultSet"]["Result"].forEach((busJson) {
        if (busJson["arrival_estimates"] != null) {
          busJson["arrival_estimates"].forEach((arrivalJson) {
            globals.serviceActive = true;
            final arrivalEstimate = new ArrivalEstimates(
                arrivalJson["route_id"], arrivalJson["arrival_at"], arrivalJson["stop_id"]
            );
            arrivalsList.add(arrivalEstimate);
          });
        }
      });
    }
    setState(() {
      _isLoading = false;
    });
  }

  Widget _buildEstimateNull() {
    return new Container(
      child: new Center(
        child: new Text("..."),
      ),
    );
  }

  Widget _buildEstimateS1() {
    if (globals.serviceActive == false) {
      print('serviceNotActive');
      _buildEstimateNull();
    } else {
      final String translocStopId = "API_STOP_ID";
      final estimateMatches = new List<String>();
      arrivalsList.forEach((arrival) {
        if (arrival.stopId == translocStopId) {
          estimateMatches.add(arrival.arrivalAt);
        }
      });
      estimateMatches.sort();
      if (estimateMatches.length == 0) {
        print("zero");
        return _buildEstimateNull();
      } else {
        return new Container(
          child: new Center(
            child: new Text(estimateMatches[0]),
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        backgroundColor: const Color(0xFF171717),
        appBar: new AppBar(),
        body: new DefaultTextStyle(
          style: new TextStyle(color: const Color(0xFFaaaaaa),),
          child: new ListView(
            children: <Widget>[
              new ListTile(
                title: new Text('S1: Forest Hills',
                    style: new TextStyle(fontWeight: FontWeight.w500, fontSize: 20.0)),
                subtitle: new Text('Orange Line'),
                contentPadding: new EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
                trailing: _isLoading ? _buildEstimateNull() : _buildEstimateS1(),
              ),
            ],
          ),
        )
    );
  }

class ArrivalEstimates {
  final String routeId;
  final String arrivalAt;
  final String stopId;
  ArrivalEstimates(this.routeId, this.arrivalAt, this.stopId);
}

Thank you so much in advance for any help you can give! I really super appreciate it! :)

like image 549
ctrinh Avatar asked Jun 26 '18 00:06

ctrinh


2 Answers

There are a few ways you could tackle this. It is slightly difficult however to tell what's going on without seeing a bit more of your code - specifically how you're getting the data and what you're doing with it. But I think I can give you a sufficient answer anyways.

The simple way of doing this is to either:

  1. Have a StatefulWidget which keeps track of the build estimates for all of the items in the list. It should request data from your API, get the results, and then call setState(() => this.listData = data);. The call to setState is what tells the widget that it needs to rebuild.
  2. Have a StatefulWidget for each item in the list. They would all each perform an API request every 5 seconds, get the results, and then each would call setState(() => this.itemData = data);. This means multiple calls to the API etc.

The advantage of #1 is that you can batch API calls, whereas the advantage to #2 is that your build would change less overall (although the way flutter works, this would be pretty minimal)... so I would probably go with #1 if possible.

However, there is a better way of doing this!

The better way of doing this is to have some sort of API Manager (or whatever you want to call it) which handles the communication with your API. It probably would live higher up in your widget tree and would be started/stopped with whatever logic you want. Depending on how far up the widget tree is, you could either pass it into each child or more likely hold it in an InheritedWidget which could then be used to retrieve it from each list element or from the overall list.

The API manager would provide various streams - either with a bunch of named fields/methods or with a getStream(id) sort of structure depending on your API.

Then, within your various list elements, you would use StreamBuilder widgets to build each of the elements based on the data - by using a StreamBuilder you get a ConnectionState object that lets you know whether the stream has received any data yet so you can choose to show an isLoading type widget instead of the one that shows data.

By using this more advanced method, you get:

  • Maintainability
    • If your API changes, you only have to change the API manager
    • You can write better testing as the API interactions and the UI interactions are separated
  • Extensibility
    • If you, later on, use push notifications for updates rather than pinging a server every 5 seconds, that can be incorporated into the API manager so that it can simply update the stream without touching the UI

EDIT: as per OP's comments, they have already implemented more or less the first suggestion. However, there are a few problems with the code. I'll list them below and I've posted the code with a couple of changes.

  1. The arrivalsList should be replaced each time a new build is done rather than simply being changed. This is because dart compares the lists and if it finds the same list, it doesn't necessarily compare all of the elements. Also, while changing it in the middle of a function isn't necessarily going to cause problems, it's generally better to use a local variable and then change the value at the end. Note that the member is actually set within setState.
  2. If serviceActive == false, the return was missed from return _buildEstimateNull();.

Here's the code:

class HomeState extends State<HomeScreen> {

  var _isLoading = true;

  void initState() {
    super.initState();
    _fetchData();
    const fiveSec = const Duration(seconds: 5);
    new Timer.periodic(fiveSec, (Timer t) {
      _fetchData();
    });
  }

  var arrivalsList = new List<ArrivalEstimates>();

  _fetchData() async {
    var arrivalsList = new List<ArrivalEstimates>(); // *********** #1
    stopsList.clear();
    final url = "API_URL";
    print("Fetching: " + url);
    final response = await http.get(url);
    final busesJson = json.decode(response.body);
    if (busesJson["service_id"] == null) {
      print("no service id");
      globals.serviceActive = false;
    } else {
      busesJson["ResultSet"]["Result"].forEach((busJson) {
        if (busJson["arrival_estimates"] != null) {
          busJson["arrival_estimates"].forEach((arrivalJson) {
            globals.serviceActive = true;
            final arrivalEstimate = new ArrivalEstimates(
                arrivalJson["route_id"], arrivalJson["arrival_at"], arrivalJson["stop_id"]
            );
            arrivalsList.add(arrivalEstimate);
          });
        }
      });
    }
    setState(() {
      _isLoading = false;
      this.arrivalsList = arrivalsList; // *********** #1
    });
  }

  Widget _buildEstimateNull() {
    return new Container(
      child: new Center(
        child: new Text("..."),
      ),
    );
  }

  Widget _buildEstimateS1() {
    if (globals.serviceActive == false) {
      print('serviceNotActive');
      return _buildEstimateNull();  // ************ #2
    } else {
      final String translocStopId = "API_STOP_ID";
      final estimateMatches = new List<String>();
      print("arrivalsList length: ${arrivalsList.length}");
      arrivalsList.forEach((arrival) {
        if (arrival.stopId == translocStopId) {
          print("Estimate match found: ${arrival.stopId}");
          estimateMatches.add(arrival.arrivalAt);
        }
      });
      estimateMatches.sort();
      if (estimateMatches.length == 0) {
        print("zero");
        return _buildEstimateNull();
      } else {
        return new Container(
          child: new Center(
            child: new Text(estimateMatches[0]),
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        backgroundColor: const Color(0xFF171717),
        appBar: new AppBar(),
        body: new DefaultTextStyle(
          style: new TextStyle(color: const Color(0xFFaaaaaa),),
          child: new ListView(
            children: <Widget>[
              new ListTile(
                title: new Text('S1: Forest Hills',
                    style: new TextStyle(fontWeight: FontWeight.w500, fontSize: 20.0)),
                subtitle: new Text('Orange Line'),
                contentPadding: new EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
                trailing: _isLoading ? _buildEstimateNull() : _buildEstimateS1(),
              ),
            ],
          ),
        )
    );
  }
like image 164
rmtmckenzie Avatar answered Oct 04 '22 05:10

rmtmckenzie


Instead of clearing and re-using the arrivalsList, create a new list every time the data is fetched. Otherwise Flutter is unable to detect if the list has changed.

Also, the code would clearer if you called setState whenever you change the list.

_fetchData() async {

  final url = "API_URL";
  print("Fetching: " + url);
  final response = await http.get(url);
  final busesJson = json.decode(response.body);
  if (busesJson["service_id"] == null) {
    globals.serviceActive = false;
    setState(() {
      _isLoading = false;
    });
  } else {
    final newArrivalsList = new List<ArrivalEstimates>();
    busesJson["ResultSet"]["Result"].forEach((busJson) {
      if (busJson["arrival_estimates"] != null) {
        busJson["arrival_estimates"].forEach((arrivalJson) {
          globals.serviceActive = true;
          final arrivalEstimate = new ArrivalEstimates(
              arrivalJson["route_id"], arrivalJson["arrival_at"], arrivalJson["stop_id"]
          );
          newArrivalsList.add(arrivalEstimate);
        });
      }
    });
    setState(() {
      arrivalsList = newArrivalsList;
      _isLoading = false;
    });
  }
}

A few side notes:

I'm not sure if you actually want to clear the list before you fetch the data. If the state was updated properly, that would cause a flicker every 5 seconds.

I'm not sure if you simplified the code, but calling the _fetchData method every five seconds may become a problem if the network is slow.

like image 44
boformer Avatar answered Oct 04 '22 06:10

boformer