Saving and loading, shared preferences - dart

List<double> timersValuesList = [30.0, 60.0, 90.0, 120.0, 150.0, 180.0];
void saveSharedPreferences() async {
// from List of double to a List of String
List<String> convertedTimerValues = timersValuesList.map((i) => i.toString()).toList();
// getting the instance of sharedPreferences as Object prefs
SharedPreferences prefs = await SharedPreferences.getInstance();
// taking my List as key of "mylist", if it doesn't exist create empty List, this is List of String
List<String> myList = (prefs.getStringList('mylist') ?? List<String>());
//saving the List of String as key"mylist"
await prefs.setStringList('mylist', convertedTimerValues);
}
void loadSharedPreferences() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> myList = (prefs.getStringList('mylist') ?? List<String>());
List<double> myOriginalList = myList.map((i)=> double.parse(i)).toList();
timersValuesList = myOriginalList;
// print('Your list $myOriginalList');
}
I tried to use SharedPreferences in this file for saving and loading a List of 6 double values, which are used in other widget and could be modified. Now the problem is that everytime I open my app it doesn't make me see updated and modified values, but always the first one I declared for default. For example. I edit my first value and it becomes like this.
timersValuesList[0] = 35
then I save it and exit the app. When I relaunch the app it shows me values of 30 not 35 like it should be. But if I edit for example with a +1, it jumps directly to 36 so it makes me thing that value was saved but was not visualized correctly at first launch. After that adding values work correctly. Can someone help me? I cannot find a way to do this. Maybe I should put in the saving function the default values in case doesn't exist any file saved already? thanks help with code of saving and loading a list of double too, because I don't think I'm doing it well. Thanks again.
I made this. ( I could call in the initState any async function so I called an external one). But i get error: "NoSuchMethodError: The method "[]" was called on null.
Receiver: null
Tried calling: "
class _SettingsPageState extends State<SettingsPage> {
List<double> timersList;
#override initState() {
super.initState();
callLoad();
}
Future callLoad() async {
timersList = await loadTimers();
}
void saveTimers(List<double> timersList) async {
List<String> convertedTimerValues = timersList.map((i) => i.toString()).toList();
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setStringList('TimerList', convertedTimerValues);
}
Future<List<double>> loadTimers() async {
List<double> defaultTimersList = [30.0, 60.0, 90.0, 120.0, 150.0, 180.0];
List<double> savedTimerList;
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> myList = prefs.getStringList('TimerList');
if (myList == null ){
return defaultTimersList;
} else{
savedTimerList = myList.map((i)=> double.parse(i)).toList();
return savedTimerList;
}
}
I edit like your code suggest. But I get this error, but the screen been loads with the correct value.
I/flutter ( 9290): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 9290): The following RangeError was thrown building SettingsPage(dirty, dependencies: [MediaQuery], state:
I/flutter ( 9290): _SettingsPageState#c0ade):
I/flutter ( 9290): RangeError (index): Invalid value: Valid value range is empty: 0
I/flutter ( 9290): When the exception was thrown, this was the stack:
I/flutter ( 9290): #0 List.[] (dart:core-patch/growable_array.dart:145:60)
I/flutter ( 9290): #1 _SettingsPageState.build (package:my_fitness_tools/pages/settings_page.dart:175:45)
I/flutter ( 9290): #2 StatefulElement.build (package:flutter/src/widgets/framework.dart:3825:27)
I/flutter ( 9290): #3 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3739:15)
I/flutter ( 9290): #4 Element.rebuild (package:flutter/src/widgets/framework.dart:3565:5)
I/flutter ( 9290): #5 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3722:5)
I/flutter ( 9290): #6 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:3864:11)
I/flutter ( 9290): #7 ComponentElement.mount (package:flutter/src/widgets/framework.dart:3717:5)
I/flutter ( 9290): #8 Element.inflateWidget (package:flutter/src/widgets/framework.dart:2961:14)
I/flutter ( 9290): #9 Element.updateChild (package:flutter/src/widgets/framework.dart:2764:12)
I/flutter ( 9290): #10 SingleChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:4876:14)
I/flutter ( 9290): #11 Element.inflateWidget (package:flutter/src/widgets/framework.dart:2961:14)
I/flutter ( 9290): #12 Element.updateChild (package:flutter/src/widgets/framework.dart:2764:12)
This is my Scaffold.
Scaffold(
appBar: new AppBar(
backgroundColor: Options.selectedTheme.primaryColorDark,
centerTitle: true,
title: Text("Settings"),
),
drawer: DrawerApp(),
body:
new Stack(
children: <Widget>[
new Container(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage("assets/backgrounds/Sfondo.jpg"),
fit: BoxFit.fill)),
),
ListView( // vertical listview
children: <Widget>[
//Inizio oggetti in ordine verticale della pagina.
SizedBox(
height: 10.0,
),
titleSettings("Timers"),
Container(
height: MediaQuery.of(context).size.height,
child: ListView( // horizontal Listview
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.horizontal,
children: <Widget>[
timerColumn(timersList[0], 0), // line 175
timerColumn(timersList[1], 1),
timerColumn(timersList[2], 2),
timerColumn(timersList[3], 3),
timerColumn(timersList[4], 4),
timerColumn(timersList[5], 5),
],
),
),
],
)
Editing the List timersList = [0,0,0,0,0,0] fixed that.
But I have a question. If I want to use now those values in another file. I have made the same exact function for loading. But I get this error.
Same thing on declaring everything and function but why it wants to be static instead the file before didn't?

This is almost certainly to do with the order of execution. You need to look for the answer in the code that you haven't shown.
widget state created
[30.0, etc assigned to timersValueList
initState called, which presumably calls...
loadSharedPreferences called, but suspends awaiting getInstance, allowing...
framework calls build - shows current value (30)
getInstance completes, so loadSharedPreferences continues assigning [35.0, etc to timersValueList
nothing informs the widget that its state has changed until you do an update.
Add a call to setState at the end of loadSharedPreferences to inform the framework of the change.
void loadSharedPreferences() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> myList = prefs.getStringList('mylist') ?? ['30', '60', '90'];
List<double> myOriginalList = myList.map((i) => double.parse(i)).toList();
setState(() {
timersValuesList = myOriginalList;
});
}
Edit
I'd change your State to this:
class _SettingsPageState extends State<SettingsPage> {
List<double> timersList;
#override
initState() {
super.initState();
loadTimers();
}
saveTimers(List<double> timersList) async {
List<String> convertedTimerValues = timersList.map((i) => '$i').toList();
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setStringList('TimerList', convertedTimerValues);
}
loadTimers() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> myList = prefs.getStringList('TimerList');
setState(() {
timersList = (myList == null)
? [30.0, 60.0, 90.0, 120.0, 150.0, 180.0]
: myList.map((i) => double.parse(i)).toList();
});
}
#override
Widget build(BuildContext context) {
if (timersList == null) return Center(child: CircularProgressIndicator());
return Whatever(/* Fill me in with the normal page*/);
}
}
You need to assign to timersList inside a setState so that the framework knows that you've changed the state! This will cause it to rebuild the widget. Note, that in the build you have to cope with timersList being null. It being null indicates that you are still waiting for stuff to happen, so should render a placeholder.

Related

Flutter returning noSuchMethodError when checking if it is mounted

I have a flutter app and it is throwing
NoSuchMethodError: The method 'markNeedsBuild' was called on null. Receiver: null Tried calling: markNeedsBuild()
here is the code that throws that:
if (this.mounted) {
setState(() {
groupDocument = groups.documents[0];
group = groupDocument.data;
});
}
I'm checking the mounted property, since that is executed after a future ends
EDIT1:
My build method is like:
#override
Widget build(BuildContext context) {
queryData = MediaQuery.of(context);
return ListView(
shrinkWrap: true,
physics: PageScrollPhysics(),
children: children
);
}
setState() causes a build, and your build method is likely faulty and possibly not returning a Widget.

DropDownButton selected item isn't changing in UI

The objects I want to add in my DropDownButton:
class Car {
int id;
String make;
Car(this.id, this.make);
static List<Car> getCars() {
var cars = new List<Car>();
cars.add(Car(1, "Ford"));
cars.add(Car(2, "Toyota"));
cars.add(Car(3, "BMW"));
return cars;
}
}
Constructing the DropDown (StatefulWidget State class):
class _MyHomePageState extends State<MyHomePage> {
Car _selectedCar;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(child: getDropDown()));
}
Widget getDropDown() {
var cars = Car.getCars();
this._selectedCar = cars.first; // Default to first value in list.
var items = cars.map((car) {
return new DropdownMenuItem<Car>(
value: car,
child: new Text(car.make),
);
}).toList();
return DropdownButton<Car>(
value: this._selectedCar,
onChanged: (Car car) {
setState(() {
this._selectedCar = car;
});
},
items: items);
}
}
DropDownButton Shows up correctly with first item selected, but when I select another item the UI never updates to show the new item as selected.
You need to initalize list just once, because there wont be a match for DropDownList value if you init new list on every draw.
Working example found here: Gist
Try initializing the _selectedCar variable in your initState() method instead of the getDropdown() method.
According to the code you posted, the _selectedCar variable gets reinitialized every time you call setState() since build() method is called.
Also you mentioned you are getting the below error when trying the solution in the first answer:
I/flutter ( 5072): 'package:flutter/src/material/dropdown.dart':
Failed assertion: line 560 pos 15: 'items == null || I/flutter (
5072): items.isEmpty || value == null ||
items.where((DropdownMenuItem item) => item.value == I/flutter (
5072): value).length == 1': is not true.
This is most likely because more than one items in your dropdown is getting the same value.
A possible fix can be using the id parameter of the Car object as the dropdown value instead of the entire object, since the id will be unique for each object. More detail about this error can be found here.

Stream inside a Stream using Providers

So I have created a BLOC structure with a Stream as given below. The Fetcher would receive changes to a list of Chatroom ids. Then using the transformer, it would add the data in the stream to a Cache map and pipe it to the output.
Now the catch here is that each Chatroom IDs will be used to create a stream instance, so subscribe to any changes in the Chatroom data. So the Cache map basically has the Chatroom ID mapped to its corresponding Stream. ChatRoomProvider is binds the bloc with the app.
class ChatRoomBloc {
// this is similar to the Streambuilder and Itemsbuilder we have in the Stories bloc
final _chatroomsFetcher = PublishSubject<String>();
final _chatroomsOutput =
BehaviorSubject<Map<String, Observable<ChatroomModel>>>();
// Getter to Stream
Observable<Map<String, Observable<ChatroomModel>>> get chatroomStream =>
_chatroomsOutput.stream;
ChatRoomBloc() {
chatRoomPath.listen((chatrooms) => chatrooms.documents
.forEach((f) => _chatroomsFetcher.sink.add(f.documentID)));
_chatroomsFetcher.stream
.transform(_chatroomsTransformer())
.pipe(_chatroomsOutput);
}
ScanStreamTransformer<String, Map<String, Observable<ChatroomModel>>>
_chatroomsTransformer() {
return ScanStreamTransformer(
(Map<String, Observable<ChatroomModel>> cache, String id, index) {
// adding the iteam to cache map
cache[id] = chatRoomInfo(id);
print('cache ${cache.toString()}');
return cache;
}, <String, Observable<ChatroomModel>>{});
}
dispose() {
_chatroomsFetcher.close();
_chatroomsOutput.close();
}
}
Observable<ChatroomModel> chatRoomInfo(String _chatrooms) {
final _chatroomInfo = PublishSubject<ChatroomModel>();
Firestore.instance
.collection('chatRooms')
.document(_chatrooms)
.snapshots()
.listen((chatroomInfo) =>
_chatroomInfo.sink.add(ChatroomModel.fromJson(chatroomInfo.data)));
dispose() {
_chatroomInfo.close();
}
return _chatroomInfo.stream;
}
Then I create a Streambuilder with a List view to list the IDs and any data from their corresponding streams as given below.
class FeedList extends StatelessWidget {
#override
Widget build(BuildContext context) {
final chatroomBloc = ChatRoomProvider.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Chat Room'),
),
body: buildList(chatroomBloc),
);
}
Widget buildList(ChatRoomBloc chatroomBloc) {
return StreamBuilder(
// Stream only top ids to display
stream: chatroomBloc.chatroomStream,
builder: (context,
AsyncSnapshot<Map<String, Observable<ChatroomModel>>> snapshot) {
if (!snapshot.hasData) { // no data yet
return Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, int index) {
print('index $index and ${snapshot.data}');
return buildTile(snapshot.data[index]);
},
);
});
}
Widget buildTile(Observable<ChatroomModel> chatroomInfoStream) {
return StreamBuilder(
stream: chatroomInfoStream,
builder: (context, AsyncSnapshot<ChatroomModel> chatroomSnapshot) {
if (!chatroomSnapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
print('${chatroomSnapshot.data.name}');
print('${chatroomSnapshot.data.members.toString()}');
return Column(children: [
ListTile(
title: Text('${chatroomSnapshot.data.name}'),
trailing: Column(
children: <Widget>[
Icon(Icons.comment),
],
),
),
Divider(
height: 8.0,
),
]);
});
}
}
The output I am getting is given below. The Streambuilder is stuck at CircularProgressIndicator in the buildTile method. I think it means that the instances are getting created and added in the cache map, but they are lot listening to the right instances or there is something wrong in the way I wired up the streams. Can you please help ?
I/flutter (12856): cache {H8j0EHhu2QpicgFDGXYZ: Instance of 'PublishSubject<ChatroomModel>'}
I/flutter (12856): cache {H8j0EHhu2QpicgFDGXYZ: Instance of 'PublishSubject<ChatroomModel>', QAhKYk1cfoq8N8O6WY2N: Instance of 'PublishSubject<ChatroomModel>'}
I/flutter (12856): index 0 and {H8j0EHhu2QpicgFDGXYZ: Instance of 'PublishSubject<ChatroomModel>', QAhKYk1cfoq8N8O6WY2N: Instance of 'PublishSubject<ChatroomModel>'}
I/flutter (12856): index 1 and {H8j0EHhu2QpicgFDGXYZ: Instance of 'PublishSubject<ChatroomModel>', QAhKYk1cfoq8N8O6WY2N: Instance of 'PublishSubject<ChatroomModel>'}
As a quick fix, maybe try:
final _chatroomInfo = BehaviorSubject<ChatroomModel>();
On a second note:
The code in its current state is hard to read and understand, it's unmaintainable and inefficient. I'm not sure what you are actually trying to do.
It's a bad idea to nest StreamBuilders. It will delay the display of the chat list by at least 2 frames, because every StreamBuilder renders at least one empty frame (data = null).
Listening to a stream and feeding the result into a Subject will also add delays.
If possible, try to remove all subjects. Instead, use rx operators.
The BLoC should provide a single output stream that provides all the data that is required to render the chat list.

how to properly implement click events on columns, fields and rows with PaginatedDataTable

I'm new to flutter and dart, so this is my first app (yay!!!)
in general I'm trying to create a table with two static rows of data. since I'm a beginner that what I've decided to start and play with :)
I use the PaginatedDataTable component for that, and I create a class that extends DataTableSource for the data source of the table.
the default rows per page is set to 10, so even when I have two rows of data it shows 2 rows and 8 empty rows, is that that the default behaviour ? probably not and I'm missing something :)
so when I click on an empty row I get an exception that onTap isn't being implemented on that row.
to make my question clearer this is my code:
this is my Widget function that returns the PaginatedDataTable component
Widget searchPageTable() {
int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage;
final List<DataColumn> _columns = new List<DataColumn>();
_columns.add(new DataColumn(label: Text("col1"),onSort: onSort));
_columns.add(new DataColumn(label: Text("col2"),onSort: onSort));
_columns.add(new DataColumn(label: Text("col3"),onSort: onSort));
return new PaginatedDataTable(header: Text("header"),
columns: _columns,
rowsPerPage: _rowsPerPage,
source: new MyDataSource(),
);
}
so here in the columns I added the onSort() function (for now an empty function) but I know that I can catch when clicking on column titles and implement that properly. moving on..
my data source is implement with the following code;
class MyDataSource extends DataTableSource {
cellTapped() {
}
#override
DataRow getRow(int index) {
if (index == 0) {
final List<DataCell> row = new List<DataCell>();
row.add(new DataCell(Text("col1txt"),onTap: cellTapped));
row.add(new DataCell(Text("col2txt"),onTap: cellTapped));
row.add(new DataCell(Text("col3txt"),onTap: cellTapped));
return new DataRow(cells: row);
} else if (index == 1) {
final List<DataCell> row = new List<DataCell>();
row.add(new DataCell(Text("col1txt2"),onTap: cellTapped));
row.add(new DataCell(Text("col2txt2"),onTap: cellTapped));
row.add(new DataCell(Text("col3txt2"),onTap: cellTapped));
return new DataRow(cells: row);
} else {
return null;
}
}
#override
int get selectedRowCount {
return 0;
}
#override
bool get isRowCountApproximate {
return false;
}
#override
int get rowCount {
return 2;
}
}
so here for each row I create a DataRow and in it for each column a DataCell and I implement an onTap for each DataCell. but what if I wanna change the onTap for each row and not for specific columns, how an I do that ?
and whenever I click on an empty row, I get the following exception:
flutter: ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
flutter: The following NoSuchMethodError was thrown while handling a gesture:
flutter: The method 'call' was called on null.
flutter: Receiver: null
flutter: Tried calling: call(true)
flutter:
flutter: When the exception was thrown, this was the stack:
flutter: #0 Object.noSuchMethod (dart:core/runtime/libobject_patch.dart:50:5)
flutter: #1 DataTable.build.<anonymous closure> (package:flutter/src/material/data_table.dart:586:38)
flutter: #2 _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:507:14)
flutter: #3 _InkResponseState.build.<anonymous closure> (package:flutter/src/material/ink_well.dart:562:30)
flutter: #4 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:102:24)
flutter: #5 TapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:242:9)
flutter: #6 TapGestureRecognizer.handlePrimaryPointer (package:flutter/src/gestures/tap.dart:175:7)
flutter: #7 PrimaryPointerGestureRecognizer.handleEvent (package:flutter/src/gestures/recognizer.dart:315:9)
flutter: #8 PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:73:12)
flutter: #9 PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:101:11)
flutter: #10 _WidgetsFlutterBinding&BindingBase&GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:180:19)
flutter: #11 _WidgetsFlutterBinding&BindingBase&GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:158:22)
flutter: #12 _WidgetsFlutterBinding&BindingBase&GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:138:7)
flutter: #13 _WidgetsFlutterBinding&BindingBase&GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:101:7)
flutter: #14 _WidgetsFlutterBinding&BindingBase&GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:85:7)
flutter: #15 _invoke1 (dart:ui/hooks.dart:168:13)
flutter: #16 _dispatchPointerDataPacket (dart:ui/hooks.dart:122:5)
flutter:
flutter: Handler: onTap
flutter: Recognizer:
flutter: TapGestureRecognizer#a6733(debugOwner: GestureDetector, state: possible, won arena, finalPosition:
flutter: Offset(209.0, 375.5), sent tap down)
flutter: ════════════════════════════════════════════════════════════════════════════════════════════════════
in general here I just want to ignore click events on empty row. how to implement that ?
any information regarding this issue would be appreciated.
thanks :)
You first question: "the default rows per page is set to 10, so even when I have two rows of data it shows 2 rows and 8 empty rows, is that that the default behaviour ? probably not and I'm missing something :)"
Yes, this is the expected behaviour because it will create the number of rows specified in rows per page property.
Your 2nd question: "whenever I click on an empty row, I get the following exception:..."
This is because in your getRow function it returns null when index > 1 so the exception is also expected. Practically, you don't want to hard-code your data source and set rows per page proportionally based on how many rows your data source has. For instance, if you know your data source will start with 100 rows, then you can either set each page having 10 or 20 rows. You should ideally avoid having empty rows in the page by dynamically building the page based on your data source update. But this exception is not going to crash your app and to some extent can be ignored if you want to simplify things.
Your final question about handling onTap of each row.
I am assuming you want to execute some actions to the selected row(s), for instance, you can enable/disable some buttons based on whether there is any row being selected. Note that there is an actions property (of type List<Widget>) on the PaginatedDataTable and you can put some buttons or other widgets you want. The way I did this is via passing in a event handler (such as onRowSelected which is simply a function taking no argument and returning nothing) into MyDataSource constructor and call it on onSelectedChanged handler within getRow function where you return a DataRow. This is an example of what I did to give you a specific idea:
class OrderSource extends DataTableSource {
int _selectedCount = 0;
final List<Order> _orders;
final Function onRowSelected;
OrderSource(this._orders, this.onRowSelected);
#override
DataRow getRow(int index) {
assert(index >= 0);
if (index >= _orders.length) return null;
final Order order = _orders[index];
return DataRow.byIndex(
index: index,
selected: order.selected,
onSelectChanged: (bool value) {
if (order.selected != value) {
_selectedCount += value ? 1 : -1;
assert(_selectedCount >= 0);
order.selected = value;
notifyListeners();
onRowSelected();
//print('selected rows: $selectedRowCount');
}
},
cells: <DataCell>[
DataCell(Text('${order.orderID}')),
DataCell(Text('${order.side}')),
...),
]);
}
}
Hope this helps.

How to rebuild widget in Flutter when a change occurs

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

Resources