setState doens't cause re-render of new screen in flutter - dart

I got a List, called savedListsList, that contains a list of Objects. When the app starts this list is empty. When the users clicks on a PopupMenuItem (depicted in the case 0: case), data is loaded from an internal database and after that, a new screen shows up where the data is shown (basically a StatelessWidget that contains a ListView.builder with a certain amount of rows).
Every row consists of a GestureDetector that listens for left and right swipes.
On every of these swipes updateSaveListItem is called. According to the beingEdited attribute the appearance of the row inside the before mentioned ListView.builder is changed.
I know that the swipes trigger the attribute to change accordingly, since I printed that out.
Unfortunately the change in updateSavedListItem doesn't cause a re-render of the current screen. When I hot-reload the app the change of beingEdited is reflected.
I'm pretty sure that I can get this to work, when I turn the
widget that surrounds the ListView.builder into a StatefulWidget.
But my question is: can I trigger a re-render of the current page with the architecture I described? Since I want to keep as much of my state management as I can inside my _MainPage class.
class _MainPage extends State<MainPage> {
List<SavedListObj> savedListsList = List();
#override
void initState() {
savedListsList = List();
}
void updateSavedListItem(int id, String action, String newName) {
setState(() {
switch (action) {
case 'beingEditedStart':
savedListsList[id].beingEdited = true;
break;
case 'beingEditedEnd':
savedListsList[id].beingEdited = false;
break;
case 'delete':
break;
case 'edit':
break;
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
PopupMenuButton(
onSelected: (value) {
switch (value) {
case 0:
DB db = DB();
db.getListNames().then((queryResult) {
savedListsList.clear();
setState(() {
savedListsList.addAll(queryResult);
});
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SavedListsPage(
savedListsList,
updateSavedListItem,
),
),
);
break;
}
},
),
],
),
);
}
}

No you cannot do that! you have to use Stateful widget when you have to update the state of your app. For futher reference you can refer this link.

Related

Bloc pattern does not show newly added user to sqflite on screen

I am using sqflite database to save user list.
I have user list screen, which shows list of user and it has a fab button,
on click of fab button, user is redirected to next screen where he can add new user to database.
The new user is properly inserted to the database
but when user presses back button and go backs to user list screen,
the newly added user is not visible on the screen.
I have to close the app and reopen it,then the newly added user is visible on the screen.
I am using bloc pattern and following is my code to show user list
class _UserListState extends State<UserList> {
UserBloc userBloc;
#override
void initState() {
super.initState();
userBloc = BlocProvider.of<UserBloc>(context);
userBloc.fetchUser();
}
#override
void dispose() {
userBloc?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).pushNamed("/detail");
},
child: Icon(Icons.add),
),
body: StreamBuilder(
stream: userBloc.users,
builder: (context, AsyncSnapshot<List<User>> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data != null) {
return ListView.builder(
itemBuilder: (context, index) {
return Dismissible(
key: Key(snapshot.data[index].id.toString()),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
userBloc.deleteParticularUser(snapshot.data[index]);
},
child: ListTile(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => UserDetail(
user: snapshot.data[index],
)));
},
title: Text(snapshot.data[index].name),
subtitle:
Text("Mobile Number ${snapshot.data[index].userId}"),
trailing:
Text("User Id ${snapshot.data[index].mobileNumber}"),
),
);
},
itemCount: snapshot.data.length,
);
}
},
),
);
}
}
Following is my bloc code
class UserBloc implements BlocBase {
final _users = BehaviorSubject<List<User>>();
Observable<List<User>> get users => _users.stream;
fetchUser() async {
await userRepository.initializeDatabase();
final users = await userRepository.getUserList();
_users.sink.add(users);
}
insertUser(String name,int id,int phoneNumber) async {
userRepository.insertUser(User(id, name, phoneNumber));
fetchUser();
}
updateUser(User user) async {
userRepository.updateUser(user);
}
deleteParticularUser(User user) async {
userRepository.deleteParticularUser(user);
}
deleteAllUser() {
return userRepository.deleteAllUsers();
}
#override
void dispose() {
_users.close();
}
}
As Remi posted answer saying i should try BehaviorSubject and ReplaySubject which i tried but it does not help. I have also called fetchUser(); inside insertUser() as pointed in comments
Following is the link of the full example
https://github.com/pritsawa/sqflite_example
Follow up from the comments, it seems you don't have a single instance of your UsersBloc in those two pages. Both the HomePage and UserDetails return a BlocProvider which instantiate a UsersBloc instance. Because you have two blocs instances(which you shouldn't have) you don't update the streams properly.
The solution is to remove the BlocProvider from those two pages(HomePage and UserDetail) and wrap the MaterialApp widget in it to make the same instance available to both pages.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: UserBloc(),
child:MaterialApp(...
The HomePage will be:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return UserList(),
);
}
}
Remove the BlocProvider from UserDetail as well.
In the UsersBloc then call fetchUser() inside the insertUser() method after the user insertion, to requery the database and update the users stream.
Also as Rémi Rousselet said, use one of the subjects that return previous values.
The issue is that you're using a PublishSubject.
When a new listener subscribes to a PublishSubject, it does not receive the previously sent value and will only receive the next events.
The easiest solution is to use a BehaviorSubject or a ReplaySubject instead. These two will directly call their listener with the latest values.
#override
void initState() {
super.initState();
userBloc = BlocProvider.of<UserBloc>(context);
userBloc.fetchUser();
}
The problem is that you have called the userBloc.fetchUser() function in the initState of the page.
Bloc stream emits whenever a new data is added to it and the userBloc.fetchUser() function does exactly that, it adds the userList that you fetch from the Sqflite database.
Whenever you come back to the userlist screen from add user screen, init function is NOT called. It is only called when the userlist screen is created, that is, whenever you push it to the navigation stack.
The workaround is to call userBloc.fetchUser() whenever your StreamBuilder's snapshot data is null.
...
if (!snapshot.hasData) {
userBloc.fetchUser();
return Center(
child: CircularProgressIndicator(),
);
}
...

Wait for subtree widgets to finish loading async data before showing them

I have multiple widget components in my app, and each component requires different data from the server. On initState of the component, I fetch the data it requires.
When the app opens I fetch what components to show from the server. Example: show only component 1 and 3. How can I wait for 1 and 3 to load without showing them initially ( just showing a loader) and when both are done loading, I would show them or show an error page?
You could use a ternary to validate if the data is ready, if not you can just show a CircleProgressIndicator, something like this :
class _LoadingComponentsState extends State<LoadingComponents> {
bool waiting = true;
loadingAsyncTask() async {
//.. your logic
setState(() {
waiting = false;
});
}
#override
void initState() {
loadingAsyncTask();
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: waiting
? Center(
child: CircularProgressIndicator(),
)
: ListView(
children: [
//your data...
],
),
);
}
}

Flutter: Maintant state of child stateful widget

I have a listview.builder inside a stateful widget and i made a separate stateful widget for the item (ImageCard).
inside the ImageCard widget i have a like button when i click it its color change to red(like), gray(dislike).
the problem is that when i scroll down and return back the color is always gray which means that no state is saved!
how can i notify the parent stateful widget to keep the state?
Parent stateful widget
#override
Widget build(BuildContext context) {
return _buildListView(models, _scrollController);
}
Widget _buildListView(
List<PhotoModel> models, ScrollController scrollController) {
return Container(
child: ListView.builder(
controller: scrollController,
itemCount: models.length,
itemBuilder: (context, int index) {
if (index == models.length - 1) {
return SpinKitThreeBounce(
color: Colors.purple,
size: 30.0,
);
} else {
return ImageCard(
models[index].regularPhotoUrl,
models[index].mediumProfilePhotoUrl,
models[index].name,
models[index].color);
}
}));
}
child stateful widget
class ImageCard extends StatefulWidget {
final String imageUrl, userProfilePic, userName, color;
ImageCard(this.imageUrl, this.userProfilePic, this.userName, this.color);
#override
_ImageCardState createState() => _ImageCardState();
}
class _ImageCardState extends State<ImageCard> {
bool isLiked = false, isFollowing = false;
#override
Widget build(BuildContext context) {
return new Card( ....
void _onLikedBtnClicked() {
setState(() {
if (isLiked)
isLiked = false;
else {
isLiked = true;
}
});
}
Flutter will automatically disposes the widget that moves out of screen, and when they re-appear, they will be re-built rather than recovered.
So common practice is to save the state in a high-level widget, which contains at least a complete aspect of business logic and is not going to be disposed anytime soon. Then a change in the state is mapped into child widgets.
For your specific case, a simple solution is: you store the information in the parent widget, and maps them to a ImageCard inside the parent widget's build function.
Add isliked,isfollowing property to the model, then
class SomeParentState extends State<SomeParent> {
List<Model> models;
//.......
#override
Widget build(BuildContext context) {
return _buildListView(models, _scrollController);
}
Widget _buildListView(List<PhotoModel> models,
ScrollController scrollController) {
return Container(
child: ListView.builder(
controller: scrollController,
itemCount: models.length,
itemBuilder: (context, int index) {
if (index == models.length - 1) {
return SpinKitThreeBounce(
color: Colors.purple,
size: 30.0,
);
} else {
return ImageCard(
models[index].regularPhotoUrl,
models[index].mediumProfilePhotoUrl,
models[index].name,
models[index].color,
models[index].isLiked,
models[index].isFollowing,
() {
setState(() {
models[index].isLiked = !models[index].isLiked;
});
},
() {
setState(() {
models[index].isFollowing = !models[index].isFollowing;
});
},
);
}
}));
}
}
class ImageCard extends StatelessWidget{
ImageCard(
//...,
this.isLiked,
this.isFollowing,
this.likeBtnClickedListener,
this.followBtnClickedListener,
)
//...
Widget build(BuildContext context){
return Card(
//.......
IconButton(
onPressed: likeBtnClickedListener,
),
IconButton(
onPressed: followBtnClickedListener,
),
)
}
}
This should basically solve your problem. Anyway, it is easier to access and sync the data in the child widgets in this method.
If you find it easier to just keep the child widget alive, you can read the documentation of AutomaticKeepAliveClientMixin. It will stop flutter from killing this widget when it moves out of sight. But it is risky of causing memory leak.
To maintain the state of a widget inside a ListView, you need to AutomaticKeepAlive or AutomaticKeepAliveMixin (for custom widgets)
This will ensure the State instance is not destroyed when leaving the screen
ListView(
children: [
// Not kept alive
Text('Hello World'),
// kept alive
AutomaticKeepAlive(
child: Text("Hello World"),
),
]
),
You should keep your state separately then. You could make a List<bool> and have one value in there for each of the List items. You probably want to save or use the data at some point anyways, then this mechanism is going to be useless.

Flutter Like button functionality using Futures

I'm trying to build a Save button that lets the user save/ unsave (like/ unlike) items displayed in a ListView.
What I have so far:
Repository that provides a Future<bool> that determines which state the icon should be rendered in
FutureBuilder that calls the repository and renders the icon as either saved/ unsaved.
Icon wrapped in a GestureDetector that makes a call to the repository within a setState call when onTap is invoked.
`
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _repository.isSaved(item),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.none:
case ConnectionState.active:
return Icon(Icons.favorite_border);
case ConnectionState.done:
return GestureDetector(
child: Icon(
snapshot.data ? Icons.favorite : Icons.favorite_border,
color: snapshot.data ? Colors.red : null),
onTap: () {
setState(() {
if (snapshot.data) {
_repository.removeItem(item);
} else {
_repository.saveItem(item);
}
});
},
);
}
});
}
`
The issue I'm having is that when I tap to save an item in the list - the item is saved however the icon is not updated until I scroll it off screen then back on again.
When I tap to unsave an item, it's state is reflected immediately and updates as expected.
I suspect that the save call is taking longer to complete than the delete call. Both of these are async operations:
void removeItem(String item) async {
_databaseClient.deleteItem(item);
}
void saveItem(String item) async {
_databaseClient.saveItem(item);
}
#override
void deleteItem(String item) async {
var client = await db;
client.delete("items_table", where: "item = '$item'"); // returns Future<int> but I'm not using this currently
}
void _saveItem(String item) async {
var client = await db;
client.insert("items_table", item); // returns Future<int> but I'm not using this currently
}
Future<bool> isSaved(String name) async {
var matching = await _databaseClient.getNameByName(name);
return matching != null && matching.isNotEmpty;
}
Any idea what could be causing this?
When you tap the button, setState will be called. then FutureBuilder will wait for the isSaved method. if the save method is being in progress. isSaved will return the last state and Icon will not change.
I suggest to wait for the result of Save and Remove method and call setState after that.
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _repository.isSaved(item),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.none:
case ConnectionState.active:
return Icon(Icons.favorite_border);
case ConnectionState.done:
return GestureDetector(
child: Icon(
snapshot.data ? Icons.favorite : Icons.favorite_border,
color: snapshot.data ? Colors.red : null),
onTap: () async{
if (snapshot.data) {
await _repository.removeItem(item);
} else {
await _repository.saveItem(item);
}
setState(() {
});
},
);
}
});
}
However, if the methods take so long, it delays which cause bad user experience. it better to change the icon to progress circle during running methods.

TextField ErrorText Will not Refresh State

I currently have a TextField embedded inside a SimpleDialog for entering information. The information entered needs to be validated before being used. If this validation fails, I want to show an error message using the TextField's ErrorText field. This validation occurs when the user presses a "save" button.
On fail, I update the text's value from null to "Error!". I know this is being done correctly because if I exit the SimpleDialog and then go back into the dialog, the text is updated. Clearly this is an issue with state.
Here is my code for the OnPressed method for the save button:
onPressed: () {
if (!checkDevEUICorrectness()) {
setState(() {
devEUIErrorText = "Error!";
});
}
else {
setState((){
devEUIErrorText = null;
});
}
},
And here a brief bit of code for the TextField:
new TextField(
controller: devEUIController,
decoration: new InputDecoration(
errorText: devEUIErrorText,
// This is the save button whose code is above
icon: new IconButton(...implementation...),
),
),
How should I properly be updating the state for the text? I don't see a difference in what I am doing compared to the many guides I have looked at online.
The code you posted dosen't help with anything but i think ik what might be the error.
When we open a SimpleDialog on a button click and try to perform something inside it.
Take care the SimpleDialog here acts as a stateless widget .
when we write
showDialog(
context: context,
builder: (BuildContext context){
return SimpeDialog(
.... // the code for deigning the simpleDialog
);
});
What you have to do is inspite of doing this .
you need to do something like this :-
showDialog(
context: context,
builder: (BuildContext context){
return DialogDemo();
});
in that DialogDemo create a stateful widget and inside that widget copy paste all the code under your main build function.
class DialogDemo extends StatefulWidget {
  DialogDemoState createState() => new DialogDemoState();
}
class DialogDemoState extends State<DialogDemo> {
  #override
  void initState() {
    super.initState();
  }
  #override
  Widget build(BuildContext context) {
return SimpleDialog(
.... // the code for deigning the simpleDialog
);
}
}

Resources