StreamBuilder flutter - flutter

I added two item in list, then add the list to sink.
var list = new List<String>();
AbcBloc _abcBloc = AbcBloc(repository: null);
#override
void didChangeDependencies() {
_abcBloc = Provider.of<AbcBloc>(context);
super.didChangeDependencies();
}
#override
void initState() {
super.initState();
list.add('Electrical');
list.add('Inspection');
_abcBloc.listSink.add(list);
}
But when I try to print the list in TextField, it prints empty?
StreamBuilder(
stream: _abcBloc.listStream,
builder: (context, snapshot) {
return TextField(
decoration: InputDecoration(
labelText: snapshot.data,
....
);
},
),
AbcBloc.dart
final _list = BehaviorSubject<List<String>>();
get listSink => _list.sink;
get listStream => _list.stream;
Edit
After use the suggestion answers, it print null value. Why would this happened?

Firstly, snapshot.data is a list of String items so to display the list as a single String you need to add a toString(); Secondly, you need to be more specific with types - you need to specify what data your StreamBuilder expects, i.e.
StreamBuilder<List<String>>(
stream: _abcBloc.listStream,
builder: (context, snapshot) {
return TextField(
decoration: InputDecoration(
labelText: snapshot.data.toString(),
));
},
),
Taking your example and making those two changes then resulted in [Electrical, Inspection] showing in the text field on my device.

Problems I see:
StreamBuilder has no generic type. Make it StreamBuilder<List<String>>
You're passing List to labelText parameter. You should pass String. (e.g. snapshot.data.toString())
You don't check whether your stream has data or not inside builder. You can do it using snapshot.hasData condition or set initialData parameter.

I was able to fix it.What I did was put the list and the stream in didChangeDependencies.
#override
void didChangeDependencies() {
_abcBloc = Provider.of<AbcBloc>(context);
list.add('Electrical');
list.add('Inspection');
_abcBloc.listSink.add(list);
super.didChangeDependencies();
}

Related

Why DropdownButtonFormField Wrapped with FutureBuilder Being Rebuilt with Same Data Appended?

I have created a Flutter stateless dropdown widget that is dependent on some future list, I used the FutureBuilder to build the dropdown as soon as the future is resolved.
But I noticed that build method was being called at least twice. I know it is normal that the build method can be called multiple times when some state changes, but why was the dropdown was being rebuilt with the same data as the previous build call? I thought for sure when build is called, Flutter will rebuild the entire widget which also implies that the previous data will be destroyed as well.
This has resulted in duplication in the items of the dropdown.
I am not sure why it is happening. What did I miss?
class _PetTypeInput extends StatelessWidget {
#override
Widget build(BuildContext context) {
final petTypes = context.read<RegisterPetProfileCubit>().getPetTypes();
return FutureBuilder<List<PetType>>(
future: petTypes,
builder: (BuildContext context, AsyncSnapshot<List<PetType>> snapshot) {
List<PetType>? petKinds = [];
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
petKinds = snapshot.data;
return DropdownButtonFormField<String>(
key: const Key('registerForm_petKindInput_dropdownButtonFormField'),
decoration: const InputDecoration(
labelText: 'pet kind',
helperText: '',
errorText: null,
),
value: 'Dog',
onChanged: (petKindValue) => context
.read<RegisterPetProfileCubit>()
.petKindChanged(petKindValue!),
items: _buildItems(petKinds),
);
}
return const TextField(
enabled: false,
keyboardType: TextInputType.name,
decoration: InputDecoration(
labelText: 'pet kind',
helperText: '',
),
);
},
);
}
List<DropdownMenuItem<String>> _buildItems(List<PetType>? petKinds) {
final petTypes = petKinds!.fold(
<String, String>{},
(Map<String, String> petTypesMap, petType) {
petTypesMap[petType.id] = petType.label;
return petTypesMap;
},
);
List<String> items = petTypes.keys.toList();
return items.map((key) {
return DropdownMenuItem<String>(
key: Key(key),
child: Text(petTypes[key]!),
value: key,
);
}).toList();
}
}
I can definitely tell that there are no duplicates in the data.
How do I prevent appending the same data? Or clear the previous data of DropdownButtonFormField?
You can build _buildItems(petKinds), before return DropdownButtonFormField<String>( and passing item[0] value on DropdownButtonFormField value. And it will make sure the value contain on DropDownMenuItems. Also, make sure to have Unique value on each DropDownMenuItem.
I finally figured it out. The issue is not duplicate dropdown items but rather my initial value is not a valid value. My initial value was Dog when it should be the id of the Dog item.
So I grabed the first item object and then grabed the id of that item and supplied it as initial value.
The answers from this question helped me figured it out.

Flutter error : The argument type 'List<Future<Widget>>' can't be assigned to the parameter type 'List<Widget>'

I'm trying to do a list of item from Firebase Firestore (this is done) and to get for each item a different image URL from Firebase Cloud Storage.
I use a function called getPhotoUrl to change the value of the variable photoUrl. The problem is that the return is executed before getPhotoUrl. If I add await in front of the function getPhotoUrl and async after _docs.map((document), I got an error saying that The argument type 'List<Future>' can't be assigned to the parameter type 'List'.
My code:
class PhotosList extends StatefulWidget {
#override
_PhotosListState createState() => _PhotosListState();
}
class _PhotosListState extends State<PhotosList> {
String photoUrl = 'lib/assets/default-image.png';
List<DocumentSnapshot> _docs;
getPhotoUrl(documentID) {
Reference ref = storage
.ref('Users')
.child(currentUser.uid)
.child('Photos')
.child(documentID)
.child('image_1.jpg');
ref.getDownloadURL().then((value) {
setState(() {
photoUrl = value.toString();
});
}).catchError((e) {
setState(() {
print(e.error);
});
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: firestore
.collection('Users')
.doc(currentUser.uid)
.collection('Photos')
.orderBy('date')
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
_docs = snapshot.data.docs;
if (_docs.isEmpty)
return Center(
child: Text("The list is empty."));
return Container(
child: ResponsiveGridList(
desiredItemWidth: 100,
squareCells: true,
minSpacing: 5,
children: _docs.map((document) {
getPhotoUrl(document.id);
return PhotosListItem(photoUrl: photoUrl);
}).toList(),
),
);
},
);
}
}
I think you mix 2 different ways. In every build cicle you map your docs and request that photoUrl, but inside that method you call setState, which re-triggers your build method. That way you should end in infinite loop of getting photo url and building your widget.
You have three options:
Load your photoUrls and store them inside your widget -> call set state -> check inside your mapping function if your photo is loaded, if yes, take it, if no, call your getPhotoUrl function
Load your photoUrls synchronously and return url from your function and set it to your PhotosListItem
(I would prefer this) Add your documentId to your photosListItem in your mapping function and inside your item you load this photo url. In this PhotoListItem you have a variable with your imageUrl and in initState you call your getPhotoUrl function
Inside your PhotoItem:
String imageUrl;
#override
void initState() {
Future.delayed(Duration.zero, () {
setState(() {
// load your data and set it to your variable
imageUrl = ..
});
});
super.initState();
}
You might use a FutureBuilder because StreamBuilder seems to be synchronous :
How to convert Future<List> to List in flutter?
Thanks for your answers guys, actually I found an other solution which is to get and write the URL in Firestore directly after uploading the image on the Storage.
This is the article which helped me a lot : https://medium.com/swlh/uploading-images-to-cloud-storage-using-flutter-130ac41741b2
(PS: some of the Firebase names changed since this article but it's still helpful.)
Regards.

Flutter: How to delete item from listview?

DBHelper dbHelper = DBHelper();
List<Map<String, dynamic>> lists;
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Map<String, dynamic>>>(
future: dbHelper.selectMemo(userkey, 1),
builder: (context, snapshot){
if(snapshot.hasData){
if(snapshot.data.length != 0){
lists = List<Map<String, dynamic>>.from(snapshot.data);
return ListView.separated(
separatorBuilder: (context, index){
return Divider(
thickness: 0,
);
},
itemCount: lists.length,
itemBuilder: (context, index){
return ListTile(
title: Text(lists[index]["memo"]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: (){
setState(() {
lists = List.from(lists)..removeAt(index);
});
},
),
);
},
);
}
}
},
);
}
This is my code. My lists come from sqlflite. And I want to delete my item from Listview. But this code doesn't work. I don't know where I made the mistake.
This behavior is normal. If you print some logs in the build statement, you will find that every time you click the delete button (setState), Widget will build again.
In addition, lists are re-assigned to DB data after each build
lists = List<Map<String, dynamic>>.from(snapshot.data);
So, it looks like the delete operation is not working.
This phenomenon if you've seen Flutter setState part of the source code will be well understood.
In setState, the callback is performed first, and then mark dirty
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
So, there are two ways to solve this problem:
(1) Do not directly change the value of lists, but when the delete button is pressed, to delete the data in the database, so that when Widget build again, the data taken out of the database is correct.
(2) Add a flag to judge whether the data is initialized, and then add a judgment before assigning lists. If the data is initialized, assignment operation will not be carried out
I hope it worked for you. ^-^

Flutter List View through Future Provider Value

I am trying to call the output of a Future Provider of type Future> into a List View builder. I think I am very near as I am able to render the final List View itself, however, prior that, an error appears and is quickly replaced by the List View after completing the Future. I believe there may be something wrong with my implementation there.
Here's what I've got so far (these are derivatives of my actual code, there's too many going on there that aren't necessary, I tried to simplify it):
class TempProvider extends ChangeNotifier(){
List<Widget> _list = <Widget>[];
List<Widget get list => _list;
Future<List<Widget>> getList() async{
List _result = await db....
_result.forEach((_item){
addToList(_item);
});
}
addToList(Widget widget){
_list.add(widget);
notifyListeners();
}
}
class Parent extends StatelessWidget{
#override
Widget build(BuildContext context) {
return FutureProvider(
create: (context) => TempProvider().getList(),
child: Child(),
);
}
}
class Child extends StatelessWidget{
#override
Widget build(BuildContext context) {
var futureProvider = Provider.of<List<Widget>>(context);
return FutureBuilder(
initialData: <Widget>[],
future: TempProvider().getList(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.none &&
snapshot.hasData == true) {
return ListView.builder(
itemCount: futureProvider.length,
itemBuilder: (BuildContext context, int index) {
return futureProvider[index];
},
);
} else {
return Text('ALAWS');
}
},
);
}
}
So basically, the output of my Future will be a list of widgets that will populate a List View that I am trying to build. Though I am able to render the list view in the end, the error below appears in between:
The getter 'length' was called on null.
Receiver: null
Tried calling: length
The relevant error-causing widget was
FutureBuilder<List<Widget>>
Hoping someone can help with this one or at least give a better example.
Thank you so much!
Not sure but, this is happening because your widget might be building twice and at first futureProvider is null and in second time it has some value.
Workaround:
Replace this:
futureProvider.length
With this:
futureProvider?.length ?? 0
What the above code does?
futureProvider?.length: if futureProvider is null don't access it's length.
Now the value returned will be null.
?? 0: if the value returned is null then return 0;
You need to think over following things and edit your code.
At first place futureProvider should not be null.
Why are you not using snaphot.data when you are using FutureBuilder.
So I have been doing my research and found the article below which shows a definite implementation based on what I need:
Flutter Provider Examples - Codetober
Credits to Douglas Tober for the article. Thanks again to Kalpesh for the quick help!

How to run a function inside a child stateful widget from parent widget?

I am trying to run a function(with arguments) inside two-levels down StateFul widget, by clicking a button in the parent of the parent of that child(after having all widgets built, so not inside the constructor). just like in the image below:
More details is that I created a Carousal which has Cards inside, published here.
I created it with StreamBuilder in mind(this was the only use case scenario that I used it for so far), so once the stream send an update, the builder re-create the whole Carousal, so I can pass the SELECTED_CARD_ID to it.
But now I need to trigger the selection of the carousal's Cards programmatically, or in another word no need for two construction based on the snapshot's data like this:
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return SelectableCarousal(
selectedId: snapshot.data.toInt(),
onTap: //Update the stream
//some props...,
);
} else {
return SelectableCarousalLoading(
selectedId: null,
onTap: //Update the stream
//some props...,
);
}
},
);
But instead, I'm trying to have something like this so I can use it for others use cases:
Widget myCarousal = SelectableCarousal(
selectedId: null,
onTap: //Update the stream
//some props...,
);
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
// Then when data ready I can update
// the selection by calling two-level down function
if (snapshot.hasData) {
myCarousal.selectById(3);
}
// Build same carousal in all cases.
return myCarousal;
},
);
so this led me to my original question "How to run a function(with arguments) inside two-levels down StateFul widget?".
I appreciate any help. thanks a lot.
I was able to solve that challenge using the BLoC & Stream & StreamSubscription, see the image below:
Inside the Homepage screen:
///...Inside Homepage screen level-0
RaisedButton(
child: Text('Update value in the BLoC'),
onPressed: () {
bloc.changeSelectedState(isSel);
},
),
//...
inside the BLoC:
class Bloc {
final BehaviorSubject<bool> _isSelectedStreamController = BehaviorSubject<bool>();
// Retrieve data from stream
Stream<bool> get isSelectedStream => _isSelectedStreamController.stream;
// Add data to stream
Function(bool) get changeSelectedState => _isSelectedStreamController.sink.add;
void dispose() {
_isSelectedStreamController.close();
}
}
final bloc = Bloc();
Inside any widget in any level as long as it can reach the bloc:
// This inside the two-levels down stateful widget..
StreamSubscription isSelectedSubscription;
Stream isSelectedStream = bloc.isSelectedStream;
isSelectedSubscription = isSelectedStream.listen((value) {
// Set flag then setState so can show the border.
setState(() {
isSelected = value;
});
});
//...other code
#override
Widget build(BuildContext context) {
return Container(
decoration: isSelected
? BoxDecoration(
color: Colors.deepOrangeAccent,
border: Border.all(
width: 2,
color: Colors.amber,
),
)
: null,
//...other code
);
}
so the new design of my widget includes the BLoC as a main part of it, see the image:
and...works like a charm with flexible and clean code and architecture ^^