I'm trying to implement BLoC pattern in my flutter app,
basically this app calculate some result and display it in table.
i have created CalculationResultProvider and CalculationResultBloc
as follows
class CalculationResultProvider
{
List<EstimationResult> resultList = new List();
List<EstimationResult> calculateResult(){
return getInitialData(); }
List<EstimationResult> getInitialData(){
var cement = new EstimationResult();
cement.material = "Cement";
cement.unit = "Ton";
cement.qty = 10;
var sand = new EstimationResult();
sand.material = "Sand";
sand.unit = "Ton";
sand.qty = 12;
var gravel = new EstimationResult();
gravel.material = "Gravel";
gravel.unit = "Ton";
gravel.qty = 5;
var steel = new EstimationResult();
steel.material = "Steel";
steel.unit = "Ton";
steel.qty = 5;
List<EstimationResult> resultList = new List();
resultList.add(cement);
resultList.add(sand);
resultList.add(gravel);
resultList.add(steel);
return resultList; } }
and my BLoC provider class as follows
class CalculationResultBloc {
final resultController = StreamController(); // create a StreamController
final CalculationResultProvider provider =
CalculationResultProvider(); // create an instance of our CounterProvider
Stream get getReult =>
resultController.stream; // create a getter for our stream
void updateResult() {
provider
.calculateResult(); // call the method to increase our count in the provider
resultController.sink.add(provider.resultList); // add the count to our sink
}
void dispose() {
resultController.close(); // close our StreamController
}
}
then i need to show this data in table widget
class ResultTableWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => ResultTableWidgetState();
}
class ResultTableWidgetState extends State {
final bloc =
CalculationResultBloc(); // create an instance of the counter bloc
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: bloc.getReult,
initialData: CalculationResultProvider().getInitialData(),
builder: (context, snapshot) {
DataTable(
columns: [
DataColumn(label: Text('Patch')),
DataColumn(label: Text('Version')),
DataColumn(label: Text('Ready')),
],
rows:
'${snapshot.data}' // Loops through dataColumnText, each iteration assigning the value to element
.map(
((element) => DataRow(
cells: <DataCell>[
DataCell(Text(element[
"Name"])), //Extracting from Map element the value
DataCell(Text(element["Number"])),
DataCell(Text(element["State"])),
],
)),
)
.toList(),
);
});
}
#override
void dispose() {
bloc.dispose();
super.dispose();
}
}
To iterate returning table it should be List<EstimationResult>
but how to transform snapshot in to List<EstimationResult> ?
where is the best place to transform inside bloc class or in widget class ?
im new to dart and flutter , can any one answer my questions?
thanks.
Your widget class will have no clue of the data type given by the stream function in your StreamBuilder, there are many ways to convert data in BloC right before streaming it, but all of them will be useless because to the widget class it's only a snapshot, and the only fields you can access in compilation time are those applied to a generic snapshot like data field. So, The only way to access custom list fields is to provide your StreamBuilder with what type of data to be expected from its stream function :
StreamBuilder<List<EstimationResult>>(
stream: bloc.getReult,
initialData: CalculationResultProvider().getInitialData(),
builder: (context, snapshot) {
//...
}
);
This way you can treat your snapshot as List<EstimationResult>, and have access to inner fields and functions right before you receive the actual snapshot. In your case probably you should import EstimationResult class into your widget class.
Related
I am trying to figure out how can i watch a StateNotifierProvider and trigger some methods (defined in the class subclassing StateNotifier) on this provider after having done some async computations in another Provider watching the StateNotifierProvider.
Loking at the example below
i need to perform a reset from the RandomAdderNotifierobject provided by the randomAdderProvider if the doneProvider return true.
I try to reset from the doReset Provider. However the provider has nothing to provide.
The point is that both the doneProvider and the doreset provider are not rebuild on state changes of AdderProvider.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:equatable/equatable.dart';
void main() {
runApp(
const ProviderScope(child: MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(home: Home());
}
}
final randomProvider = Provider<Random>((ref) {
return Random(1234);
});
//immutable state
class RandomAdder extends Equatable {
final int sum;
const RandomAdder(this.sum);
#override
List<Object> get props => [sum];
}
//State notifier extension
class RandomAdderNotifier extends StateNotifier<RandomAdder> {
RandomAdderNotifier(this.ref) : super(const RandomAdder(0));
final Ref ref;
void randomIncrement() {
state = RandomAdder(state.sum + ref.read(randomProvider).nextInt(5));
}
void reset() {
state = RandomAdder(0);
}
}
/// Providers are declared globally and specify how to create a state
final randomAdderProvider =
StateNotifierProvider<RandomAdderNotifier, RandomAdder>(
(ref) {
return RandomAdderNotifier(ref);
},
);
Future<bool> delayedRandomDecision(ref) async {
int delay = ref.read(randomProvider).nextInt(5);
await Future.delayed(Duration(seconds: delay));
print("You waited $delay seconds for a decision.");
return delay > 4;
}
final doneProvider = FutureProvider<bool>(
(ref) async {
ref.watch(randomAdderProvider);
bool decision = await delayedRandomDecision(ref);
print("the decision is $decision");
return decision;
},
);
final doreset = Provider((ref) {
if (ref.watch(doneProvider).value!) {
ref.read(randomAdderProvider.notifier).reset();
}
});
class Home extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
// Consumer is a widget that allows you reading providers.
child: Consumer(builder: (context, ref, _) {
final count = ref.watch(randomAdderProvider);
return Text('$count');
}),
),
floatingActionButton: FloatingActionButton(
// The read method is a utility to read a provider without listening to it
onPressed: () =>
ref.read(randomAdderProvider.notifier).randomIncrement(),
child: const Icon(Icons.add),
),
);
}
}
I think ref.listen is more appropriate for usage within the doreset function than ref.watch.
Similarly to ref.watch, it is possible to use ref.listen to observe a provider.
The main difference between them is that, rather than rebuilding the widget/provider if the listened to provider changes, using ref.listen will instead call a custom function.
As per the Riverpod documentation
For ref.listen we need an additional argument - the callback function that we wish to execute on state changes - Source
The ref.listen method needs 2 positional arguments, the first one is the Provider and the second one is the callback function that we want to execute when the state changes.
The callback function when called will be passed 2 values, the value of the previous State and the value of the new State.
&
We will need to handle an AsyncValue - Source
As you can see, listening to a FutureProvider inside a widget returns an AsyncValue – which allows handling the error/loading states.
In Practice
doreset function
I chose to handle the AsyncValue by only handling the data case with state.whenData()
final doReset = Provider<void>(
(ref) {
final done = ref.listen<AsyncValue<bool>>(doneProvider, (previousState, state) {
state.whenData((value) {
if (value) {ref.read(randomAdderProvider.notifier).reset();}
});
});
},
);
Don't forget to watch either doReset/doneProvider in your Home Widget's build method. Without that neither will kick off (Don't have an explanation for this behaviour)
class Home extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(doReset);
...
Lastly, your random function will never meet the condition for true that you have setup as delay>4, as the max possible delay is 4. Try instead using delay>3 or delay=4.
Also perhaps disable the button to prevent clicks while awaiting updates
and in a case where you are using ChangeNotifier You can pass ref in you provider and use the ref same as we can use in ConsumerWidget.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class YourProvider extends ChangeNotifier {
Ref ref;
YourProvider(this.ref) : super();
callOtherProviderFromThisProvider() {
ref.read(otherProvider).someMethodINeedToTrigger();
}
}
final yourProvider = ChangeNotifierProvider<YourProvider>(
(ref) => YourProvider(ref));
I'm building three pages of alarms (stored in different lists) that I have displayed via a Scaffold and TabViewer. Each alarm is stored as a row with a toggle switch to enable it. The rows for each page are stored as a List. Despite making sure to use set state when changing values and even trying to assign unique keys nothing I do seems to preserve the state of the switches when I changed tabs.
This is my first time coding in Flutter/Dart or designing an app for mobile in general. As such I'm still learning about some basic features of this language.
I've tried adding keys to everything using Uniquekey() to generate up keys didn't work so I've removed them.
I've made sure all variable changes are inside set state functions.
I've tried to store the variable inside the immutable super class of AlarmToggle which is both ill-advised and doesn't work anyways.
I haven't tired using PageStorageKey as I'm not sure how they'd be implemented in my code but I feel this is likely the only solution.
class Alarms {
List<Widget> allAlarms = []; // Store all alarms for the object
buildAlarm(
GlobalKey<ScaffoldState>
pageKey,
[int hour,
int minute,
List<bool> alarmDaysOfWeek]) {
TimeOfDay alarmTime = TimeOfDay(hour: hour, minute: minute);
AlarmRow _newAlarm = new AlarmRow(UniqueKey(), alarmTime, alarmDaysOfWeek);
allAlarms.add(_newAlarm);
}
void removeAlarm(GlobalKey<ScaffoldState> pageKey) {allAlarms.removeLast();}}
class AlarmRow extends StatefulWidget {
final TimeOfDay _alarmTime;
final List<bool> _alarmDaysofWeek;
final UniqueKey key;
AlarmRow(this.key, this._alarmTime, this._alarmDaysofWeek);
AlarmRowState createState() => new AlarmRowState();
}
class AlarmRowState extends State<AlarmRow> {
bool _alarmIsActive;
AlarmRowState(){_alarmIsActive = _alarmIsActive ?? false;}
void toggleChanged(bool state) {this.setState(() {_alarmIsActive = state;});}
#override
Widget build(BuildContext context) {
return new Container(
child: Row(
children: <Widget>[
new AlarmIcon(_alarmIsActive),
new Column(
children: <Widget>[
new AlarmTime(widget._alarmTime),
new AlarmWeekly(widget._alarmDaysofWeek),
],
),
new AlarmToggle(
_alarmIsActive,
() => toggleChanged(!_alarmIsActive),
),
],
),
);
} // Build
} // Class
No matter what I seem to try the _alarmIsActive variable in AlarmRow() gets reset to null each time the tab is changed. I'm trying to preserve its state when changing pages.
The solution is as jdv stated to use AutomaticKeepAliveClientMixin, it's not hard to use but I figured I'd include the instruction that I found after searching here in case anyone searches and finds this and doesn't know automatically how to implement it like myself.
class AlarmRowState extends State<AlarmRow> with AutomaticKeepAliveClientMixin {
#override bool get wantKeepAlive => true;
It's implemented in the state class with any modifiable variables you want to preserve. The 'with' after the 'extends' adds a Mixin which is a sort of class inheritance. Finally, it requires you to set the 'wantKeepAlive' to true and it compiles and state is no longer lost while the widget is not being rendered.
Why a stateful widget loses state while it's not rendered is something I'm still searching for. But at least I have a solution.
I hope this will help you #Ender ! check for this code, you can create a Global shared preference and use it, as shown below:-
class AlarmRow extends StatefulWidget {
#override
State<StatefulWidget> createState() => new _AlarmRowState();
}
class _AlarmRowState extends State<AlarmRow>{
bool alarmIsActive;
#override
void initState() {
alarmIsActive = Global.shared.alarmIsActive;
super.initState();
}
#override
Widget build(BuildContext context) {
....
.....
....
....
body: new Container(
child: Switch(
value: alarmIsActive,
onChanged: (bool isEnabled) {
setState(() {
alarmIsActive = isEnabled;
Global.shared.alarmIsActive = isEnabled;
isEnabled =!isEnabled;
});
},
.....
......
),
),
);
}
}
class Global{
static final shared =Global();
bool alarmIsActive = false;
}
Switch enabled and will maintain its state
I am writing a code in flutter in which I am using an SQFlite database. I want to insert image widget from the assets, and I am getting the name of the image from database.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Single Line diagram"),backgroundColor: Colors.red.shade700,),
body: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Align(
//alignment: Alignment.center,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Row(
//crossAxisAlignment: CrossAxisAlignment.center,
children: imageList(),
),
),
)
),
);
}
The above code calls imageList() for the list of images to display.
List<Widget> imageList(){
List<Widget> singleLineImages = new List();
List unit;
for (int i = 0; i <= widget.unitsList.length-1; i++){
for (int j = 1; j <= int.parse(widget.unitsList[i].quantity); j++){
print("${widget.unitsList[i].bulletin}, ${widget.unitsList[i].mountType}, ${widget.unitsList[i].disconnect}");
getfileName(widget.unitsList[i].bulletin, widget.unitsList[i].mountType, widget.unitsList[i].disconnect);
//if(fileName != null) {
singleLineImages.add(
Image.asset("images/SD_Files_2100/$fileName.jpg", height: 400.0, width: 200.0,));
//}
}
}
return singleLineImages;
}
I am getting the filename from getFileName() method which is using the database.
getfileName(String bulletin, String mountType, String disconnect)async {
fileNameList = await db.getSDfileName(bulletin, disconnect, mountType);
fileName = fileNameList[0]['FileName'];
print("filename: $fileName");
}
Now, after calling the getFileName(), the program is not waiting for the fileName and proceeding further, which takes filename as null. The filename is obtained correctly after the Image.asset code. Is there any way, so that the program waits untill it gets the proper filename?
Start fetching the list in initState() and call setState when the list is fetched, to do this asynchronously. Below you can find a simplified example of this process. Also note the await statement before get Filename. This makes sure you return to that piece of code after is is done executing.
class ListPage extends StatefulWidget {
#override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
// This should actually be a List<MyClass> instead of widgets.
List<Widget> _list;
#override
void initState() {
super.initState();
_fetchList();
}
Future _fetchList() async {
List<Widget> singleLineImages = new List();
List unit;
for (int i = 0; i <= widget.unitsList.length-1; i++){
for (int j = 1; j <= int.parse(widget.unitsList[i].quantity); j++){
print("${widget.unitsList[i].bulletin}, ${widget.unitsList[i].mountType}, ${widget.unitsList[i].disconnect}");
String fileName = await getfileName(widget.unitsList[i].bulletin, widget.unitsList[i].mountType, widget.unitsList[i].disconnect);
singleLineImages.add(
Image.asset("images/SD_Files_2100/$fileName.jpg", height: 400.0, width: 200.0,));
}
}
// call setState here to set the actual list of items and rebuild the widget.
setState(() {
_list = singleLineImages;
});
}
#override
Widget build(BuildContext context) {
// Build the list, or for example a CircularProcessIndicator if it is null.
}
}
Sidenote: you are making a lot of calls to the database, which is probably inefficient. Try to get the required data in a single db call. But that is another topic.
Keep Future<Widget> _list; field in your class.
Add _list = _fetchList(); in the initState() function.
Also note that _fetchList should return Future<Widget> in this case.
Use FutureBuilder in your build function.
I have a problem/question regarding the bloc plattern with flutter.
Currently, i am starting my app like this
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: MyBloc(),
child: MaterialApp(
title: "MyApp",
home: MyHomePage(),
routes: {
'/homePage': (context) => MyHomePage(),
'/otherPage': (context) => OtherPage(),
'/otherPage2': (context) => OtherPage2(),
...
},
));
So that i can retrieve/access myBloc like
myBloc = BlocProvider.of(context) as MyBloc;
and the data represented by the state like
BlocBuilder<MyBlocEvent, MyObject>(
bloc: myBloc,
builder: (BuildContext context, MyObject myObject) {
....
var t = myObject.data;
....
myBloc.onFirstEvent();
...
};
wherever i need it.
MyBloc is implemented like this:
abstract clas MyBlocEvent {}
class FirstEvent extends MyBlocEvent {}
class SecondEvent extends MyBlocEvent {}
class MyBloc extends Bloc<MyBlocEvent , MyObject>
void onFirstEvent()
{
dispatch(FirstEvent());
}
void onSecondEvent()
{
dispatch(SecondEvent());
}
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
state.data = "test1";
}
else if (event is SecondEvent) {
state.otherData = 5;
}
yield state;
}
The problem i now have, is that as soon as i change on of the state values and call
Navigator.pop(context)
to go back in the current stack, i can't change anything is the state anymore because the underlying stream seems to be closed. It fails with the message:
Another exception was thrown: Bad state: Cannot add new events after calling close"
Now this only happens after i call pop. If i only push new screens i can happily change the state data without any problems.
Am i doing something wrong regarding the Navigation here or is there something else i didn't catch regarding flutter or the bloc pattern itself?
Bad state: Cannot add new events after calling close
This error means that you are calling add on a StreamController after having called close:
var controller = StreamController<int>();
controller.close();
controller.add(42); // Bad state: Cannot add new events after calling close
It is likely related to you calling close inside the dispose method the "wrong" widget.
A good rule of thumb is to never dispose/close an object outside of the widget that created it. This ensure that you cannot use an object already disposed of.
Hope this helps in your debugging.
The navigation of the app depends on your widget designs.
I use stateless widgets and render the view using bloc's data.
Whenever i navigate to another page, i would pop the current widget and navigate to the next widget.
The next stateless widget declare the bloc,
then in your subsequent stateless widgets should contain calls like MyBloc.dispatch(event(param1: value1, param2: value2));
In MyBloc, you need to set the factory of your state that contains final values;
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
// set it in the state, so this code is omitted
// state.data = "test1";
// add this
yield state.sampleState([], "test1");
}
else if (event is SecondEvent) {
// state.otherData = 5;
yield state.sampleState([], 5);
} else {
yield state.sampleState([], null);
}
The MyObjectState needs to be setup like this,
class MyObjectState {
final List<Bar> bars;
final String Foo;
const MyObjectState(
{this.bars,
this.foo,
});
factory MyObjectState.sampleState(List<Bar> barList, String value1) {
return MyObjectState(bars: barList, foo: message);
}
}
So that the stateless widget can use the bloc like this
MyBloc.currentState.sampleState.foo
You can try run Felix Angelov's flutter project.
Login Flow Example
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! :)
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:
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.
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.
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.
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(),
),
],
),
)
);
}
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.
If you are certain that you want a child widget to rebuild every time you call setState() and it is stubbornly refusing, you can give it a UniqueKey(). This will ensure that when setState() triggers a rebuild the child widget keys will not match, the old widget will be popped and disposed of, and, the new widget will replace it in the widget tree.
Note that this is using keys in sort of the opposite way for which they were intended (to reduce rebuilding) but if something beyond your control is hindering necessary rebuilds then this is a simple, built-in way to achieve the desired goal.
Here is a very helpful Medium article on keys from one the Flutter team members, Emily Fortuna:
https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d
I am not sure if this is what your looking for but and im probably late on this but i believe you can use a change notifier efficiently to achieve this. Basically a change notifier is hooked to your backed logic() for instance an api data fetch. A widget is then registered with a change notifier of the same type as the change notifier provider. In event of data change, the widgets registered with the change notifier will be rebuild.
For instance
// extend the change notifier class
class DataClass extends ChangeNotifier {
....
getData(){
Response res = get('https://data/endpoint')
notifyListeners()
}
void onChange() {
notifyListeners();
}
....
}
Every time there is change in data you call the notifyListeners() that will trigger rebuild of consuming widgets.
Register you widget with a changenotifier
class View extends StatefulWidget {
Widget create(BuildContext context) {
return ChangeNotifierProvider<ModelClass>(
builder: (context) => DataClass(auth: auth),
child: Consumer<ModelClass>(
builder: (context, model, _) => View(model: model),
),
);
}
}
You can also user a Consumer for the same. Get more on this from the Documentation