Snapshot data null but FutureBuilder return data? - flutter

I have http response data but IT IS NULL?????
...
Future getcategoryimage() async{
var url = "http://172.17.40.225/shoplacviet/getcategoryimage.php";
var reponse = await http.get(Uri.parse(url));
var list = reponse.body;
Uint8List _bytesImage;
_bytesImage = Base64Decoder().convert(list);
return _bytesImage;
}
...
FutureBuilder(
future: getcategoryimage(),
builder: (context,snapshot){
List lista = snapshot.data as List;//------------> I have http response data but IT IS NULL?????
if(snapshot.hasError) print(snapshot.error);
return snapshot.hasData ? ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: lista.length,
itemBuilder: (context,index){
var blob = lista[index]['categoryimage'];
Uint8List _bytesImage;
_bytesImage = Base64Decoder().convert(blob);
return Container(
child: Image.memory(_bytesImage),
);
}):Center(child: CircularProgressIndicator(),) ;
},
),

Do not access data before it is available. Use hasData and hasError properties something like this:
FutureBuilder<future type>(
future: _future, // a previously-obtained Future
builder: (BuildContext context, AsyncSnapshot<future type> snapshot) {
if (snapshot.hasData) {
// here snapshot.data is available
return <hasData widget>
} else if (snapshot.hasError) {
return <hasError widget>
} else {
return <waiting widget>
}
}
)

You're building the future as the future: argument to your FutureBuilder. Since this is in a build() method, your future might be getting reset up to 60 times per second. The proper strategy (according to the first few paragraphs of the documentation) is:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
A general guideline is to assume that every build method could get called every frame, and to treat omitted calls as an optimization.
So there you have it. Move your call to getcategoryimage() out into initState(), saving it into a State variable.
I illustrate this in a 10-minute video, if you need further clarification.

Related

uid is not defined for type 'Object' in flutter

i am trying to check if a user id is the same as the current user's id by using data.uid but i keep getting this error:
The getter 'uid' isn't defined for the type 'Object'.
this is the code
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.value(FirebaseAuth.instance.currentUser),
builder: (context, futureSnapshot){
if(futureSnapshot.connectionState == ConnectionState.waiting){
return Center(child: CircularProgressIndicator(),);
}
return StreamBuilder <QuerySnapshot>(
stream: firestore.collection('chats').orderBy('timeStamp', descending: true).snapshots(),
builder:(ctx, chatSnapshot){
if(chatSnapshot.connectionState == ConnectionState.waiting){
return Center(child: CircularProgressIndicator(),);
}
final chatdocs = chatSnapshot.data!.docs;
return ListView.builder(
reverse: true,
itemCount: chatdocs.length ,
itemBuilder: (ctx, index) => messageBubble(
chatdocs[index]['text'],
chatdocs[index]['userId'] == futureSnapshot.data!.uid, //this is the error
)
);
}
);
} );
Since you don't declare the type of the Future in your FutureBuilder, it resolves to an Object. And an Object doesn't have a uid property, which is why you get the error.
To solve this declare the type of your FutureBuilder, which in your case would be:
return FutureBuilder<User>(
Note that I have no idea why you're using a FutureBuilder here to begin with. The FirebaseAuth.instance.currentUser is a synchronous value, so you don't need a FutureBuilder to access it. Removing the FutureBuilder would lead to the exact same result.
If you're trying to make your code respond to auth state changes, like that it takes a moment for Firebase to restore the user's sign-in state when you start the app, you'll want to actually listen to authStateChanges with a StreamBuilder for that nowadays, as shown in the first code snippet in the documentation on getting the current user. Here too, you'll want to declare your StreamBuilder with a User type, just like we did above for the FutureBuilder.
You can try json decoding your variable into the object User you are looking for:
If the futureSnapshot.data is a user you'll be able to use the uid as a map key to check with chatdocs[index]['userId']
Like this:
import 'dart:convert';
final Map<String, dynamic> _valueMap = json.decode(futureSnapshot.data!);
chatdocs[index]['userId'] == _valueMap['uid'];
Try the following code:
StreamBuilder<QuerySnapshot>(
stream: firestore.collection('chats').orderBy('timeStamp', descending: true).snapshots(),
builder: (ctx, chatSnapshot) {
if (chatSnapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
final List<QueryDocumentSnapshot<Object?>> chatdocs = chatSnapshot.data!.docs;
final String uid = FirebaseAuth.instance.currentUser?.uid ?? '';
return ListView.builder(
reverse: true,
itemCount: chatdocs.length,
itemBuilder: (ctx, index) {
final Map<String, dynamic> chat = (chatdocs[index]).data() as Map<String, dynamic>;
return messageBubble(
chat['text'],
chat['userId'] == uid,
);
},
);
},
),

FutureBuilder update by timer

I don't quite understand how you can update future in FutureBuilder by timer. I tried to create a timer and give it to the future, but it didn't work out and there was an error: type '_Timer' is not a subtype of the 'Future?'
my handler with a request:
Future<ObjectStateInfoModel> read(int id) async {
TransportResponse response = await transport.request(
'get',
RequestConfig(path: path + '($id)'),
TransportConfig(
headers: {},
));
ObjectStateInfoModel objectState = ObjectStateInfoModel.fromJson(response.data);
return objectState;
}
my FutureBuilder:
return FutureBuilder<ObjectStateInfoModel>(
future: logexpertClient.objectsState.read(object.id),
builder: (context, snapshot) {
if (snapshot.hasData) {
final data = snapshot.data!;
on the advice of one of the commentators i converted FutureBuilder to StreamBuilder and created such a stream and then everything works correctly:
stream = Stream.periodic(const Duration(seconds: 5)).asyncMap((_) async {
return logexpertClient.objectsState.read(object.id);
});
Use refreshable_widget, which is built specifically for this.
https://pub.dev/packages/refreshable_widget
Flexible(
child: RefreshableWidget<num>(
initialValue: challenge.userParticipation!.donePercent,
refreshCall: () async {
final challenge =
await cadoo.getChallengeDetail(
id: widget.challengeId,
);
return challenge.userParticipation!.donePercent;
},
builder: (context, value) {
return DonePercentWidget(
percent: value,
);
},
),
),
Pass a refresh call and how often you want to refresh, widget will build whatever on builder method.

StreamBuilder not refreshing after asyncMap future resolves

I'm using the following StreamBuilder in a Stateful widget:
StreamBuilder<List<int>>(
stream: widget.model.results(widget.type),
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
if (snapshot.hasError) return Text('Error');
final List<int> results = snapshot.data;
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
return _buildListTile(results[index]);
});
})
And here's the bit where the Streams get built:
// inside the ViewModel
late final List<StreamController> _streamControllers = [
StreamController<List<int>>.broadcast(),
StreamController<List<int>>.broadcast(),
];
List<int> _results = [];
Stream<List<int>> results(int index) =>
_streamControllers[index]
.stream
.debounce(Duration(milliseconds: 500))
.asyncMap((filter) async {
final List<int> assets = await search(filter); // 👈 Future
return _results..addAll(assets);
});
The issue is that the UI doesn't get rebuilt after the search results are returned.
The debugger shows that the Future is getting resolved correctly, but that the UI doesn't get rebuilt once the result is returned (within asyncMap).
Am I using asyncMap correctly? Is there an alternative way to set this up that could potentially get it working?
EDIT: Showing the code that adds events to the stream
[0, 1].forEach((index) =>
textController.addListener(() =>
_streamControllers[index]
.sink
.add(textController[index].text));
U are using asyncMap correctly.
Your issue might be that you add events to stream before Streambuilder starts to listen to widget.model.results(widget.type) stream.
Either use:
BehaviorSubject
final List<BehaviorSubject> _streamControllers = [
BehaviorSubject<List<int>>(),
BehaviorSubject<List<int>>(),
];
or add events AFTER widgets are built (or when we start to listen to them)
How to use onListen callback to start producing events?
You are creating a new Stream every build therefore it will be always empty and won't update correctly. You have the same controller, but asyncMap is creating a new Stream under the hood. The docs:
Creates a new stream with each data event of this stream asynchronously mapped to a new event.
The fix would be to save the instance of the stream after asyncMap is used. This can be done multiple ways. One would be to make a late initialized field inside your State.
late Stream<List<int>> myStream = widget.model.results(widget.type);
and then use this instance in the StreamBuilder:
StreamBuilder<List<int>>(
stream: myStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
if (snapshot.hasError) return Text('Error');
final List<int> results = snapshot.data;
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
return _buildListTile(results[index]);
});
})
But you can also save the instance in initState or completely outside the widget and make Stream<List<int>> results(int index) return the saved instance or make it the list like this:
List<Stream<List<int>>> results = _streamControllers
.map((s) => s.stream.asyncMap((filter) async {
final List<int> assets = await search(); // 👈 Future
return _results..addAll(assets);
}))
.toList();

How to load list only after FutureBuilder gets snapshot.data?

I have a JSON method that returns a List after it is completed,
Future<List<Feed>> getData() async {
List<Feed> list;
String link =
"https://example.com/json";
var res = await http.get(link);
if (res.statusCode == 200) {
var data = json.decode(res.body);
var rest = data["feed"] as List;
list = rest.map<Feed>((json) => Feed.fromJson(json)).toList();
}
return list;
}
I then call this, in my initState() which contains a list hello, that will filter out the JSON list, but it shows me a null list on the screen first and after a few seconds it loads the list.
getData().then((usersFromServer) {
setState(() {. //Rebuild once it fetches the data
hello = usersFromServer
.where((u) => (u.category.userJSON
.toLowerCase()
.contains('hello'.toLowerCase())))
.toList();
users = usersFromServer;
filteredUsers = users;
});
});
This is my FutureBuilder that is called in build() method, however if I supply the hello list before the return statement, it shows me that the method where() was called on null (the list method that I am using to filter out hello() )
FutureBuilder(
future: getData(),
builder: (context, snapshot) {
return snapshot.data != null ?
Stack(
children: <Widget>[
CustomScrollView(slivers: <Widget>[
SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
child: puzzle[0],
);
},
childCount: 1,
),
)
]),
],
)
:
CircularProgressIndicator();
});
You are calling your getData method multiple times. Don't do that. Your UI waits for one call and your code for the other. That's a mess. Call it once and wait for that call.
You need to define the future to wait for in your state:
Future<void> dataRetrievalAndFiltering;
Then in your initstate, assign the whole operation to this future:
(note that I removed the setState completely, it's not needed here anymore)
dataRetrievalAndFiltering = getData().then((usersFromServer) {
hello = usersFromServer.where((u) => (u.category.userJSON.toLowerCase().contains('hello'.toLowerCase()))).toList();
users = usersFromServer;
filteredUsers = users;
});
Now your FurtureBuilder can actually wait for that specific future, not for a new Future you generate by calling your method again:
FutureBuilder(
future: dataRetrievalAndFiltering,
builder: (context, snapshot) {
Now you have one Future, that you can wait for.
This response is a little too late to help you, but for anyone wondering how to load a Future and use any of it's data to set other variables without having the issue of saving it in a variable, and then calling setState() again and loading your future again, you can, as #nvoigt said, set a variable of the Future in your state, then you can call the .then() function inside the addPostFrameCallback() function to save the result, and finally using the future variable in your FutureBuilder like this.
class _YourWidgetState extends State<YourWidget> {
...
late MyFutureObject yourVariable;
late Future<MyFutureObject> _yourFuture;
#override
void initState() {
super.initState();
_yourFuture = Database().yourFuture();
WidgetsBinding.instance?.addPostFrameCallback((_) {
_yourFuture.then((result) {
yourVariable = result;
// Execute anything else you need to with your variable data.
});
});
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _yourFuture,
builder: ...
);
}
}
dynamic doSomeStuffWithMyVariable() {
// Do some stuff with `yourVariable`
}
So, the advantage of this is that you can use the data loaded in the future outside the scope of the FutureBuilder, and only loading it once.
In my case, I needed to get a map of objects from my future, then the user could select some of those objects, save them on a map and make some calculations based on that selection. But as selecting that data called setState() in order to update the UI, I wasn't able to do so without having to write a complicated logic to save the user selection properly in the map, and having to call the future again outside the scope of the FutureBuilder to get it's data for my other calculations.
With the example above, you can load your future only once, and use the snapshot data outside the scope of the FutureBuilder without having to call the future again.
I hope I was clear enough with this example. If you have any doubts I will gladly clarify them.

AsyncSnapshot rejecting Type Annotation

I have a StreamBuilder that is taking data from my bloc component.
However it keeps rejecting my type annotation AsyncSnapshot<List<int>> snapshot and only accepts dynamic as a type AsyncSnapshot<List<dynamic>> snapshot. Yet in the examples i've viewed they do have type annotaions with no complaints.
Here is my stream creation.
Widget buildList(StoriesBloc bloc) {
return StreamBuilder(
stream: bloc.topIds,
builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) {
if (!snapshot.hasData) {
return Text("Still Waiting for Ids to fetch");
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, int index) {
return Text('${snapshot.data[index]}');
},
);
},
);
}
Here is the VSCode error generated.
What could i be doing wrong ?
Turns out my bloc.topIds result type was not of type List<int>.
Observable<List> get topIds => _topIds.stream;
So i simply changed it to fulfill the required type.
Observable<List<int>> get topIds => _topIds.stream;
And that fixed the issue. Hope it helps someone else.