How to sort data from api? - dart

There are two dropdown button with the list of countries and types of sport. If on them somsething is chosen it is need to show listTile with the leagues on it is chosen to the country and/or sport and if on them nothing is chosen - show all leagues.
But I get:
Dart Error: Unhandled exception:
setState () called after dispose (): _SportLigPageState # b5830 (lifecycle state: defunct, not mounted)
This is what happens if you see the widget tree (e.g.). This error can occur when a call is made. Dispose () callback. It is necessary to ensure that the object is still in the tree.
This can be a memory card if it’s not. To avoid memory leaks, consider dispose ().
Api with leagues: https://www.thesportsdb.com/api/v1/json/1/all_leagues.php:
class LigList extends StatefulWidget {
#override
_LigListState createState() => _LigListState();
}
class _LigListState extends State<LigList> {
String sport;
String country;
List data;
Future<String> getJsonData() async {
http.Response response;
if (sport != null) {
if (country != null) response = await http
.get(Uri.encodeFull('https://www.thesportsdb.com/api/v1/json/1/all_leagues.php?c=$sport&s=$country'), headers: {"Accept": "application/json"});
else response = await http
.get(Uri.encodeFull('https://www.thesportsdb.com/api/v1/json/1/all_leagues.php?c=$sport'), headers: {"Accept": "application/json"});}
else if (country == null){ response = await http
.get(Uri.encodeFull('https://www.thesportsdb.com/api/v1/json/1/all_leagues.php'), headers: {"Accept": "application/json"});}
else response = await http
.get(Uri.encodeFull('https://www.thesportsdb.com/api/v1/json/1/all_leagues.php?c=$country'), headers: {"Accept": "application/json"});
var convertDatatoJson = json.decode(response.body);
data = convertDatatoJson['leagues'];
return "Success";
}
static const menuItems = countriesList;
final List<DropdownMenuItem<String>> _dropDownItems = menuItems
.map((String CountruValue) =>
DropdownMenuItem<String>(
value: CountruValue,
child: Text(CountruValue),
),
).toList();
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Column(children: <Widget>[
FutureBuilder(
builder: (BuildContext context, AsyncSnapshot snapshot) {
return DropdownButton(
value: country,
hint: Text("Choose a countre league of which you want to find"),
items: _dropDownItems,
onChanged: (value) {
country = value;
print(country);
setState(() {});
},
);}),
SizedBox(width: 5),
FutureBuilder(
future: _getSports(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return snapshot.hasData
? DropdownButton(
value: sport,
hint: Text("Choose a sport league of which you want to find"),
items: snapshot.data,
onChanged: (value) {
sport = value;
print(sport);
setState(() {});
},
)
: Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: CircularProgressIndicator());
}),
Flexible(
child:FutureBuilder(
future: getJsonData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return ListView.separated(
itemCount: data == null ? 0 : data.length,
itemBuilder: (BuildContext context, int i) {
return Container(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment
.stretch,
children: <Widget>[
ListTile(
title: Text(data[i]['strLeague']),
subtitle: Text(
data[i]['strSport']),
onTap: () {
Navigator.push(
context,
new MaterialPageRoute(
builder: (
BuildContext context) =>
new ComandListScreen()
// (data[i])
));
},
),
]
)
)
);
});
}))
]),
),
);
}
}
Any assistance is very much appreciated.

There's a lot of things wrong with your code. The first child in your code is wrapped in a FutureBuilder but you're not using any Future functionality.
FutureBuilder(
builder: (BuildContext context, AsyncSnapshot snapshot) {
return DropdownButton(
value: country,
hint: Text("Choose a countre league of which you want to find"),
items: _dropDownItems,
onChanged: (value) {
country = value;
print(country);
setState(() {}); // Remove this line
},
);}),
In addition to that you also are calling setState() randomly in your onChanged callback with nothing inside of it. I'd suggest you take that widget out of the FutureBuilder and just use the DropdownButton on it's own.
Then also in this line
itemCount: data == null ? 0 : data.length,
You're using data, which is set in the future that you call there. You might want to read up on how to properly use the FutureBuilder widget. Just return the data object from your _getJsonData() Future because it's always returning "Success" anyway. Return the list you want from the Future and then access it using snapshot.data
And lastly there's literally only one setState call in there so remove it and you'll be fine. My assumption is that there's some additional dispose you're calling or navigating away and the app crashes. Will need a lot more info to figure out, but you'll have to fix the way you use Futures and the Future builder so we can ensure it's not because of latent threads coming back and setting the state once you've left the view you were on.

Related

How do i receive a future value as String

I am trying to receive the return value of the future as a string. How do i go about it.
//Get a stock info
Future<String> getStock(int productID) async{
var dbClient = await db;
var result = await dbClient.rawQuery('SELECT * FROM $tableStock WHERE $columnProductID = $productID');
if(result.length == 0) return null;
return Stock.fromMap(result.first).currentStock;
}
Widget _buildProductInfo(Product data){
return Container(
child: ListView(
padding: EdgeInsets.all(8.0),
children: <Widget>[
_infoRow('Product ID', data.name),
_infoRow('Product Name', data.productID),
_infoRow('Cost Price', data.costPrice),
_infoRow('Selling Price', data.salePrice),
_infoRow('CategoryID', data.categoryID),
_infoRow('Currrent Stock', db.getStock(int.parse(data.productID)))
],
),
);
}
I expect this code to show a "value" rather it says "Instance of Future". But i can print the returned value when i try
final res = await db.getStock(int.parse(data.productID);
print(res);
You have to await for the future in order to unwrap the value. You can use a future builder to do this.
Instead of having this:
_infoRow('Currrent Stock', db.getStock(int.parse(data.productID))),
Have this:
FutureBuilder(
future: db.getStock(int.parse(data.productID),
builder: (context, snapshot) => _infoRow('Currrent Stock', snapshot.data),
),
Your complete code will look like this:
child: StreamBuilder<Product>(
initialData: barcode,
stream: bloc.scannedCode,
builder: (BuildContext context, AsyncSnapshot snapshot){
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Select lot');
case ConnectionState.waiting:
return _buildProductInfo(snapshot.data);
case ConnectionState.active:
case ConnectionState.done:
return _buildProductInfo(snapshot.data);
}
},
)
Widget _buildProductInfo(Product data){
return Container(
child: ListView(
padding: EdgeInsets.all(8.0),
children: <Widget>[
_infoRow('Product ID', data.name),
_infoRow('Product Name', data.productID),
_infoRow('Cost Price', data.costPrice),
_infoRow('Selling Price', data.salePrice),
_infoRow('CategoryID', data.categoryID),
FutureBuilder(
future: db.getStock(int.parse(data.productID),
builder: (context, snapshot) => _infoRow('Currrent Stock', snapshot.data),
)
],
),
);
}
You have to use async on your _buildProductInfo() method and use await before db.getStock(int.parse(data.productID)) . This way, the execution is suspended until the Future completes.

Flutter how to update text on dropdownbutton when using Sqflite to populate the list

I have no problem populating the list from Sqflite database on DropdownButton. My only problem is updating the text once it's selected. It kept showing 'Airport' and I'm still learning to work with Object instead of String. I just couldn't figure that out.
Here's the code:
String selectedAirport;
AirportModel _currentAirport;
...
children: <Widget>[
FutureBuilder<List<AirportModel>>(
future: db.getAllAirports(),
builder: (BuildContext context, AsyncSnapshot<List<AirportModel>> snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return DropdownButton<AirportModel>(
items: snapshot.data
.map((airportItem) =>
DropdownMenuItem<AirportModel>(
value: airportItem,
child: Text(airportItem.airportName),
))
.toList(),
onChanged: (AirportModel value) {
setState(() {
_currentAirport = value;
selectedAirport = _currentAirport.airportName;
});
},
hint: Text("Airport"),
);
}),
DropdownButton has a property value. use it like value=_currentAirport
return DropdownButton<AirportModel>(
value:_currentAirport,
items: snapshot.data
.map((airportItem) =>
DropdownMenuItem<AirportModel>(
value: airportItem,
child: Text(airportItem.airportName),
))
.toList(),
onChanged: (AirportModel value) {
setState(() {
_currentAirport = value;
selectedAirport = _currentAirport.airportName;
});
},
hint: Text("Airport"),
);
Maybe items didn't reach yet or empty when value is set to DropdownButton. is _currentAirport initialized to some other value already?
Can you try like this? Also check if the items list are empty
items: snapshot.data == null ? null : _currentAirport
You can declare a Future and init in initState and in FutureBuilder use this future.
AirportModel _currentAirport;;
Future _future;
#override
void initState() {
_future = db.getAllAirports();
super.initState();
}
body: FutureBuilder<List<AirportModel>>(
future: _future,
You can use stream builder. Please check the example below.
class DropDownMenu extends StatefulWidget {
#override
_DropDownMenuState createState() => _DropDownMenuState();
}
class _DropDownMenuState extends State<DropDownMenu> {
var _currentSelectedValue;
final _dbHelper = DatabaseHelper.instance;
LoginPageManager _loginPageManager = new LoginPageManager();
final ValueNotifier<List<DropdownMenuItem<String>>> _dropDownMenuItems =
ValueNotifier<List<DropdownMenuItem<String>>>([]);
#override
void initState() {
_updateList();
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
width: 300,
height: 50,
margin: const EdgeInsets.only(top: 00.0),
child: ValueListenableBuilder(
builder: (BuildContext context, List<DropdownMenuItem<String>> list,
Widget child) {
return Container(
child: DropdownButton<String>(
hint: Text("Please Select a Server"),
value: _currentSelectedValue,
onChanged: (value) {
setState(() {
_currentSelectedValue = value;
});
},
items: list),
);
},
valueListenable: _dropDownMenuItems,
),
);
}
_updateList() async {
print("Update server has been called");
_dropDownMenuItems.value.clear();
List<Map<String, dynamic>> x = await _dbHelper.queryAllRows();
_dropDownMenuItems.value.add(_getAddServerButton());
x.forEach((element) {
_dropDownMenuItems.value.add(_getDropDownWidget(element));
});
}
DropdownMenuItem<String> _getDropDownWidget(Map<String, dynamic> map) {
int id = map['yxz'];
String text =
map['xyz'];
String value = map['zyx'];
return DropdownMenuItem<String>(
value: value,
child: Container(
width: 270,
child: Row(
children: [_getText(text), _getRemoveButton(id), _getEditButton(id)],
),
));
}
}
To make sure api data is not null:
child: _identity1 != null
? DropdownButtonFormField<dynamic>(
validator: (value) => value == null ? 'field required' : null

Bloc Pattern to hide and unhide Widgets

I am trying to fetch some data from the internet and show it int a list.
Following is my bloc code
class StudentsBloc {
final _repository = Repository();
final _students = BehaviorSubject<StudentModel>();
final BehaviorSubject<bool> _showProgress = BehaviorSubject<bool>();
final BehaviorSubject<bool> _showNoInternetViews = BehaviorSubject<bool>();
Observable<StudentModel> get students => _students.stream;
Observable<bool> get showProgress => _showProgress.stream;
Observable<bool> get showNoInternetViews => _showNoInternetViews.stream;
//FetchStudent from my Api
fetchStudents(String disciplineId, String schoolId, String year_id,
String lastIndex) async {
final student = await _repository.fetchStudents(
disciplineId, schoolId, year_id, lastIndex);
_students.sink.add(student);
}
//Check to see if user has internet or not
isNetworkAvailable(String disciplineId, String schoolId, String year_id,
String lastIndex) async {
checkInternetConnection().then((isAvailable) {
if (isAvailable) {
fetchStudents(disciplineId, schoolId, year_id, lastIndex);
} else {
_students.sink.addError(NO_NETWORK_AVAILABLE);
}
});
}
Function(bool) get changeVisibilityOfProgress => _showProgress.sink.add;
Function(bool) get changeVisibilityOfNoInternetViews =>
_showNoInternetViews.sink.add;
dispose() {
_students.close();
_showProgress.close();
_showNoInternetViews.close();
}
}
Following is my main code to hide unide Widgets
Widget buildList(StudentsBloc bloc) {
return StreamBuilder(
stream: bloc.students,
builder: (context, AsyncSnapshot<StudentModel> snapshot) {
if (snapshot.hasError) {
bloc.changeVisibilityOfProgress(false);
bloc.changeVisibilityOfNoInternetViews(true);
return StreamBuilder(
stream: bloc.showNoInternetViews,
builder: (context, snapshot) {
bool showNoInternetView = snapshot.hasData ?? false;
return Visibility(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("No Network Available"),
RaisedButton(
onPressed: () {
fetchStudents();
},
child: Text("Retry"),
)
],
),
),
visible: showNoInternetView ? true : false,
);
},
);
}
if (snapshot.hasData) {
bloc.changeVisibilityOfProgress(false);
bloc.changeVisibilityOfNoInternetViews(false);
return Refresh(
year_id: "2",
schoolId: "1",
lastIndex: "0",
disciplineId: "1",
child: ListView.builder(
itemBuilder: (context, int index) {
return buildTile(
snapshot.data.toBuilder().data.studentData[index]);
},
itemCount: snapshot.data.toBuilder().data.studentData.length,
),
);
}
if (!snapshot.hasData) {
return StreamBuilder(
builder: (context, snapshot) {
bool showProgressIndicator = snapshot.data ?? false;
return Visibility(
child: Center(
child: CircularProgressIndicator(),
),
visible: showProgressIndicator ? true : false,
);
},
stream: bloc.showProgress,
);
}
},
);
}
The buildList method is called in the body of Scaffold
void fetchStudents() {
bloc?.changeVisibilityOfNoInternetViews(false);
bloc?.changeVisibilityOfProgress(true);
bloc?.isNetworkAvailable("1", "1", "2", "0");
}
Suppose the user has internet when app is opened then i am able to see circularprogressindicator and then the list of data is visible
but suppose at the start when app is opened and the user does not have internet then i am showing the No Network Available Text and a button to retry,
now if the user has connected to the internet and then click on button to retry i am directly seeing the list of data after few seconds instead of the circularprogressindicator.I am not able to understand why the NoInternetviews are not hiding and progressindicator is showing when retry button is clicked before showing list of data.
My stream is not getting updated on retry button called. Are there any caveats for StreamBuilder within StreamBuilder?
I tried changing the StreamBuilder order as mentioned by #ivenxu in the answer but it still does not work.
Following are the links of attached code
https://drive.google.com/file/d/15Z8jXw1OpwTB1CxDS8sHz8jKyHhLwJp7/view?usp=sharing
https://drive.google.com/open?id=1gIXV20S1o5jYRnno_NADabuIj4w163fF
in view layer you can use Visibility() widget and pass visible parameter true or false when load data from Internet.
let's think about how to change the visible variable from bloc.
The parent of Visibility() widget the StreamBuilder() to stream on changes data.
for your case you need a PublishSubject inside your bloc to stream and add new data, and Observable to stream on this data on your widget.
let's show a snippet code to help you how you can implement it
The bloc contains PublishSubject and Observable to stream and add data
//this Subject allows sending data, error and done events to the listener
final PublishSubject<bool> _progressStateSubject = new PublishSubject();
//the listener are streaming on changes
Observable<bool> get progressStateStream => _progressStateSubject.stream;
//to change your progress state
void changeProgressState({bool state}) => _progressStateSubject.sink.add(state);
Here you can change your state of your progress
void callWebService() async {
//when you call your func to fetch your data change your state to true
changeProgressState(state: true);
.
.
.
if(response != null){
//when your call is done change the state to false
changeProgressState(state: false);
}
}
Your progress widget is
// Loading Widget
Widget _buildLoadingWidget(Bloc bloc) {
return StreamBuilder<bool>(
stream: bloc.progressStateStream,//streaming on changes
builder: (context, snapshot) {
return Visibility(
visible: snapshot.data ?? false,//calling when data changes
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Loading data from API...",
textDirection: TextDirection.ltr),
CircularProgressIndicator()
],
),
),
);
});
}
It seems the cause is that CircularProgressIndicator is inside the students stream's update cycle. And the student steam only get next snapshot when the data returned from internet call in the case of retry.
Have a try on changing the order of stream builders, try putting the student streambuilder inside of the progress stream builder.
Widget buildList(StudentsBloc bloc) {
return StreamBuilder(
stream: bloc.showProgress,
builder: (context, snapshot) {
bool showProgressIndicator = snapshot.data ?? false;
if (!showProgressIndicator) {
StreamBuilder(
stream: bloc.students,
builder: (context, AsyncSnapshot<StudentModel> snapshot) {
....
//your original code without progress StreamBuilder
}
}
return Visibility(
child: Center(
child: CircularProgressIndicator(),
),
visible: showProgressIndicator ? true : false,
);
},
);
}

Change Variable values in Flutter

I'm trying to change some variables in different methos in Flutter, but the value isn't changed.
An example is something like:
enum UserPlaceStatusType { NONE, GOING, THERE, OUT, CANCELLED }
class PlaceCardState extends State<PlaceCard> {
UserPlaceStatusType _isOtherPlaceActive = UserPlaceStatusType.NONE;
Widget build(BuildContext context) {
return Card(
child: Scaffold(
body: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: this._getBody(),
),
bottomNavigationBar: this._getBottomNavigationBar()));
}
List<Widget> _getBody() {
return [
Expanded(child: Text('test'), flex: 3),
Expanded(child: Text('test'), flex: 6),
Expanded(child: this._getActionsMenu(), flex: 1)
];
}
Widget _getActionsMenu() {
return Container(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 0.0),
child: IconButton(
icon: Icon(Icons.arrow_forward_ios),
color: Colors.grey[400],
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new ListTile(
leading: new Icon(Icons.train),
title: new Text(Utility.format(
Language.of(context).takePlace, [_place.title])),
onTap: () {
showUserStatusDialog<DialogActions>(
context: context,
//It opens a simple dialog
child: this._getCurrentUserPlaceStatus());
},
),
],
);
});
},
));
}
Widget _getCurrentUserPlaceStatus() {
return new GraphqlProvider(
client: new ValueNotifier(
Client(endPoint: 'GraphQLUrl', cache: new InMemoryCache()),
),
child: new Query(
'The GraphQL Query',
variables: {},
builder: ({
bool loading,
var data,
var error,
}) {
if (data != null && data['getCurrentUserPlaceStatus'] != null) {
this._isOtherPlaceActive = UserPlaceStatusType.THERE;
Navigator.pop(context, DialogActions.cancel);
return Container();
} else {
this._isOtherPlaceActive = UserPlaceStatusType.GOING;
Navigator.pop(context, DialogActions.cancel);
return Container();
}
},
));
}
void showUserStatusDialog<T>({BuildContext context, Widget child}) async {
//here there is a validation but the variable value is the initial one, I mean NONE
if (this._isOtherPlaceActive == UserPlaceStatusType.GOING) {
//Cod to do
return;
}
showDialog<T>(
context: context,
builder: (BuildContext context) => child,
).then<void>((T value) {
if (value != null) {
this._isOtherPlaceActive = UserPlaceStatusType.NONE;
Navigator.pop(context);
}
});
}
}
I changed the variable value through some methods, but when I need to apply the validation, that's the initial value, it isn't changed, and I could not apply SetState method cuz it breaks the modal and throws an exception.
I will appreciate any feedback.
The method setState() can't be called inside a widget directly. I'm curious with your use of GrapQLProvider since it returns an empty Container() widget just to check the status of the data.
While I'm unfamiliar with the use of GraphQL, if the client that you're using inherits either a Stream or Future, it can be used to listen when the query is done.
Here's some snippets as demo. Let _testFuture() as the sample for a Future callback.
Future _testFuture() async{
return null;
}
Future can be listened to inside a Widget. When the request finishes, we have the opportunity to call setState().
_testFuture().then((value) {
// Check for values here
setState(() {
// Update values
});
});
Or if the request is set in a Stream, it's also possible to listen for Stream changes inside a Widget.
_streamController.add(_testFuture());
_streamController.stream.listen((event) {
// Check for values here
setState(() {
// Update values
});
});
This may not be the exact answer that you're looking for, but I hope this can guide you for a solution to your approach. I also found a GraphQL sample that uses ObservableQuery as a Stream that you can try.
Your code is very complex and should be refactored. Please notice how dialogs must be called.
enum DialogResult {ok, cancel}
caller_widget.dart
FlatButton(
child: Text('Open dialog'),
onPressed: () async {
// Call dialog and wait for result (async call)
final dialogResult = await showDialog<DialogResult>(
context: context,
builder: (context) => DialogWidget(),
);
if (dialogResult == DialogResult.ok) {
// do something
}
},
),
dialog_widget.dart
...
FlatButton(
child: Text('Ok'),
onPressed: () => Navigator.pop(context, DialogResult.ok), // DialogResult.ok returns
),
FlatButton(
child: Text('Cancel'),
OnPressed: () => Navigator.pop(context, DialogResult.cancel), // DialogResult.cancel returns
),
So you can return required value from dialog and set it to required variable.
P.S. Try to avoid use of old fashion then process of futures and use async/await.

Accessing variables out of scope

I am trying to navigate to a new page newData[index]["title"] when tap on listTile, how can I access newData, index and data variables out of this scope, I know I have to declare them in global scope but I tried this.index, but it doesn't work, I created a _onTap() method but I don't have access to index so I can only access the by asking for a specific position [0] for instance.
class _ContentPageState extends State<ContentPage> {
#override
Widget build(BuildContext context,) {
List data;
return new Scaffold(
appBar: new AppBar(
title: new Text("Local json file"),
),
body: new Container(
child: new Center(
child: new FutureBuilder(
future: DefaultAssetBundle
.of(context)
.loadString('data_files/file.json'),
builder: (context, snapshot) {
var newData = JSON.decode(snapshot.data.toString());
return new ListView.builder(
itemBuilder: (BuildContext context, int index) {
return new Card(
child: new ListTile(
title: new Text(newData[index]['title'],textScaleFactor: 1.5),
),
);
},
itemCount: newData == null ? 0 : newData.length,
);
}),
),
));
}
}
I guess you're trying to have a private _onTap method inside your class similar to :
_onTap(newData, int index) {
}
That's pretty simple, inside your ListTile you can wrap your _onTap inside another function like this :
return new Card(
child: new ListTile(
onTap: () => _onTap(newData, index),
title: new Text(newData[index]['title'], textScaleFactor: 1.5),
),
);
the previous answer is passing newData as argument. Another alternative is declare newData outside the build method.
class _ContentPageState extends State<ContentPage> {
var newData;// << here >> after class
#override
Widget build(BuildContext context,) {
So, in the assign remove de type (var)
newData = JSON.decode(snapshot.data.toString());

Resources