Flutter: update stateful widget from child StreamBuilder - flutter

I have written some simplified code where I want the condition of firestore data to be able to update a parent widget.
I get the error: setState() or markNeedsBuild() called during build.
How can I update the state of myVar from inside StreamBuilder? After updating myVar the StreamBuilder will not be accessed, instead only the text Waiting... should be displayed.
Edit: I updated myVar to be used as the document ID of Firestore. To clarify, I want to be able to update the document of the StreamBuilder from inside the StreamBuilder. In other words as the question says, update stateful widget from child StreamBuilder.
class MyWidget extends StatefulWidget {
MyWidget({Key key})
: super(key: key);
#override
_MyWidgetState createState() {
return _MyWidgetState();
}
}
class _MyWidgetState extends State<MyWidget> {
String myVar = "someID";
#override
Widget build(BuildContext context) {
if (myVar == null) {
// update myVar async, so that StreamBuilder will be re-rendered
asyncFunctUpdate(myVar); // edit: finally got it to work if doing setState from asyncFuncUpdate(myvar).then(()=>setState)
return Text("Waiting...");
}
else
return StreamBuilder<DocumentSnapshot>(
stream: Firestore.instance
.collection('myCollection')
.document(myVar)
.snapshots(),
builder: (context, orderSnapshot) {
if (!orderSnapshot.data.exists)
setState(() {
myVar = null;
});
else return Text(orderSnapshot);
});
}
}
Workaround:
In my case it was simpler to just return buttons from the StreamBuilder and setState as normally from onPressed functions.
Solution:
Instead of doing setState from an async function do it within the .then clause (see code above)

In general, you do not have to do what you are doing now. You can do return Text ("Waiting ..."); right away without calling setState. This is a better decision in your case. You respond to events coming from the stream and must return content according to the current state
return StreamBuilder<DocumentSnapshot>(
stream: Firestore.instance
.collection('myCollection')
.document('myDocument')
.snapshots(),
builder: (context, orderSnapshot) {
if (!orderSnapshot.data.exists)
return Text("Waiting...");
else return Text(orderSnapshot);
});

You can make the update async like putting it inside an anonymous async callback like
() async {
setState();
}();
OR
You can avoid having state in that particular use case like:
#override
Widget build(BuildContext context) {
return StreamBuilder<DocumentSnapshot>(
stream: Firestore.instance
.collection('myCollection')
.document('myDocument')
.snapshots(),
builder: (context, orderSnapshot) {
if (!orderSnapshot.data.exists) {
return Text("Waiting...");
} else {
return Text(orderSnapshot);
}
});
}

Related

Using a FutureBuilder in a Flutter stateful widget with RefreshIndicator

I have a Flutter widget which gets data from a server and renders a List. After getting the data, I parse the data and convert it to an internal object in my application, so the function is something like this:
Future<List<Data>> getData(Thing thing) async {
var response = await http.get(Uri.parse(MY_URL));
// do some processing
return data;
}
After that, I've defined a stateful widget which calls this function and takes the future to render a List.
class DataList extends StatefulWidget {
const DataList({Key key}) : super(key: key);
#override
_DataListState createState() => _DataListState();
}
class _DataListState extends State<DataList> {
Widget createListView(BuildContext context, AsyncSnapshot snapshot) {
List<Data> values = snapshot.data;
if (values.isEmpty) {
return NoResultsWidget('No results.');
}
return ListView.builder(
itemCount: values.length,
itemBuilder: (BuildContext context, int index) {
return values[index];
},
);
}
#override
Widget build(BuildContext context) {
var data = getSomething().then((thing) => getData(thing));
return FutureBuilder(
future: data,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return CustomErrorWidget('Error');
case ConnectionState.waiting:
return LoadingWidget();
default:
if (snapshot.hasError) {
return CustomErrorWidget('Error.');
} else {
return createListView(context, snapshot);
}
}
},
);
}
}
Now, the code works just fine in this manner. But, when I try to move my data to be a class variable (of type Future<List>) that I update through the initState method, the variable just never updates. Example code below:
class _DataListState extends State<DataList> {
Future<List<Data>> data;
....
#override
void initState() {
super.initState();
updateData();
}
void updateData() {
data = getSomething().then((thing) => getData(thing));
}
....
}
I want to add a refresh indicator to update the data on refresh, and to do that I need to make my data a class variable to update it on refresh, but I can't seem to figure out how to make my data part of the state of the stateful widget and have it work. any help or guides to a github code example would be appreciated.
You need to wrap the assignment of the data variable in setState so that Flutter knows the variable changed and rebuilds your widget.
For example:
void updateData() {
setState(() {
data = getSomething().then((thing) => getData(thing));
});
}

How to access Shared Preferences String in build method flutter

I'm trying to access userEmail in shared preferences, inside my build method. Here's some of the code for context:
Widget build(BuildContext context) {
final prefs = await SharedPreferences.getInstance();
final userEmail = prefs.getString('userEmail') ?? '';
...
Return Scaffold(
body: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: StreamBuilder<QuerySnapshot>(
stream: _firestore.collection(userEmail).orderBy('time', descending: false).snapshots(),
...
The issue I have is, an error comes up highlighting the await. When i hover over it with my cursor for more info, it say The await expression can only be used in an async function. Try marking the function body with either 'async' or 'async*'.
There is then an option to add 'async' modifier. So i clicked that, which transformed code into this:
Future<Widget> build(BuildContext context) async {
...
This causes another error message: '_HomeScreenState.build' ('Future<Widget> Function(BuildContext)') isn't a valid override of 'State.build' ('Widget Function(BuildContext)').
Any ideas how to solve this issue? I've tried saving the userEmail using the Provider package. This works perfectly when the user first signs in or registers, but if you hot reload, the stream doesn't work.
You can use WidgetsBinding.instance.addPostFrameCallback, this helps you to run a callback during a frame, just after the persistent frame callbacks (which is when the main rendering pipeline has been flushed). If a frame is in progress and post-frame callbacks haven't been executed yet, then the registered callback is still executed during the frame. Otherwise, the registered callback is executed during the next frame.
In code, you can use it something like this.
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final prefs = await SharedPreferences.getInstance();
final userEmail = prefs.getString('userEmail') ?? '';
});
Hope this answers your question.
Long Story short you should not perform any side effects inside your build method . See here
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final prefs ;
#override
void initState() async{
super.initState();
prefs = await SharedPreferences.getInstance();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: prefs,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if(snapshot.hasData){}else if (snapshot.hasError)
return Center(child: CircularProgressIndicator());
},);
}
}
As your build method can be called multiple times you should not perform network calls or call complex methods because as the docs say. This method can and will be called multiple times.
In your case I used a FutureBuilder to handle the future's state and awaited it in the initState insida a stateful widget.
Check this article for more info
As #croxx5f and #AhmetKAYGISIZ suggested, I ended up using FutureBuilder to solve this problem. Thank you both so much for your help with this.
Here's the final code for anyone else who is stuck on this problem:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
var prefs;
#override
void initState() {
super.initState();
getUserEmailFromSharedPrefs();
}
Future<String> getUserEmailFromSharedPrefs() async {
prefs = await SharedPreferences.getInstance();
final userEmail = prefs.getString('userEmail') ?? '';
return userEmail;
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getUserEmailFromSharedPrefs(),
builder: (context, AsyncSnapshot<String> snapshot) {
if(snapshot.hasData) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: StreamBuilder<QuerySnapshot>(
stream: _firestore.collection(snapshot.data).orderBy('time', descending: false).snapshots(),
...
So in summary, I wrapped my streambuilder in a futurebuilder.

Flutter: How to fetch data from api only once while using FutureBuilder?

How can I fetch data only once while using FutureBuilder to show a loading indicator while fetching?
The problem is that every time the user opens the screen it will re-fetch the data even if I set the future in initState().
I want to fetch the data only the first time the user opens the screen then I will use the saved fetched data.
should I just use a stateful widget with a loading variable and set it in setState()?
I'm using Provider package
Future<void> fetchData() async {
try {
final response =
await http.get(url, headers: {'Authorization': 'Bearer $_token'});......
and my screen widget:
class _MyScreenState extends State<MyScreen> {
Future<void> fetchData;
#override
void initState() {
fetchData =
Provider.of<Data>(context, listen: false).fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: fetchData,
builder: (ctx, snapshot) =>
snapshot.connectionState == ConnectionState.done
? Consumer<Data>(
builder: (context, data, child) => Text(data.fetchedData)): Center(
child: CircularProgressIndicator(),
),
);
}
}
If you want to fetch the data only once even if the widget rebuilds, you would have to make a model for that. Here is how you can make one:
class MyModel{
String value;
Future<String> fetchData() async {
if(value==null){
try {
final response =
await http.get(url, headers: {'Authorization': 'Bearer $_token'});......
value=(YourReturnedString)
}
}
return value;
}
}
Don't forget to place MyModel as a Provider. In your FutureBuilder:
#override
Widget build(context) {
final myModel=Provider.of<MyModel>(context)
return FutureBuilder<String>(
future: myModel.fetchData(),
builder: (context, snapshot) {
// ...
}
);
}
A simple approach is by introducing a StatefulWidget where we stash our Future in a variable. Now every rebuild will make reference to the same Future instance:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Future<String> _future;
#override
void initState() {
_future = callAsyncFetch();
super.initState();
}
#override
Widget build(context) {
return FutureBuilder<String>(
future: _future,
builder: (context, snapshot) {
// ...
}
);
}
}
Or you can simply use a FutureProvider instead of the StatefulWidget above:
class MyWidget extends StatelessWidget {
// Future<String> callAsyncFetch() => Future.delayed(Duration(seconds: 2), () => "hi");
#override
Widget build(BuildContext context) {
// print('building widget');
return FutureProvider<String>(
create: (_) {
// print('calling future');
return callAsyncFetch();
},
child: Consumer<String>(
builder: (_, value, __) => Text(value ?? 'Loading...'),
),
);
}
}
You can implement provider and pass data among its child.
Refer this example for fetching the data once and using it throughout its child.
As Aashutosh Poudel suggested, you could use an external object to maintain your state,
FOR OTHERS COMING HERE!
To manage state for large applications, the stateful widgets management becomes a bit painful. Hence you have to use an external state object that is shall be your single source of truth.
State management in flutter is done by the following libraries | services:
i. Provider: Well, i have personally played with this a little bit, even did something with it. I could suggest this for beginners.
ii. GetX: That one library that can do everything, its a good one and is recommended for novice || noob.
iii. Redux: For anyone coming from the react and angular world to flutter, this is a very handy library. I personally love this library, plus when you give it additional plugins, you are just superman
iv. Bloc: Best for data that is in streams. in other words, best for reactive programming approach....
Anyways, that was a lot given your question. Hope i helped

Flutter: Stateful Widget does not update

Imagine two Widgets: Main that manages a tabbar and therefore holds several Widgets - and Dashboard.
On Main Constructor I create a first Instance of Dashboard and the other tabbar Widgets with some dummy data (they are getting fetched in the meanwhile in initState). I build these with Futurebuilder. Once the data arrived I want to create a new Instance of Dashboard, but it won't change.
class _MainState extends State<HomePage> {
var _tabs = <Widget>[];
Future<dynamic> futureData;
_MainState() {
_tabs.add(Dashboard(null));
}
#override
void initState() {
super.initState();
futureData = _getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data != null) {
tabs[0] = Dashboard(snapshot.data);
} else {
return CircularProgressIndicator();
}
});
}
}
class DashboardScreen extends StatefulWidget {
final data;
DashboardScreen(this.data,
{Key key})
: super(key: key) {
print('Dashboard Constructor: ' + data.toString());
}
#override
_DashboardScreenState createState() => _DashboardScreenState(data);
}
class _DashboardScreenState extends State<DashboardScreen> {
var data;
_DashboardScreenState(this.data);
#override
void initState() {
super.initState();
print('InitState: ' + data.toString());
}
#override
void didUpdateWidget(Widget oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget');
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies' + data.toString());
}
#override
Widget build(BuildContext context) {
return Text(data.toString());
}
}
When I print on several available methods it comes clear that the DasboardScreenState is not recreated. Only the DashboardScreen Constructor is called again when the data arrived, but not it's state...
flutter: MainConstructor: null
flutter: Dashboard Constructor: null
flutter: InitState: null
flutter: didChangeDependencies: null
flutter: Dashboard Constructor: MachineStatus.Manual <- Here the data arrived in futureBuilder
How can I force the State to recreate? I tried to use the key parameter with UniqueKey(), but that didn't worked. Also inherrited widget seems not to be the solution either, despite the fact that i don't know how to use it in my use case, because the child is only available in the ..ScreenState but not the updated data..
I could imagine to inform dashboardScreenState by using Stream: listen to messages and then call setState() - I think, but that's only a workaround.
Can anyone help me please :)?
I know I have had issues with the if statement before, try:
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) { //use hasData
DataType data = snapshot.data; //Declare Values first
tabs[0] = Dashboard(data);
} else {
return CircularProgressIndicator();
}
});

Make a Stream in StreamBuilder only run once

I have which cycles a heavy function X times. If I put this stream in a StreamBuilder, the Stream runs again and again forever, but I only need it to run once (do the X cycles) and the stop.
To solve this problem for future functions I used an AsyncMemoizer, but I cannot use it for stream functions.
How can I do it?
If you are sure, your widget should not be rebuilt, than try sth like this code below.
The _widget will be created once in initState, then the 'cached' widget will be returned in the build method.
class MyStreamWidget extends StatefulWidget {
#override
_MyStreamWidgetState createState() => _MyStreamWidgetState();
}
class _MyStreamWidgetState extends State<MyStreamWidget> {
StreamBuilder _widget;
// TODO your stream
var myStream;
#override
void initState() {
super.initState();
_widget = StreamBuilder(
stream: myStream,
builder: (context, snapshot) {
// TODO create widget
return Container();
})
}
#override
Widget build(BuildContext context) {
return _widget;
}
}
As RĂ©mi Rousselet suggested, the StreamBuilder should be used in a Widget Tree where the state is well managed. I was calling setState((){}) in the Stream which caused the UI to update every time, making the StreamBuilder rebuild so restarting the stream.