How to properly feed data to StreamBuilder initialData parameter? - dart

I am using a StreamBuilder widget to display some data. When the app is recently opened, I wanted to display some initial data from my json file and feed it into the initialData optional keyword parameter of StreamBuilder.
Here is how I feed it:
MyStorage m = new MyStorage(); // Using path_provider, I accessed the json file inside this class
int x;
#override
void initState(){
super.initState();
getData();
}
getData() async{
Map<String, dynamic> myMap = await m._getMap;
x = int.parse(myMap["total"]);
}
...
#override
Widget build(BuildContext context){
...
child: StreamBuilder(
stream: mystream, // coming from my BLoC class
initialData: x,
builder: (context, snapshot){
return new Text("${snapshot.data}");
}
...
The problem is it that the Text widget inside my StreamBuilder is displaying "null".
I tried to rewrite my code to this:
MyStorage m = new MyStorage();
Future<int> getData() async{
Map<String, dynamic> myMap = await m._getMap;
return int.parse(myMap["total"]);
}
...
#override
Widget build(BuildContext context){
...
child: StreamBuilder(
stream: mystream, // coming from my BLoC class
initialData: getData(),
builder: (context, snapshot){
return new Text("${snapshot.data}");
}
...
but it displayed on my Text widget as "Instance of Future:int"
I have no problem with my stream parameter in the StreamBuilder. It displays the correct value that I'm expecting from the BLoC class.
The only problem I had is the feeding of initialData from my json file.
What am I doing wrong? I would appreciate any kind of help. Thanks
[UPDATE]
After a long hours of thinking a solution, I gave up using initialData parameter since after I added int to StreamBuilder like this StreamBuilder<int>() it prompted me that it will only take a value of integer. I couldn't fed it with a type of Future or Stream so I decided not to use it. What I did was I nested a FutureBuilder inside the StreamBuilder through ConnectionState.
Here is what my code now:
MyStorage m = new MyStorage();
Future<int> getData() async{
Map<String, dynamic> myMap = await m._getMap;
return int.parse(myMap["total"]);
}
...
#override
Widget build(BuildContext context){
...
child: StreamBuilder<int>(
stream: mystream, // coming from my BLoC class
//initialData: getData(),
builder: (context, snapshot){
swith(snapshot.connectionState){
case ConnectionState.none:
return new FutureBuilder(
future: getData(),
builder: (context, snapshot){
return new Text('${snapshot.data}');
}
);
case ConnectionState.active:
case ConnectionState.waiting:
return new FutureBuilder(
future: getData(),
builder: (context, snapshot){
return new Text('${snapshot.data}');
}
);
case ConnectionState.done:
if (snapshot.hasData){
return new Text('${snapshot.data}');
}
return new FutureBuilder(
future: getData(),
builder: (context, snapshot){
return new Text('${snapshot.data}');
}
);
}
}
...
I know that this solution is very inefficient but I couldn't think of a better solution as of now.

initialData is supposed to be the data to show before your actual data is available.
place 0 as initialData.
StreamBuilder has an additional parameter stream which is responsible for fetching your stream data. here you can do stream: getData().asStream,
edit:
Also make sure to specify the type of data you are expecting in your StreamBuilder. StreamBuilder<int>()

Related

FutureBuilder running two times bug

I added debug point for
future: _futureData
Future getRegister1() async{ first run getRegister1() and then future: _futureData and again two times run getRegister1() after
that screen comes _mainCategory State Widget
class _mainCategory extends State<mainCategory3> {
Future _futureData;
#override
void initState() {
super.initState();
_futureData = getRegister1();
}
#override
Widget build(BuildContext context) {
return Scaffold(
FutureBuilder inside Body
body: FutureBuilder(
future: _futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
getRegister1()
Future getRegister1() async {
//this data coming from cache (sqflite)
List _catLocal = (await HelperDatabase1().displayRegisterCat());
List _defCatLocal = (await HelperDatabase1().display());
...
}
full code https://github.com/flutter/flutter/issues/31838

How to prevent FutureBuilder's rebuilds even its underlying future does not change

In Flutter, I am getting unwanted rebuilds. In my case I use FutureBuilder to show a list by fetching db result, which is a future and have a dependency on query parameter. I tried to make it that FutureBuilder's future does not change if the query parameter does not change, But still the FutureBuilder's builder block is called every time. How can I make the FutureBuilder does not rebuild itself where its future does not change.
Below is my codes, every time the MusicList2's parent widget build, MusicList2 get rebuild, its FutureBuilder get rebuild.
class MusicList2 extends StatefulWidget {
final MusicRowActionCallback onTapItem;
final MusicRowActionCallback onDoubleTap;
final MusicRowActionCallback onLongPressed;
final String facetName;
final String facetValue;
const MusicList2(
{Key key,
this.onTapItem,
this.onDoubleTap,
this.onLongPressed,
this.facetName,
this.facetValue}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _MusicList2State();
}
}
class _MusicList2State extends State<MusicList2> {
Future<List<Music>> loadMusicByFacet;
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Music>>(
future:
loadMusicByFacet,
builder: (context, snapshot) {
if (snapshot.data == null)
return Center(child: CircularProgressIndicator(),);
return ListView.builder(
shrinkWrap: true,
key: const ValueKey<String>('music-list'),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
final random = Random();
var i = random.nextInt(5);
return MusicRow(
avatarBgColor: colors[i],
music: snapshot.data[index],
onTap: widget.onTapItem,
onDoubleTap: widget.onDoubleTap,
onLongPressed: widget.onLongPressed,
);
},
);
},
);
}
#override
void initState() {
super.initState();
loadMusicByFacet = MusicsDatabaseRepository.get.getMusicsByFacet(widget.facetName, widget.facetValue);
}
#override
void didUpdateWidget(MusicList2 oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.facetValue != widget.facetValue || oldWidget.facetName != widget.facetName) {
loadMusicByFacet = MusicsDatabaseRepository.get.getMusicsByFacet(widget.facetName, widget.facetValue);
}
}
}
For anyone else having this error, I solved it using AbdulRahmanAlHamali's solution on GitHub. This is his answer:
Hello, I believe the problem here is that didUpdateWidget of the
FutureBuilder state is being called every time a rebuild is issued.
This function checks if the old future object is different from the
new one, and if so, refires the FutureBuilder.
To get past this, we can call the Future somewhere other than in the
build function. For example, in the initState, and save it in a member
variable, and pass this variable to the FutureBuilder.
So instead of having:
FutureBuilder(
future: someFunction(),
....
We should have:
initState() {
super.initState();
_future = SomeFunction();
}
and then
FutureBuilder(
future: _future,
....
Find the original thread here.

Fetching all entries from db(Firestore) to a list using a model

Trying to fetch a List from firestore using a Model which works without the model but not with it.
In a StatefulWidget I got an empty List of Products. basically comes from the model like this
class Product {
int id;
String title, price, message;
Product(this.id, this.title, this.price, this.message);
}
The List of Products like this
import 'package:presentation_app/models/product.dart';
class _ProductsListState extends State<ProductsList> {
List<Product> products = [];
#override
Widget build(BuildContext context) {
return Scaffold(
...
then added a StreamBuilder with the firestore collection like this
StreamBuilder(
stream: Firestore.instance.collection('products').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot){
if (!snapshot.hasData) return CircularProgressIndicator();
return BuildListView(products);
}
),
and in the BuildListView class I've then called the List of Products and used it like so:
class BuildListView extends StatelessWidget {
final List<Product> products;
BuildListView(this.products);
in the listViewBuilder I then tried to access the data like so:
ListView.builder(
itemCount: products.length,
itemBuilder: (BuildContext context, int index) {
int id = products[index].id;
String title = products[index].title;
String message = products[index].message.toString();
String price = products[index].price.toString();
I get the CircularProgressIndicator but after it finishes loading nothing pops up.
using this approach it works, but of course not with the model
body: StreamBuilder(
stream: Firestore.instance.collection('products').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot){
if (!snapshot.hasData) return CircularProgressIndicator();
return BuildListView(products: snapshot.data.documents);
}
),
);
}
}
class BuildListView extends StatelessWidget {
final List<DocumentSnapshot> products;
BuildListView({this.products});
...
In this case the model is not needed, because I was not working with a business logic, so the model is obsolete, If using Bloc and a db like firebase the bloc using a model should look something like this.
Future<void> createData(docId) {
final Tracker createTracker = Tracker(
id: docId,
comment: _commentController.value,
exercise: _exerciseController.value,
number: _numberController.value,
repetition: _repetitionController.value,
sets: _setsController.value,
weight: _weightController.value
);
print(_commentController.value + _numberController.value.toString());
return trackerDb.createData(docId, createTracker);
}
and the db model for creating data something like this for example.
Future createData(String docId, Tracker tracker) async {
await db.document(docId).setData(convertTrackerToMap(tracker));
}

From function Widget to StatelessWidget in Flutter

I'm working on firestore from Google Lab example. What I want to happen is convert _buildList() and _buildListItem() function Widget into StatelessWidget including parameters because I red an article that splitting into function Widget is performance antipattern. But I don't know where to start. Anyone who can give a shed of light to this problem. Thank You.
class _VideoListState extends State<VideoList> {
#override
Widget build(BuildContext context) {
...
body: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection(widget.category).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
// I want StatelessWidget not function widget
return _buildList(context, snapshot.data.documents);
},
),
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
return ListView(
// I want StatelessWidget not function widget
children: snapshot.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot(data);
return Column(
children: <Widget>[
Text(record.title),
YoutubePlayer(
source: record.videoId.toString(),
quality: YoutubeQuality.LOW,
autoPlay: false,
context: context
);
}
}
It's simple. Take a look at the source code and read the comments. The source is auto explained by itself. I have used your methods names as class names.
// the method buildList into a stateless widget
class BuildListWidget extends StatelessWidget{
final List<DocumentSnapshot> snapshotList;
BuildListWidget({this.snapshotList}){} // you can use this approach to initialize your snapshotList.
// Here there parameter is already the member of class snapshotList
#override
Widget build(BuildContext context) {
//building a listView in this way allows you build items on demand.
return ListView.builder(
itemCount: snapshotList.length, // number of items in list
itemBuilder: (BuildContext context, int index){
//creating list members. Each one with your DocumentSnapshot from list
return BuildListItemWidget(
dataSnapshot: snapshotList[index], // getting DocumentSnapshot from list
);
}
);
}
}
// the mehtod _buildListItem into a stateless widget
class BuildListItemWidget extends StatelessWidget {
final DocumentSnapshot _data; // just if you want to hold a snapshot...
final Record _record; // your record reference
//here another approach to inicialize class data using named parameters and
// initialization list in class contructor
BuildListItemWidget({#required DocumentSnapshot dataSnapshot}) :
_record = Record.fromSnapshot(dataSnapshot),
_data = dataSnapshot;
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(record.title),
YoutubePlayer(source: _record.videoId.toString(),
quality: YoutubeQuality.LOW,
autoPlay: false,
context: context
);
}
}
// usage...
class _VideoListState extends State<VideoList> {
#override
Widget build(BuildContext context) {
...
body: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection(widget.category).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
// so here you have a statelessWidget
return BuildListWidget( snapshotList: snapshot.data.documents );
},
),
}
}

Usage of FutureBuilder with setState

How to use the FutureBuilder with setState properly? For example, when i create a stateful widget its starting to load data (FutureBuilder) and then i should update the list with new data, so i use setState, but its starting to loop for infinity (because i rebuild the widget again), any solutions?
class FeedListState extends State<FeedList> {
Future<Null> updateList() async {
await widget.feeds.update();
setState(() {
widget.items = widget.feeds.getList();
});
//widget.items = widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<Null>(
future: updateList(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Center(
child: new CircularProgressIndicator(),
);
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics:
const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: widget.items.length,
itemBuilder: (context, index) {
return FeedListItem(widget.items[index]);
},
),
onRefresh: updateList,
),
);
}
},
);
}
}
Indeed, it will loop into infinity because whenever build is called, updateList is also called and returns a brand new future.
You have to keep your build pure. It should just read and combine variables and properties, but never cause any side effects!
Another note: All fields of your StatefulWidget subclass must be final (widget.items = ... is bad). The state that changes must be stored in the State object.
In this case you can store the result (the data for the list) in the future itself, there is no need for a separate field. It's even dangerous to call setState from a future, because the future might complete after the disposal of the state, and it will throw an error.
Here is some update code that takes into account all of these things:
class FeedListState extends State<FeedList> {
// no idea how you named your data class...
Future<List<ItemData>> _listFuture;
#override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
void refreshList() {
// reload
setState(() {
_listFuture = updateAndGetList();
});
}
Future<List<ItemData>> updateAndGetList() async {
await widget.feeds.update();
// return the list here
return widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<List<ItemData>>(
future: _listFuture,
builder: (BuildContext context, AsyncSnapshot<List<ItemData>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data ?? <ItemData>[]; // handle the case that data is null
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return FeedListItem(items[index]);
},
),
onRefresh: refreshList,
),
);
}
},
);
}
}
Use can SchedulerBinding for using setState() inside Future Builders or Stream Builder,
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isServiceError = false;
isDataFetched = true;
}));
Screenshot (Null Safe):
Code:
You don't need setState while using FutureBuilder.
class MyPage extends StatefulWidget {
#override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
// Declare a variable.
late final Future<int> _future;
#override
void initState() {
super.initState();
_future = _calculate(); // Assign your Future to it.
}
// This is your actual Future.
Future<int> _calculate() => Future.delayed(Duration(seconds: 3), () => 42);
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<int>(
future: _future, // Use your variable here (not the actual Future)
builder: (_, snapshot) {
if (snapshot.hasData) return Text('Value = ${snapshot.data!}');
return Text('Loading...');
},
),
);
}
}

Resources