I've come across a problem while trying out Flutter that I can't figure out. The case I'm thinking of has a FutureBuilder widget like below:
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Example Page"),
),
body: new FutureBuilder(
future: _exampleFuture,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting: return new Center(child: new CircularProgressIndicator(),);
default:
if(snapshot.hasError) {
return new Center(child: new Text('Error: ${snapshot.error}'),);
}
else {
return new Center(child: new Text("Result: ${snapshot.data}"),);
}
}
}
)
);
}
Now let's assume the future is an http call that ends up with a 401 error, indicating that the user is unauthorized. At this point, I'd like the app to erase any token that's stored and redirect to the login page or just rebuild the app. But I can't call a method that does that in the build function, and I don't think didUpdateWidget() is guaranteed to be called, as the future might return it's value before build is called? Maybe I'm approaching this completely wrong, but is there a way to do this in Flutter?
You can check for a statusCode inside your Async method, and use setState to erase the value of the token based on the statusCode value; otherwise, if the connection is authorized, return your desired data. Now, in your FutureBuilder , check if the you snapshot is null to show a SignIn() page instead.
For example, your method that handles the http requests might look something like:
_Request() async {
var httpClinet = createHttpClient();
var response = await httpClinet.get(
url, headers: {'Authorization': "Bearer $_currentUserToken"});
if (response.statusCode == 200) {
var myRequest = JSON.decode(response.body);
var myDesiredData;
///TODO: Some data conversions and data extraction
return myDesiredData;
}
else {
setState(() {
_currentUserToken = null;
});
return null;
}
}
Then you can have a FutureBuilder like this:
#override
Widget build(BuildContext context) {
return new FutureBuilder(
future: _request(),
builder: (BuildContext context, AsyncSnapshot response) {
response.hasData==false? new SignIn(): new Scaffold(
appBar: new AppBar(title: new Text("Future Builder"),),
body: new Center(
child: new Text("Build your widgets"),
),
);
},
);
}
Related
I am very beginner to Flutter and Dart. So I am trying to update the state of the parent widget, but to be honest after trying many different solutions none worked for me, or am I doing something wrong?
What I'm trying to do is to update the _title in _BooksState() when the page changes in _Books() class.
How do I set the _title state from the child (_Books()) widget?
class Books extends StatefulWidget {
#override
_BooksState createState() {
return _BooksState();
}
}
class _BooksState extends State<Books> {
String _title = 'Books';
_setTitle(String newTitle) {
setState(() {
_title = newTitle;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
),
body: _Books(),
);
}
}
class _Books extends StatelessWidget {
final PageController _controller = PageController();
final Stream<QuerySnapshot> _stream =
Firestore.instance.collection('Books').orderBy('title').snapshots();
_setAppBarTitle(String newTitle) {
print(newTitle);
// how do I set _title from here?
}
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _stream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
final books = snapshot.data.documents;
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
return PageView.builder(
controller: _controller,
scrollDirection: Axis.horizontal,
itemCount: books.length,
itemBuilder: (context, index) {
final book = books[index];
return ListTile(
title: Text(book['title']),
subtitle: Text(book['author']),
);
},
onPageChanged: (index) {
_setAppBarTitle(books[index].data['title']);
},
);
}
},
);
}
}
let me repeat your question in other words: You want to setstate a widget(or refresh a page, or change a variable 'binded' to a widget) when something happens(not inside the same class of the widget).
This is a common problem for all newbies in flutter(including me), which is called state management.
Of course you can always put everything inside the same dart file, or even the same class, But we don't do that for larger app.
In order to solve this problem, I created 2 examples:
https://github.com/lhcdims/statemanagement01
This example uses a timer to check whether something inside a widget is changed, if so, setstate the page that the widget belongs to.
try to take a look at the function funTimerDefault() inside main.dart
Ok, this was my first try, not a good solution.
https://github.com/lhcdims/statemanagement02
This example's output is the same as 1, But is using Redux instead of setState. Sooner or later you'll find that setstate is not suitable for all cases(like yours!), you'll be using Redux or BLoC.
Read the readme inside the examples, build and run them, you'll then be able to (refresh) any widget(or changes variables binded to a widget), at any time(and anywhere) you want. (even the app is pushed into background, you can also try this in the examples)
What you can do is move you _Books class inside the _BooksState class..
And instead of using _Books as class you can use it as Widget inside _BooksState class so that you can access the setState method of StatefulWidget inside the Widget you create.
I do it this way and even I'm new to Flutter and Dart...This is working for me in every case even after making an API call..I'm able to use setState and set the response from API.
Example:
class Books extends StatefulWidget {
#override
_BooksState createState() {
return _BooksState();
}
}
class _BooksState extends State<Books> {
String _title = 'Books';
_setTitle(String newTitle) {
setState(() {
_title = newTitle;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
),
body: _books(), // Using the Widget here
);
}
// Your `_Books` class created as `Widget` for setting state and new title.
Widget _books() {
final PageController _controller = PageController();
final Stream<QuerySnapshot> _stream =
Firestore.instance.collection('Books').orderBy('title').snapshots();
_setAppBarTitle(String newTitle) {
print(newTitle);
// how do I set _title from here?
// Since you created this method and setting the _title in this method
// itself using setstate you can directly pass the new title in this method..
_setTitle(newTitle);
}
return StreamBuilder<QuerySnapshot>(
stream: _stream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
final books = snapshot.data.documents;
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
return PageView.builder(
controller: _controller,
scrollDirection: Axis.horizontal,
itemCount: books.length,
itemBuilder: (context, index) {
final book = books[index];
return ListTile(
title: Text(book['title']),
subtitle: Text(book['author']),
);
},
onPageChanged: (index) {
_setAppBarTitle(books[index].data['title']);
},
);
}
},
);
}
}
I am currently trying to retrieve data (tags) from a REST API and use the data to populate a dropdown menu which I can successfully do but upon selection of the item, I get the following error which according to this would mean that the "selected value is not member of the values list":
items == null || value == null || items.where((DropdownMenuItem item) => item.value == value).length == 1': is not true.
This occurs after the dropdown menu shows my selected item. However, this is error should not be occurring as I've done the necessary logging to check that the data is indeed assigned to the list in question. Could anyone help me resolve this issue? I have isolated it to down to it originating in the setState() method in onChanged of DropdownButton but can't seem to understand why that should be causing an issue. Any help would be deeply appreciated!
My code is as follows:
class _TodosByTagsHomePageState extends State<TodosByTagsHomePage> {
Tag selectedTag;
final Logger log = new Logger('TodosByTags');
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: ListView(
children: <Widget>[
FutureBuilder<List<Tag>> (
future: fetchTags(),
builder: (context, snapshot) {
if (snapshot.hasData) {
log.info("Tags are present");
_tagsList = snapshot.data;
return DropdownButton<Tag>(
value: selectedTag,
items: _tagsList.map((value) {
return new DropdownMenuItem<Tag>(
value: value,
child: Text(value.tagName),
);
}).toList(),
hint: Text("Select tag"),
onChanged: (Tag chosenTag) {
setState(() {
log.info("In set state");
selectedTag = chosenTag;
Scaffold.of(context).showSnackBar(new SnackBar(content: Text(selectedTag.tagName)));
});
},
) ;
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return Container(width: 0.0, height: 0.0);
}),
])
);
}
// Async method to retrieve data from REST API
Future<List<Tag>> fetchTags() async {
final response =
await http.get(REST_API_URL);
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
var result = compute(parseData, response.body);
return result;
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
static List<Tag> parseData(String response) {
final parsed = json.decode(response);
return (parsed["data"] as List).map<Tag>((json) =>
new Tag.fromJson(json)).toList();
}
List<Tag> _tagsList = new List<Tag>();
}
// Model for Tag
class Tag {
final String tagName;
final String id;
final int v;
Tag({this.id, this.tagName, this.v});
factory Tag.fromJson(Map<String, dynamic> json) {
return new Tag(
id: json['_id'],
tagName: json['tagName'],
v: json['__v'],
);
}
}
update your code like this
I think issues that when calling setState in FutureBuilder that call fetchTags() move fetchTags() to initState() for once call
class _TodosByTagsHomePageState extends State<TodosByTagsHomePage> {
Tag selectedTag;
Future<List<Tag>> _tags;
#override
void initState() {
_tags = fetchTags();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: ListView(children: <Widget>[
FutureBuilder<List<Tag>>(
future: _tags,
builder: (context, snapshot) {
if (snapshot.hasData) {
return DropdownButton<Tag>(
value: selectedTag,
items: snapshot.data.map((value) {
print(value);
return DropdownMenuItem<Tag>(
value: value,
child: Text(value.tagName),
);
}).toList(),
hint: Text("Select tag"),
onChanged: (Tag chosenTag) {
setState(() {
selectedTag = chosenTag;
});
},
);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return Container(width: 0.0, height: 0.0);
}),
]));
}
I currently have a simple API but my flutter widgets could not detect the changes happening in the API without restarting the whole application.
The API was made as a test API on https://www.mockable.io/. I managed to read from this API using a StreamBuilder. And still it does not change the values in the widget whenever i update their value in the API.
Default JSON
{
"id": "123",
"token": "1token",
"status": "Valid"
}
After Value Change
{
"id": "123",
"token": "1token",
"status": "Not Valid"
}
My StreamBuilder Code
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
final String url = "http://demo2722084.mockable.io/user";
class StreamPage extends StatefulWidget {
#override
StreamPageState createState() {
return new StreamPageState();
}
}
class StreamPageState extends State<StreamPage> {
StreamController _userController;
Future fetchUser() async{
final response = await http.get(url);
if(response.statusCode == 200){
return json.decode(response.body);
}else{
print("Exception caught: Failed to get data");
}
}
loadUser() async{
fetchUser().then((res) async{
_userController.add(res);
return res;
});
}
#override
void initState() {
_userController = new StreamController();
loadUser();
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Stream"),
),
body: new StreamBuilder(
stream: _userController.stream,
builder: (context, snapshot){
if(snapshot.hasError){
print("Exception: ${snapshot.error}");
}
if(snapshot.hasData){
var user = snapshot.data;
var id = user["id"] ?? "";
var token = user["token"] ?? "";
var status = user["status"] ?? "";
return new Center(
child: new ListTile(
title: new Text("Token: $token"),
subtitle: new Text("Status: $status"),
leading: new Text(id),
),
);
}
if(snapshot.connectionState != ConnectionState.waiting){
return new Center(
child: new CircularProgressIndicator(),
);
}
if(!snapshot.hasData && snapshot.connectionState != ConnectionState.done){
return new Center(
child: new CircularProgressIndicator(),
);
}
},
),
);
}
}
I expected the StreamBuilder to pick up the changed status value but the app has to be restarted for the new value to be reflected in the app.
Is there an example of how I can achieve this properly since this is quite frankly my first time trying out a StreamBuilder.
Your HTTP request results in just one data point, so for that you could use a FutureBuilder. But you want to know when something changes at the server. Your HTTP connection has finished when the change is made and so has no idea that it has happened.
There are several ways to detect changes at the server, for example, polling, user-initiated refresh, websockets, etc. For demonstration purposes, instead of calling loadUser in initState you could start a 1 second periodic timer to do that, which would update the stream every second and you should see the change. (Don't forget to cancel the timer in dispose.)
That's unlikely to be viable in production, so explore websockets, etc for pushed notifications from the web server. Also explore whether Firebase Cloud Messaging is a better way to send notification of changes to your application.
#override
void initState() {
super.initState();
_userController = StreamController();
Timer.periodic(Duration(seconds: 1), (_) => loadUser());
}
you probably also need to do this only while the app is in foreground. You can use my LifecycleAwareStreamBuilder, there is an example on how to use it here
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...');
},
),
);
}
}
Let's say I have something like this:
return FutureBuilder(
future: _loadingDeals,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return RefreshIndicator(
onRefresh: _handleRefresh,
...
)
}
)
In the _handleRefresh method, I want to programmatically trigger the re-run of the FutureBuilder.
Is there such a thing?
The use case:
When a user pulls down the refreshIndicator, then the _handleRefresh simply makes the FutureBuilder rerun itself.
Edit:
Full code snippet end to end, without the refreshing part. I've switched to using the StreamBuilder, how will the refreshIndicator part fit in all of it?
class DealList extends StatefulWidget {
#override
State<StatefulWidget> createState() => new _DealList();
}
class _DealList extends State<DealList> with AutomaticKeepAliveClientMixin {
// prevents refreshing of tab when switch to
// Why? https://stackoverflow.com/q/51224420/1757321
bool get wantKeepAlive => true;
final RestDatasource api = new RestDatasource();
String token;
StreamController _dealsController;
#override
void initState() {
super.initState();
_dealsController = new StreamController();
_loadingDeals();
}
_loadingDeals() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
this.token = prefs.getString('token');
final res =
this.api.checkInterests(this.token).then((interestResponse) async {
_dealsController.add(interestResponse);
return interestResponse;
});
return res;
}
_handleRefresh(data) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final token = prefs.getString('token');
await this.api.checkInterests(token).then((interestResponse) {
_dealsController.add(interestResponse);
});
return null;
}
#override
Widget build(BuildContext context) {
super.build(context); // <-- this is with the wantKeepAlive thing
return StreamBuilder(
stream: _dealsController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
...
}
if (snapshot.connectionState != ConnectionState.done) {
return Center(
child: CircularProgressIndicator(),
);
}
if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return Text('No deals');
}
if (snapshot.hasData) {
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: snapshot.data['deals'].length,
itemBuilder: (context, index) {
final Map deal = snapshot.data['deals'][index];
return ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DealsDetailPage(
dealDetail: deal,
),
),
);
},
title: Text(deal['name']),
subtitle: Text(deal['expires']),
);
},
),
}
},
);
}
}
Why not using a StreamBuilder and a Stream instead of a FutureBuilder?
Something like that...
class _YourWidgetState extends State<YourWidget> {
StreamController<String> _refreshController;
...
initState() {
super...
_refreshController = new StreamController<String>();
_loadingDeals();
}
_loadingDeals() {
_refreshController.add("");
}
_handleRefresh(data) {
if (x) _refreshController.add("");
}
...
build(context) {
...
return StreamBuilder(
stream: _refreshController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return RefreshIndicator(
onRefresh: _handleRefresh(snapshot.data),
...
)
}
);
}
}
I created a Gist with the Flutter main example using the StreamBuilder, check it out
Using StreamBuilder is a solution, however, to trigger the FutureBuilder programmatically, just call setState, it'll rebuild the Widget.
return RefreshIndicator(
onRefresh: () {
setState(() {});
},
...
)
I prefer FutureBuilder over StreamBuilder since I am using Firestore for my project and you get billed by reads so my solution was this
_future??= getMyFuture();
shouldReload(){
setState(()=>_future = null)
}
FutureBuilder(
future: _future,
builder: (context, snapshot){
return Container();
},
)
and any user activity that needs you to get new data simply call shouldReload()