Flutter Future and setstate - dart

I am trying to add a Image widget when I get the results of an API call. My code is:
class AnimalDetailsPage extends StatefulWidget {
final selection;
_AnimalDetailsPage createState() => new _AnimalDetailsPage();
AnimalDetailsPage({Key key, this.selection}) : super(key: key);
}
class _AnimalDetailsPage extends State<AnimalDetailsPage> {
Future<List> getphotos(horseId) async {
http.Response response = await http.get(
Uri.encodeFull(
"http://myhorses.com/api/getHorsePhotos?horse_id=" +
horseId));
return JSON.decode(response.body);
}
#override
Widget build(BuildContext context) {
final menu = new MyMenuBar();
List<Widget> bodyContent = [menu];
dynamic body = new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: bodyContent,
);
if (widget.selection != null) {
final horse = widget.selection;
getphotos(horse['id'].toString()).then((res) {
setState(() {
bodyContent.add(new Image.network(res[0]['image']));
});
});
}
return Scaffold(
body: body,
);
}
}
What I can't manage to understand is that the setState does not updates the view. If I move the setState out of the then statement and hard code the image src, then it works fine.

bodyContent is declared inside build()
#override
Widget build(BuildContext context) {
final menu = new MyMenuBar();
List<Widget> bodyContent = [menu];
...
bodyContent.add(new Image.network(res[0]['image']));
...
and setState() causes build to be executed again, which means the bodyContent that holds the image is discarded an a new one created.
Move List<Widget> bodyContent = [menu]; out of the build() method and make it a class-level field and you should get the desired result.

Related

How to fix FutureBuilder open multiple times error?

these my two classes(two pages). these two classes open multiple times.
I put debug point in futurebuilder in two classes.
debug point running,
MainCategory page and got to the next page
SubCategory page and again running MainCategory page(previous page) futurebuilder and again running MainCategory page futurebuilder
navigate subcategory page to third page running subcategory page and main category page
I upload my two classes to GitHub and please let me know what the issue is.
MainCategory code: https://github.com/bhanuka96/ios_login/blob/master/MainCategory.dart
SubCategory code: https://github.com/bhanuka96/ios_login/blob/master/subCategory.dart
As stated in the documentation, you should not fetch the Future for the Futurebuilder during the widget's build event.
https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateConfig, or
State.didChangeDependencies. It must not be created during the
State.build or StatelessWidget.build method call when constructing the
FutureBuilder. If the future is created at the same time as the
FutureBuilder, then every time the FutureBuilder's parent is rebuilt,
the asynchronous task will be restarted.
So, try to move your call to getRegister method outside the build method and replace it with the returned Future value.
For example, below I have a class that returns a Future value which will be consumed with the help of FutureBuilder.
class MyApiHelper{
static Future<List<String>> getMyList() async {
// your implementation to make server calls
return List<String>();
}
}
Now, inside your widget, you will have something like this:
class _MyHomePageState extends State<MyHomePage> {
Future<List<String>> _myList;
#override
void initState() {
super.initState();
_myList = MyApiHelper.getMyList();
}
#override
Widget build(BuildContext context) {
return Scaffold(body: FutureBuilder(
future: _myList,
builder: (_, AsyncSnapshot<List<String>> snapLs) {
if(!snapLs.hasData) return CircularProgressIndicator();
return ListView.builder(
itemCount: snapLs.data.length,
itemBuilder: (_, index) {
//show your list item row here...
},
);
},
));
}
}
As shown above, the Future is fetched in the initState function and used inside the build method and used by FutureBuilder.
I hope this was helpful.
Thanks.
If you happen to use Provider, here's (in my opinion) a clearer alternative based on your question:
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureProvider<List<String>>(
create: (_) => MyApiHelper.getMyList(),
child: Consumer<List<String>>(
builder: (_, list, __) {
if (list == null) return CircularProgressIndicator();
return ListView.builder(
itemCount: list.length,
itemBuilder: (_, index) {
//show your list item row here...
},
);
};
),
);
}
}
This can also be achieved of course as a StatefulWidget as suggested by the other answer, or even with flutter_hooks as explained in Why is my Future/Async Called Multiple Times?
You can create new Widget and pass Function to
returnFuture as
() {
return YourFuture;
}
import 'dart:developer';
import 'package:flutter/material.dart';
class MyFutureBuilder<T> extends StatefulWidget {
final Future<T> Function() returnFuture;
final AsyncWidgetBuilder<T> builder;
final T initialData;
MyFutureBuilder({
this.returnFuture,
#required this.builder,
this.initialData,
Key key,
}) : super(key: key);
#override
_MyFutureBuilderState<T> createState() => _MyFutureBuilderState<T>();
}
class _MyFutureBuilderState<T> extends State<MyFutureBuilder<T>> {
bool isLoading = false;
Future<T> future;
#override
void initState() {
super.initState();
future = widget.returnFuture();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
builder: widget.builder,
initialData: widget.initialData,
future: future,
);
}
}
Example
MyFutureBuilder<List<User>>(
returnFuture: () {
return moderatorUserProvider
.getExecutorsAsModeratorByIds(val.users,
save: true);
},
builder: (cont, asyncData) {
if (asyncData.connectionState !=
ConnectionState.done) {
return Center(
child: MyCircularProgressIndicator(
color: ModeratorColor.executors.color,
),
);
}
return Column(
children: asyncData.data
.map(
(singlExecutor) =>
ChooseInfoButton(
title:
'${singlExecutor.firstName} ${singlExecutor.secondName}',
subTitle: 'Business analyst',
middleText: '4.000 NOK',
subMiddleText: 'full time',
label: 'test period',
subLabel: '1.5 month',
imageUrl:
assetsUrl + 'download.jpeg',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) =>
ModeratorExecutorEditPage(),
),
);
},
),
)
.toList());
},
)
```

Reset State of children widget

Here is the summary of the code I'm having a problem with:
Parent widget
class HomePage extends StatefulWidget {
#override
State<HomePage> createState() => HomePageState();
}
class HomePageState extends State<HomePage> {
final GlobalKey<AsyncLoaderState> _asyncLoaderState = GlobalKey<AsyncLoaderState>();
List<DateTime> rounds;
List<PickupModel> pickups;
DateTime currentDate;
Widget build(BuildContext context) {
var _asyncLoader = AsyncLoader(
key: _asyncLoaderState,
initState: () async => await _getData(),
renderLoad: () => Scaffold(body: Center(child: CircularProgressIndicator())),
renderError: ([error]) => Text('Sorry, there was an error loading'),
renderSuccess: ({data}) => _buildScreen(context),
);
return _asyncLoader;
}
Widget _buildScreen(context) {
return Scaffold(
body: PickupList(pickups),
);
}
Future<Null> _selectDate(BuildContext context) async {
final DateTime picked = await showDatePicker(
context: context,
);
if (picked != null && picked != currentDate) {
currentDate = picked;
pickups = await api.fetchPickupList(currentDate);
setState(() {
});
}
}
_getData() async {
rounds = await api.fetchRoundsList();
currentDate = _getNextRound(rounds);
pickups = await api.fetchPickupList(currentDate);
}
}
Children Widget
(Listview builds tiles)
class PickupTile extends StatefulWidget{
final PickupModel pickup;
PickupTile(this.pickup);
#override
State<StatefulWidget> createState() {
return PickupTileState();
}
}
class PickupTileState extends State<PickupTile> {
Duration duration;
Timer timer;
bool _isUnavailable;
bool _isRunning = false;
bool _isDone = false;
#override
void initState() {
duration = widget.pickup.duration;
_isUnavailable = widget.pickup.customerUnavailable;
super.initState();
}
#override
Widget build(BuildContext context) {
return Row(
children: [
// UI widgets
]
}
So I have a parent widget an initial list of pickups which are displayed in the children PickupTile. One can change the date of the pickups displayed using _selectDate. This fetches a new list of Pickups which are stored in the parent State, and the children are rebuilt with their correct attributes. However, the State of the children widget (duration, isRunning, isDone...) is not reset so they stay on screen when changing the date.
If feel like I'm missing something obvious but I can't figure out how to reset the State of the children Widget or create new PickupTiles so that when changing the date I get new separate States.

Update UI after removing items from List

I want to update my ListView if i remove or add items. Right now i just want to delete items and see the deletion of the items immediately.
My application is more complex so i wrote a small example project to show my problems.
The TestItem class holds some data entries:
class TestItem {
static int id = 1;
bool isFinished = false;
String text;
TestItem() {
text = "Item ${id++}";
}
}
The ItemInfoViewWidget is the UI representation of the TestItem and removes the item if it is finished (whenever the Checkbox is changed to true).
class ItemInfoViewWidget extends StatefulWidget {
TestItem item;
List<TestItem> items;
ItemInfoViewWidget(this.items, this.item);
#override
_ItemInfoViewWidgetState createState() =>
_ItemInfoViewWidgetState(this.items, this.item);
}
class _ItemInfoViewWidgetState extends State<ItemInfoViewWidget> {
TestItem item;
List<TestItem> items;
_ItemInfoViewWidgetState(this.items, this.item);
#override
Widget build(BuildContext context) {
return new Card(
child: new Column(
children: <Widget>[
new Text(this.item.text),
new Checkbox(
value: this.item.isFinished, onChanged: isFinishedChanged)
],
),
);
}
void isFinishedChanged(bool value) {
setState(() {
this.item.isFinished = value;
this.items.remove(this.item);
});
}
}
The ItemViewWidget class builds the ListView.
class ItemViewWidget extends StatefulWidget {
List<TestItem> items;
ItemViewWidget(this.items);
#override
_ItemViewWidgetState createState() => _ItemViewWidgetState(this.items);
}
class _ItemViewWidgetState extends State<ItemViewWidget> {
List<TestItem> items;
_ItemViewWidgetState(this.items);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text('Test'),
),
body: ListView.builder(
itemCount: this.items.length,
itemBuilder: (BuildContext context, int index) {
return new ItemInfoViewWidget(this.items, this.items[index]);
}),
);
}
}
The MyApp shows one TestItem and a button that navigates to the ItemViewWidget page.
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
List<TestItem> items = new List<TestItem>();
_MyHomePageState() {
for (int i = 0; i < 20; i++) {
this.items.add(new TestItem());
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Column(
children: <Widget>[
ItemInfoViewWidget(this.items, this.items.first),
FlatButton(
child: new Text('Open Detailed View'),
onPressed: buttonClicked,
)
],
));
}
void buttonClicked() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ItemViewWidget(this.items)),
);
}
}
If i toggle the Checkbox of the first item, the Checkbox is marked as finished (as expected), but it is not removed from the UI - however it is removed from the list.
Then I go back to the Main page and I can observe that Item 1 is checked there as well.
So if I go to the ItemViewWidget page again, I can observe that the checked items are no longer present.
Based on these observations, I come to the conclusion that my implementation works, but my UI is not updating.
How can I change my code to make an immediate update of the UI possible?
Edit: This is not a duplicate, because
I dont want to create a new instance of my list just to get the UI updated.
The answer does not work: I added this.items = List.from(this.items); but the behavior of my app is the same as already described above.
I don't want to break my reference chain by calling List.from, because my architecture has one list that is referenced by several classes. If i break the chain i have to update all references by my own. Is there a problem with my architecture?
I dont want to create a new instance of my list just to get the UI updated.
Flutter uses immutable object. Not following this rule is going against the reactive framework. It is a voluntary requirement to reduce bugs.
Fact is, this immutability is here especially to prevents developers from doing what you currently do: Having a program that depends on sharing the same instance of an object between classes; as multiple classes may want to modify it.
The real problem lies in the fact that it is your list item that removes delete an element from your list.
The thing is since it's your item which does the computing, the parent is never notified that the list changed. Therefore it doesn't know it should rerender. So nothing visually change.
To fix that you should move the deletion logic to the parent. And make sure that the parent correctly calls setState accordingly. This would translate into passing a callback to your list item, which will be called on deletion.
Here's an example:
class MyList extends StatefulWidget {
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
List<String> list = List.generate(100, (i) => i.toString());
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
return MyItem(list[index], onDelete: () => removeItem(index));
},
);
}
void removeItem(int index) {
setState(() {
list = List.from(list)
..removeAt(index);
});
}
}
class MyItem extends StatelessWidget {
final String title;
final VoidCallback onDelete;
MyItem(this.title, {this.onDelete});
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(this.title),
onTap: this.onDelete,
);
}
}

I am trying to create a list view with the data that I got from API

class Search extends StatefulWidget {
int id;
Search([this.id]);
#override
_SearchState createState() => new _SearchState();
}
class _SearchState extends State<Search> {
#override
void initState() {
super.initState();
}
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
widget.id;
return new Scaffold(
appBar: new AppBar(
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.exit_to_app),
onPressed: _getTicketDetails
),
],
centerTitle: true,
title: new Text
("TicketsDetails", style: const TextStyle(
fontFamily: 'Poppins'
,),
),
),
);
}
_getTicketDetails() async {
print(widget.id);
var userDetails = {};
final response = await http.get(
"https....", headers: {
HttpHeaders.AUTHORIZATION: access_token
});
List returnTicketDetails = json.decode(response.body);
print(returnTicketDetails);
for (var i = 0; i < (returnTicketDetails?.length ?? 0); i++) {
final ticketresponse = await http.get(
"https:...
.toString()}", headers: {
HttpHeaders.AUTHORIZATION:
access_token
});
userDetails[returnTicketDetails[i]["user_id"]] =
json.decode(ticketresponse.body);
}
print(userDetails);
}
}
I would like to display in a Listview the index of my userDeatails,
however for some reason the compiler does not recognise the userDetails,
hence it highlight it as an error. I have done this before, but I
don't get why I am encountering this issue now.
At the moment when I run it only display the appBar
As mentioned in the comments, your userDetails variable is scoped inside the _getTicketDetails method. You need to declare it outside of that method if you want it visible to the rest of your class:
var userDetails = {}; // Moved outside
_getTicketDetails() async {
...
}
Though note that you should also call setState when you modify this variable so that Flutter knows that this widget has changed and needs to be rebuild/rendered.

Am not rebuilding objects on build() call

Despite flutter calling build (and printing the correct information as below), it doesn't seem to build new TaskWidgets (the print in TaskWidgetState's constructor is not called). This is creating some unusual behaviour in my application (for example, the persistence of deleted ListView items).
I have the following code:
class TaskWidget extends StatefulWidget {
TaskWidget({this.task, this.callToSave, this.callToDelete});
final Task task;
final Function callToSave;
final Function callToDelete;
#override
State<StatefulWidget> createState() {
return new TaskWidgetState(task, callToSave, callToDelete);
}
}
class TaskWidgetState extends State<TaskWidget>{
Task task;
Function toCallOnChange;
Function callToDelete;
TaskWidgetState(Task task, Function callToSave, Function callToDelete){
print("I'm a task widget for " + task.serialise().toString());
this.task = task;
toCallOnChange = callToSave;
this.callToDelete = callToDelete;
}
}
and
class ToDoListWidget extends State<ToDoList>{
List<Task> _toDo = new List<Task>();
...
#override
Widget build(BuildContext context) {
print("building");
return new Scaffold(
body: new ListView(
children: <Widget> [
generateCard(),
...
]
),
);
}
Widget generateCard() {
return new Card(
child: new Column (
children: generateWidgets()
),
...
);
}
List<Widget> generateWidgets() {
print("generating Widgets");
List<Task> tasks = getTasks();
List<Widget> widgets = new List<Widget>();
print("I have " + tasks.length.toString() + " widgets to build");
for(Task t in tasks) {
print(t.title);
TaskWidget widget = new TaskWidget(task: t, callToSave: saveList, callToDelete: deleteTask,);
widgets.add(widget);
}
return widgets;
}
}
Prints out:
building
I/flutter (28783): Returning for Daily
I/flutter (28783): // correct, undeleted task
but onscreen state doesn't reflect this
You're not using State and Stateful Widget properly.
How it works in flutter is that the Widget can be created many times, but there will most likely only be one instance of a State to go along with it.
It's a bit of an anti-pattern to have a constructor for a state.
Instead you should be doing something like this:
class TaskWidget extends StatefulWidget {
TaskWidget({this.task, this.callToSave, this.callToDelete});
final Task task;
final Function callToSave;
final Function callToDelete;
#override
State<StatefulWidget> createState() => new TaskWidgetState();
}
class TaskWidgetState extends State<TaskWidget>{
Widget build(Context context) {
// you can just use the widget.task, this is to illustrate.
var task = widget.task;
var callToSave = widget.callToSave;
var callToDelete = widget.calltoDelete;
}
}
This way, when the widget changes, your state will be re-built and will use whatever the updated values are that were passed into the widget.

Resources