How to set local state with flutter bloc - flutter

I have a Flutter app that uses Bloc pattern for state management.
I have a local variable in a page that stores state.
PageA:
// Outside build function
List<String> _names = []; // local variable to show the changes visually
.
.
.
// Inside build function
return Scaffold(
body: BlocConsumer<PageABloc, PageAState>(
builder: (context, state) {
if (state is PageASuccessState) {
_names = state.names; // here is problem
return _body();
} else if (state is PageALoadingState ||
state is PageAInitState) {
return Center(child: CircularProgressIndicator());
} else
return Center(child: Text("Something went wrong"));
},
listener: (context, state) {},
),
);
I need to only update state of state.names when user clicks on a save button that is in _body(), but to show the visual changes to user, I'm using _names local variable.
How to load the initial values of state.names to _names?
The code that I tried doesn't work as it resets any changes to _names(local) on every frame.
I tried something like,
if (state is PageASuccessState) {
final List<String> _n = state.names;
_names.addAll(_n);
return _body(_width);
}
But this just adds state.names repeatedly to _names, infinite number of times.
Help!

This is happening because your state is containing the names list every time builder of BlocBuilder is called.
You should replace List<String> _names = [] with LinkedHashSet<String> _names = LinkedHashSet<String>();
LinkedHashSet will work exactly like list but it won't allow duplicates. You can change it to list by calling
_names.iterator.toList(); anytime you want.

Related

How to update the initial state on Flutter

I'm building a flutter page that shows to the users a list of the credit cards that are stored on the back-end and lets the user delete already existing cards.
To fetch the cards from the back-end I'm using initState(). Note that controller.getCreditCards() returns a Future<List<CreditCardSummary>>:
#override
void initState() {
super.initState();
_futureCreditCards = controller.getCreditCards();
}
This List is then rendered using a FutureBuilder, just like the documentation recommends:
Widget build(BuildContext context) {
return FutureBuilder<List<CreditCardSummary>>(
future: _futureCreditCards,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Scaffold(
// My page that renders all the cards goes here
// Inside this future builder I can access the cards list with snapshot.data
);
} else if (snapshot.hasError) {
return Text('ERROR');
}
// Show a loading screen while the data is beeing fetched:
return const CircularProgressIndicator();
});
}
This is all working fine, the problem only begins when I need to update this data. For example, when the user deletes a creditCard, I want to delete the card from the List and to re-render the page with the new version of the List, but I don't know a good way of doing that.
In order to get updated data/ refresh the FutureBuilder, you need to reassign the future variable.
For your case, when ever you like to update,
_futureCreditCards = controller.getCreditCards();
setState((){});

Table_calendar not show event indicator and event when data load

I have load data in init state but the event indicator does not show and when I click on other date and come bak data gone.
Here is my code file
Since initState is not async, you normally wouldn't add code to it that executes asynchronously (like a fetch of data). Instead, I would suggest using a FutureBuilder (or StreamBuilder.periodic if you want to keep fetching your data periodically).
Say you have the function
Future<List<Event>> fetchEvents() async {
...
return events;
}
Which fetches the events from your local or online database and returns them as a list. You could then wrap your widget in a FutureBuilder:
FutureBuilder<List<Event>>(
future: fetchEvents,
builder: (BuildContext context, AsyncSnapshot<List<Event>> snapshot) {
List<Event> events = snapshot.data ?? [];
return MyAwesomeCalendarWidget(events: events);
}
);
This way, the Widget will build first with the empty List (no events fetched yet), showing you an empty calendar, and once the data is fetched it'll rebuild accordingly. You would no longer need to use a StatefulWidget in this case.
eventLoader: (day) {
for (var i = 0; i < listtasks.length; i++) {
if (day == listtasks[i].date) {
return [listtasks[i]];
}
}
return [];
},

How to display a Firebase list in REAL TIME using BLoC Pattern?

I have a TODO List function (Alarmas), but I feel I'm not taking advantage of Firebase's Realtime features enough.
The Widget displays the list very well, however when someone puts a new task from another cell phone, I am not being able to show it automatically, but I must call the build again by clicking on the "TODO button" in the BottomNavigationBar.
Is there a way that the new tasks are automatically displayed without doing anything?
I'm using BLOC Pattern and Provider to get Data through Streams...
#override
Widget build(BuildContext context) {
alarmaBloc.cargarAlarmas();
///---Scaffold and others
return StreamBuilder(
stream: alarmaBloc.alarmasStream,
builder: (BuildContext context, AsyncSnapshot<List<AlarmaModel>> snapshot){
if (snapshot.hasData) {
final tareasList = snapshot.data;
if (tareasList.length == 0) return _imagenInicial(context);
return ListView(
children: [
for (var itemPendiente in tareasList)
_crearItem(context, alarmaBloc, itemPendiente),
//more widgets
],
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center (child: Image(image: AssetImage('Preloader.gif'), height: 200.0,));
},
),
#puf published a solution in How to display a Firebase list in REAL TIME? using setState, but I don't know how to implement it because I can't use setState inside my BLoC pattern page.
UPDATE
My BLoC Pattern looks like this...
class AlarmaBloc {
final _alarmaController = new BehaviorSubject<List<AlarmaModel>>();
final _alarmaProvider = new AlarmaProvider();
Stream <List<AlarmaModel>> get alarmasStream => _alarmaController.stream;
Future<List<AlarmaModel>> cargarAlarmas() async {
final alarmas = await _alarmaProvider.cargarAlarmas();
_alarmaController.sink.add(alarmas);
return alarmas;
}
//---
dispose() {
_alarmaController?.close();
}
And my PROVIDER looks like this...
Future<List<AlarmaModel>> cargarAlarmas() async {
final List<AlarmaModel> alarmaList = new List();
Query resp = db.child('alarmas');
resp.onChildAdded.forEach((element) {
print('Provider - Nuevo onChild Alarma ${element.snapshot.value['fecha']} - ${element.snapshot.value['nombreRefEstanque']} - ${element.snapshot.value['pesoPromedio']}}');
final temp = AlarmaModel.fromJson(Map<String,dynamic>.from(element.snapshot.value));
temp.idAlarma = element.snapshot.key;
alarmaList.add(temp); // element.snapshot.value.
});
await resp.once().then((snapshot) {
print("Las Alarmas se cargaron totalmente - ${alarmaList.length}");
});
return alarmaList;
How can I display a List from Firebase in "true" Real Time using BLoC Pattern?

Fixing Issues with FutureBuilder

In my Flutter project, I am trying to implement a button click event by using FutureBuilder. Basically when the button clicked, it supposed to get the data and display in a table. So my button onPressed event handling is as below:
onPressed: () async{
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
var p = double.parse(loanAmount);
var r = double.parse(interestRate);
var n = int.parse(monthes);
Api api = new Api();
new FutureBuilder<List>(
future: api.calculateEmi(p, r, n),
builder: (BuildContext buildContext, AsyncSnapshot<List> snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
print( snapshot.data);
return new SingleChildScrollView(
padding: const EdgeInsets.all(8.0),
child: DataTableWidget(listOfColumns: snapshot.data.map(
(e)=>{
'Month': e['Month'],
'Principal': e['Principal'],
'InterestP': e['InterestP'],
'PrinciplaP': e['PrinciplaP'],
'RemainP': e['RemainP']
}).toList()
),
);
}
}
);
}
}
The Api call is working and the method calculateEmi is called and get data returned ( a List of Map), but the view just not updated and no table appeared at all, and I use breakpoint at the builder portion but it never go into it, where did I do wrong, can anyone help? thanks.
The FutureBuilder needs to be inserted somewhere in the flutter widget tree. Simply creating a new FutureBuilder doesn't tell flutter what to do with it.
I'm guessing you instead want to put the FutureBuilder you created somewhere in the parent widget of the onPressed method. If you need it to only show when the button is pressed you can do that with a bool that determines whether to show the FutureBuilder or not.
Ex.
Widget build(context) {
if(buttonPressed) {
return FutureBuilder(
...
);
}
else {
return Container();
}
}

How to inform FutureBuilder that database was updated?

I have a group profile page, where a user can change the description of a group. He clicks on the description, gets on a new screen and saves it to Firestore. He then get's back via Navigator.pop(context) to the group profile page which lists all elements via FutureBuilder.
First, I had the database request for my FutureBuilder inside the main build method (directly inside future builder 'future: request') which was working but I learnt it is wrong. But now I have to wait for a rebuild to see changes. How do I tell FutureBuilder that there is a data update?
I am loading Firestore data as follows within the group profile page:
Future<DocumentSnapshot> _future;
#override
void initState() {
super.initState();
_getFiretoreData();
}
Future<void> _getFiretoreData() async{
setState(() {
this._future = Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();});
}
The FutureBuilder is inside the main build method and gets the 'already loaded' future like this:
FutureBuilder(future: _future, ...)
Now I would like to tell him: a change happened to _future, please rebuild ;-).
Ok, I managed it like this (which took me only a few lines of code). Leave the code as it is and get a true callback from the navigator to know that there was a change on the second page:
// check if second page callback is true
bool _changed = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProfileUpdate(userId: globals.userId.toString())),
);
// if it's true, reload future data
_changed ? _getFiretoreData() : Container();
On the second page give the save button a Navigator.pop(context, true).
i would advice you not to use future builder in this situation and use future.then() in an async function and after you get your data update the build without using future builder..!
Future getData() async {
//here you can call the function and handle the output(return value) as result
getFiretoreData().then((result) {
// print(result);
setState(() {
//handle your result here.
//update build here.
});
});
}
How about this?
#override
Widget build(BuildContext context) {
if (_future == null) {
// show loading indicator while waiting for data
return Center(child: CircularProgressIndicator());
} else {
return YourWidget();
}
}
You do not need to set any state. You just need to return your collection of users in your GetFirestoreData method.
Future<TypeYouReturning> _getFirestoreData() async{
return Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();
}
Inside your FutureBuilder widget you can set it up something like Theo recommended, I would do something like this
return FutureBuilder(
future: _getFirestoreData(),
builder: (context, AsyncSnapshot<TypeYouReturning> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
if (snapshot.data.length == 0)
return Text("No available data just yet");
return Container();//This should be the desire widget you want the user to see
}
},
);
Why don't you use Stream builder instead of Future builder?
StreamBuilder(stream: _future, ...)
You can change the variable name to _stream for clarity.