How to sort list from FireStore - dart

In my documents on FireStore, each one has a list of strings. When I'm displaying the document in app, I would like to sort them alphabetically. What I'm trying doesn't work.
var words = document['list'].cast<String>();
words.sort(); // Outputs 'null'
On inspection in the debugger, when I'm casting the list the object is of type CastList, but I can't find any info on this, and trying to create an object with that declared type tells me that it is an undefined class. So then I tried to specify the class that I'd like it to be:
List<String> words = document['list'].cast<String>();
But it still outputs null when I try to sort.
My collections look like this
I'm getting all of the documents inside of lists and displaying each of them in a listView.
StreamBuilder(
stream: Firestore.instance.collection('lists').orderBy('releases').snapshots,
builder: (context, snapshot) {
if (!snapshot.hasData)
return const Center(child: Text('Loading...'));
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) =>
_buildRow(context, snapshot.data.documents[index], index),
);
},
)
Widget _buildRow(BuildContext context, DocumentSnapshot document, int index) {
var words = document['list'].cast<String>();
var wordsString = words.toString();
wordsString = wordsString.substring(1, wordsString.length - 1);
return CheckboxListTile(
title: Text(
document['name'],
style: _largerTextStyle,
),
subtitle: Text(
wordsString,
style: _textStyle,
),
value: _selectedIndices.contains(index),
onChanged: (bool value) {
setState(() {
if (value) _selectedIndices.add(index);
else _selectedIndices.remove(index);
});
},
);
}

It should work , don't need to call cast.
Edit:
I think you forgot to extract the data.
List words = document.data['list'];
words.sort();

Related

Generic type check - keep type and not dynamic

I have these classes
class CustomPopupAction<T> extends CustomAction {
final Icon icon;
final List<CustomPopupActionItem<T>> actions;
final void Function(T) onActionSelected;
CustomPopupAction({
required this.icon,
required this.actions,
required this.onActionSelected,
});
}
class CustomPopupActionItem<T> {
final T value;
final Widget Function(T) itemBuilder;
CustomPopupActionItem({
required this.value,
required this.itemBuilder,
});
}
and I am trying to create overflow menu which will work like this:
if the button is visible, I will create PopupMenuButton
if the button is overflown, I will create ListTile which will open dialog
it can hold multiple different types like CustomAction, CustomPopupAction<Locale>, CustomPopupAction<String>...
I am building that row like this
if (a is CustomPopupAction) {
return PopupMenuButton(
icon: a.icon,
onSelected: (i) => a.onActionSelected(i),
itemBuilder: (context) {
return a.actions.map((i) => PopupMenuItem(
value: i.value,
child: i.itemBuilder(i.value),
)).toList();
},
);
} else {
return IconButton(...);
}
and finally my main code:
...
return OverflowMenu(
actions: [
CustomPopupAction<Locale>(
icon: Icon(Icons.translate),
actions: [
CustomPopupActionItem<Locale>(
value: Locale('en'),
itemBuilder: (l) => ListTile(title: Text(l.toString()),
),
],
onActionSelected: (l) => print(l),
],
);
But this doesn't work for me, I am getting an exception Expected a value of type '(dynamic) => Widget', but got one of type '(Locale) => ListTile'.
I know it's because if (a is CustomPopupAction) is actually getting CustomPopupAction<dynamic>.
can I somehow convince Dart that a nas not dynamic type and that it should work with it's real type?
if not, why am I getting that exception? Locale can be assigned to dynamic variable and ListTile is clearly a Widget.
can I do this without going through dynamics at all?

StreamBuilder TextField does not update its value when changed elsewhere

I have a reactive login form following the BLOC pattern. I'm trying to programmatically clear all the values in it. In my Bloc, my submit function passes empty strings to my stream sinks:
class Bloc with Validators {
final _email = BehaviorSubject<String>();
final _password = BehaviorSubject<String>();
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password => _password.stream.transform(validatePassword);
Stream<bool> get submitValid => Observable.combineLatest2(email, password, (String e, String p) {
var valid = (e != null && e.isNotEmpty)
&& (p != null && p.isNotEmpty);
print('$e && $p = $valid');
return valid;
});
Function(String) get changeEmail => _email.sink.add;
Function(String) get changePassword => _password.sink.add;
submit() {
final validEmail = _email.value;
final validPassword = _email.value;
print('final values: $validEmail && $validPassword');
changeEmail('');
changePassword('');
}
dispose() {
_email.close();
_password.close();
}
}
When I press the submit button that calls this submit() function, I get the error messages for both of the text fields, because the values of email and password have changed behind the scenes, but they are not visually updated in the TextFields. Here are my StreamBuilders for my TextFields and Submit button:
Widget emailField(Bloc bloc) {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) { // re-runs build function every time the stream emits a new value
return TextField(
onChanged: bloc.changeEmail,
autocorrect: false,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
icon: Icon(Icons.email),
hintText: 'email address (you#example.com)',
labelText: 'Email',
errorText: snapshot.error
)
);
}
);
}
Widget passwordField(Bloc bloc) {
return StreamBuilder(
stream: bloc.password,
builder: (context, AsyncSnapshot<String> snapshot) {
return TextField(
onChanged: bloc.changePassword,
autocorrect: false,
obscureText: true,
decoration: InputDecoration(
icon: Icon(Icons.security),
hintText: 'must be greater than 6 characters',
labelText: 'Password',
errorText: snapshot.error
)
);
}
);
}
Widget submitButton(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
child: Text('Logins'),
color: Colors.blue,
onPressed: !snapshot.hasData || snapshot.hasError || snapshot.data == false
? null
: bloc.submit
);
}
);
}'
And here is the code I'm using for my validators in my Bloc:
class Validators {
final validateEmail = StreamTransformer<String, String>.fromHandlers(
handleData: (email, sink) {
RegExp exp = new RegExp(r"^[a-zA-Z0-9.]+#[a-zA-Z0-9]+\.[a-zA-Z]+");
var valid = exp.hasMatch(email);
if (valid) {
sink.add(email);
} else {
sink.add('');
sink.addError('Invalid email address!');
}
}
);
final validatePassword = StreamTransformer<String, String>.fromHandlers(
handleData: (password, sink) {
var valid = password.length >= 6;
if (valid) {
sink.add(password);
} else {
sink.add('');
sink.addError('Password must be at least 6 characters long!');
}
}
);
}
In my validators, I emit an empty string whenever there is an error. This makes it so the submitValid getter works when the user invalidates something that used to be valid.
I know it's been a long time, but that's my way for solving it.
First, I've created a TextEditingController for my TextField. Then I've created two methods on my BLoC: updateTextOnChanged and updateTextElsewhere. On the fisrt one I just retrieved the value (because I need it to use later). On the second one I added a sink to update the controller on TextField.
Widget:
return StreamBuilder<String>(
stream: bloc.streamText,
builder: (context, snapshot) {
_controller.text = snapshot.data;
return Expanded(
child: TextField(
controller: _controller,
onChanged: (value) => {bloc.updateTextOnChanged(value)},
),
);
}
);
Bloc:
Stream<String> get streamText => _controllerTxt.stream;
String _myText;
void updateTextElsewhere(String value) {
_controllerTxt.sink.add(value);
}
void updateTextOnChanged(String value) {
_myText = value;
}
Then you just need to call updateTextElsewhere() whenever you need to update it outside onChanged.
In you're case just add an empty string like: updateTextElsewhere("");
In submit(), you seem like reseting username and password
changeEmail('');
changePassword('');
And as you commented , 're-runs build function every time the stream emits a new value'. It re-builds UI since the value updated to empty. Maybe does it cause the problem?

Flutter List + Pull to load more data is not waiting for data to load before it finishes causing the list / scrolling to become unstable

If you make a new Flutter project and include the dependencies and then replace your main.dart file you should be where I am on this question.
I left the original load: with Future.delayed but it doesn't seem to matter. I know partially what my problem is but am unable to come up with a better solution.
1) I don't seem to be using my snapshot.data and instead I am just making a empty List with str and then i just addAll into it and use that. So i'd love to not do that, i originally was using snapshot.data but ran into problems when I tried to "pull to load more data" which happens after you scroll to the bottom of the list.
The problem with my current method of doing this is that if you pull to load more users and then try to pull again before the users have loaded, The app breaks and doesn't wait for the data to properly load. I believe that I need to be doing that all in the load: of this library easy_refresh... but I am not sure how to rewrite my code to accomplish that.
How can I get my data to load with snapshot.data and then when I pull to refresh, I append 100 more users to that list but the UI waits for the list to update before it finishes the load. Would I be better off just putting a Blocking UI element and after the str list updates? and when new users are loaded I unblock the UI? which sorta feels hackish and not the correct way to solve this. The plugin itself should be able to do the loading and when its ready it stops the spinner under the list and says "finished".
pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_easyrefresh: ^1.2.7
http: ^0.12.0+2
main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:http/http.dart' as http;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
backgroundColor: Colors.white
),
home: DuelLeaderBoards(),
);
}
}
class DuelLeaderBoards extends StatefulWidget {
#override
_DuelLeaderBoardsState createState() => _DuelLeaderBoardsState();
}
class _DuelLeaderBoardsState extends State<DuelLeaderBoards> {
List<Entry> str = [];
GlobalKey<EasyRefreshState> _easyRefreshKey = new GlobalKey<EasyRefreshState>();
GlobalKey<RefreshHeaderState> _headerKey = new GlobalKey<RefreshHeaderState>();
GlobalKey<RefreshHeaderState> _connectorHeaderKey = new GlobalKey<RefreshHeaderState>();
GlobalKey<RefreshFooterState> _footerKey = new GlobalKey<RefreshFooterState>();
GlobalKey<RefreshFooterState> _connectorFooterKey = new GlobalKey<RefreshFooterState>();
Future<LeaderBoards> getLeaderBoards(start) async {
String apiURL = 'https://stats.quake.com/api/v2/Leaderboard?from=$start&board=duel&season=current';
final response = await http.get(apiURL);
if (response.statusCode == 200) {
final responseBody = leaderBoardsFromJson(response.body);
return responseBody;
} else {
throw Exception('Failed to load Data');
}
}
void updateLeaderBoardList(e) async {
setState(() {
str.addAll(e.entries);
});
}
#override
void initState() {
getLeaderBoards(0).then((onValue) => str = onValue.entries );
super.initState();
}
#override
Widget build(BuildContext context) {
Widget header = ClassicsHeader(
key: _headerKey,
refreshText: "pullToRefresh",
refreshReadyText: "releaseToRefresh",
refreshingText: "refreshing...",
refreshedText: "refreshed",
moreInfo: "updateAt",
bgColor: Colors.transparent,
textColor: Colors.white,
);
Widget footer = ClassicsFooter(
key: _footerKey,
loadHeight: 50.0,
loadText: "pushToLoad",
loadReadyText: "releaseToLoad",
loadingText: "loading",
loadedText: "loaded",
noMoreText: "Finished",
moreInfo: "updateAt",
bgColor: Colors.transparent,
textColor: Colors.white,
);
return FutureBuilder(
future: getLeaderBoards(0),
builder:
(BuildContext context, AsyncSnapshot<LeaderBoards> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return Builder(builder: (BuildContext context) {
return Center(
child: new EasyRefresh(
key: _easyRefreshKey,
behavior: ScrollOverBehavior(),
refreshHeader: ConnectorHeader(
key: _connectorHeaderKey,
header: header,
),
refreshFooter: ConnectorFooter(
key: _connectorFooterKey,
footer: footer,
),
child: CustomScrollView(
semanticChildCount: str.length,
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(<Widget>[header]),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return new Container(
height: 70.0,
child: Card(
child: new Text(
'${index+1}: ${str[index].userName}',
style: new TextStyle(fontSize: 18.0),
),
));
},
childCount: str.length,
)),
SliverList(
delegate: SliverChildListDelegate(<Widget>[footer]),
)
],
),
onRefresh: () async {
await new Future.delayed(const Duration(seconds: 0), () {
setState(() {});
});
},
loadMore: () async {
getLeaderBoards(str.length).then((onValue) => {
updateLeaderBoardList(onValue)
});
},
// loadMore: () async {
// await new Future.delayed(const Duration(seconds: 0), () {
// getLeaderBoards(str.length).then((onValue) => {
// updateLeaderBoardList(onValue)
// });
// });
// },
)
);
});
}
});
}
}
LeaderBoards leaderBoardsFromJson(String str) {
final jsonData = json.decode(str);
return LeaderBoards.fromJson(jsonData);
}
String leaderBoardsToJson(LeaderBoards data) {
final dyn = data.toJson();
return json.encode(dyn);
}
class LeaderBoards {
String boardType;
List<Entry> entries;
int totalEntries;
LeaderBoards({
this.boardType,
this.entries,
this.totalEntries,
});
factory LeaderBoards.fromJson(Map<String, dynamic> json) => new LeaderBoards(
boardType: json["boardType"] == null ? null : json["boardType"],
entries: json["entries"] == null ? null : new List<Entry>.from(json["entries"].map((x) => Entry.fromJson(x))),
totalEntries: json["totalEntries"] == null ? null : json["totalEntries"],
);
Map<String, dynamic> toJson() => {
"boardType": boardType == null ? null : boardType,
"entries": entries == null ? null : new List<dynamic>.from(entries.map((x) => x.toJson())),
"totalEntries": totalEntries == null ? null : totalEntries,
};
}
class Entry {
String userName;
int eloRating;
String profileIconId;
String namePlateId;
Entry({
this.userName,
this.eloRating,
this.profileIconId,
this.namePlateId,
});
factory Entry.fromJson(Map<String, dynamic> json) => new Entry(
userName: json["userName"] == null ? null : json["userName"],
eloRating: json["eloRating"] == null ? null : json["eloRating"],
profileIconId: json["profileIconId"] == null ? null : json["profileIconId"],
namePlateId: json["namePlateId"] == null ? null : json["namePlateId"],
);
Map<String, dynamic> toJson() => {
"userName": userName == null ? null : userName,
"eloRating": eloRating == null ? null : eloRating,
"profileIconId": profileIconId == null ? null : profileIconId,
"namePlateId": namePlateId == null ? null : namePlateId,
};
}
I looked at the documentation of loadMore. Since it says that the body of the function assigned to loadMore should be async, you do not need to use then:
loadMore: () async {
final result = await getLeaderBoards(str.length);
updateLeaderboardList(result);
},
loadMore: () async {
await getLeaderBoards(str.length).then((onValue) => {
updateLeaderboardList(onValue)
});
},
but putting "await" my loader waits for the function to complete before it finishes the animation.

Flutter reading from JSON if value matches

I have a json of customers:
customer.json
[
{
"name": "Customer 1",
"id": "1"
},
{
"name": "Customer 2",
"id": "2"
},
{
"name": "Customer 3",
"id": "3"
}
]
This is the dart file using this json data:
customerslist.dart
Future Method
Future<String> loadCustomers() async{
var res = await http.get(
Uri.encodeFull(url),
headers: {"Accept": "application/json"});
return res.body;
}
Widget
widgets.add(new FutureBuilder(
future: loadCustomers(),
builder: (context, snapshot){
if(snapshot.hasData){
//get snapshot data from JSON tree
var jsondecode = json.decode(snapshot.data);
return new ListView.builder(
shrinkWrap: true,
itemCount: jsondecode.length,
itemBuilder: (context, index){
String name = jsondecode[index]["name"];
String id = jsondecode[index]["id"];
if(name == "Customer 2"){
return new Column(
children: <Widget>[
new ListTile(
title: new Text("Name"),
trailing: new Text(name),
),
new ListTile(
title: new Text("ID"),
trailing: new Text(id),
)
],
);
}
});
}else{
return new Center(
child: new CircularProgressIndicator(),
);
}
}));
What I am trying to do is bringing out the values if the name matches as you can see from if(name == "Customer 2").
But the problem I am having is:
When i change the if statement to if(name == "Customer 1"), the output is as you would expect:
But if i were to change it to if(name == "Customer 2"), the output turns out blank as such:
Could someone explain why is it turning out like this or is there another method I could go about doing to fix this issue?
I think it's because Column widget is taking full height. You could try using the minimum height for your column and add else condition.
if(name == "Customer 2"){
return new Column(
children: <Widget>[
new ListTile(
title: new Text("Name"),
trailing: new Text(name),
),
new ListTile(
title: new Text("ID"),
trailing: new Text(id),
)
],
);
} else {
return new Container();
}
When you use 'Customer 1' it works fine because your first element is 'Customer 1' , probably you have an error on your console because for other items there are no returning widgets.
In the case of 'Customer 2' the first item is not returned, so check your console log.
itemBuilder expects you return a widget for all the cases.
You just convert the var string to list
var jsondecode = json.decode(snapshot.data);
List list = jsondecode;
String name = list[index]["name"];
String id = list[index]["id"];
then pass the list to get the index may be this will work bocasue the var type may be get the first json string only
enter code here

Flutter setState() make a loop to be always recalled

I have a code like this below, the simple flow is I make a loop from a list of objects to create some widgets.
class ScoringAttribute {
int _id;
bool _isdelete;
double _scorehigh, _scorelow, _scorevalue;
String _name, _scoretype, _description, _title;
}
class HomePageState extends State<HomePage> with TickerProviderStateMixin {
List dataScoringAttributes;
List<ScoringAttribute> listScoringAttributeObjects = new List<ScoringAttribute>();
final String urlPresentation = ".../.resentations/getPresentations";
final String urlScoringAttribute = ".../.scoringattributes/getScoringattributes";
Future<String> getPresentationData() async {
var responseScoringAttribute = await http.get(
Uri.encodeFull(urlScoringAttribute),
headers: {"Accept": "application/json"}
);
var scoringAttributeJson = json.decode(responseScoringAttribute.body);
dataScoringAttributes = scoringAttributeJson['scoringattributes'];
for(int i = 0; i < dataScoringAttributes.length; i++) {
var scoringAttributeObject = new ScoringAttribute();
scoringAttributeObject._id = dataScoringAttributes[i]["id"];
scoringAttributeObject._description = dataScoringAttributes[i]["iddescription"];
scoringAttributeObject._isdelete = dataScoringAttributes[i]["isdelete"];
scoringAttributeObject._name = dataScoringAttributes[i]["name"];
scoringAttributeObject._scorehigh = double.parse(dataScoringAttributes[i]["scorehigh"].toString());
scoringAttributeObject._scorelow = double.parse(dataScoringAttributes[i]["scorelow"].toString());
scoringAttributeObject._scoretype = dataScoringAttributes[i]["scoretype"];
scoringAttributeObject._title = dataScoringAttributes[i]["title"];
scoringAttributeObject._scorevalue = double.parse(dataScoringAttributes[i]["scorelow"].toString());
listScoringAttributeObjects.add(scoringAttributeObject);
}
return "Success";
}
List<Widget> scoringAttributeList() {
List<Widget> list = new List();
for(int i = 0; i < listScoringAttributeObjects.length; i++) {
if(listScoringAttributeObjects[i]._scoretype == "slider") {
list.add(
new Container(
child: new Column(
children: <Widget>[
new Column(
children: <Widget>[
//THE SLIDER VALUE TEXT
new Text(
//CONVERT DOUBLE TYPE TO STRING WITHOUT DECIMAL POINTS
listScoringAttributeObjects[i]._scorevalue.toStringAsFixed(listScoringAttributeObjects[i]._scorevalue.truncateToDouble() == listScoringAttributeObjects[i]._scorevalue ? 0 : 0),
style: new TextStyle(
fontSize: 28.0,
),
),
//THE SLIDER
new Slider(
activeColor: Colors.blueAccent,
inactiveColor: const Color(0xFFb7d2e0),
min: double.parse(listScoringAttributeObjects[i]._scorelow.toString()),
max: double.parse(listScoringAttributeObjects[i]._scorehigh.toString()),
value: double.parse(listScoringAttributeObjects[i]._scorevalue.toString()),
onChanged: (double value) {
setState(() {
listScoringAttributeObjects[i]._scorevalue = double.parse(value.round().toString());
});
},
),
],
),
],
),
),
);
}
else if(listScoringAttributeObjects[i]._scoretype == "text_field") {
list.add(...);
}
else if(listScoringAttributeObjects[i]._scoretype == "stars") {
list.add(...);
}
else if(listScoringAttributeObjects[i]._scoretype == "thumb") {
list.add(new Container(...);
}
}
return list;
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: FutureBuilder<String> (
future: getPresentationData(),
builder: (context, snapshot) {
if(snapshot.hasData) {
return new Column(
children: <Widget>[
new Column(
children: scoringAttributeList(),
),
],
),
}
},
),
);
}
}
There are some different widgets depending on the type, and there are 4 types, and 1 type might have more than 1 widget in it, so I make the loop depend on the data that it got from DB.
The problem is I don't know why every time I use setState() inside the loop, it always processes the loop again, so it'll be an infinite loop to create a new widget, and it'll duplicate the widget from the beginning (only happen when the setState() is called).
Ex: there are 4 data inside the List, and if the setState() is called, it'll show 8 data (show the first 4 data twice)
Here's the example of how I setState() into the data inside the List
onChanged: (double value) {
setState(() {
listScoringAttributeObjects[i]._scorevalue = double.parse(value.round().toString());
});
},
I think the problem is because I setState() into some data inside the List. So when the List state is changed, it'll re-render anything that is related to the List.
Is it true?
If yes, is there any other solution how to change my code?
If not, is there any mistake in my code or my logic maybe?
Thank you. Really looking forward to some solution about this, cause I really got stuck in this, and its already been a week :(
Simple move your getPresentationData() into state variable. So that it will get triggered only once.
class HomePageState extends State<HomePage> with TickerProviderStateMixin {
Future<String> _presentationFuture;
initState() {
_presentationFuture = getPresentationData()
}
//other contents
#override
Widget build(BuildContext context) {
return new Scaffold(
body: FutureBuilder<String> (
future: _presentationFuture,
builder: (context, snapshot) {
if(snapshot.hasData) {
Reason for duplicate: we can calling setState on Slider dataChange which will re-render the HomePageState which will again trigger the network call (getPresentationData())
Note: If you want to trigger network on slider change, clear the list before making a network call
Future<String> getPresentationData() async {
listScoringAttributeObjects = new List<ScoringAttribute>(); // clear data
var responseScoringAttribute = await http.get(
Uri.encodeFull(urlScoringAttribute),
headers: {"Accept": "application/json"}
);
I don't see how this would be related to the one setState() in your code. It is only called when the slider is used.
I think the problem is caused by list.add(...); in scoringAttributeList(). You shouldn't modify data when build() is executed.
You should assume that build() can be called repeatedly and at any time.
Build your code so that this doesn't cause issues when it happens.

Resources