How does setState actually work?
It seems to not do what I expect it to do when the Widget which should have been rebuilt is built in a builder function. The current issue I have is with a ListView.builder and buttons inside an AlertDialog.
One of the buttons here is an "AutoClean" which will automatically remove certain items from the list show in the dialog.
Note: The objective here is to show a confirmation with a list of "Jobs" which will be submitted. The jobs are marked to show which ones appear to be invalid. The user can go Back to update the parameters, or press "Auto Clean" to remove the ones that are invalid.
The button onTap looks like this:
GeneralButton(
color: Colors.yellow,
label: 'Clear Overdue',
onTap: () {
print('Nr of jobs BEFORE: ${jobQueue.length}');
for (int i = jobQueue.length - 1; i >= 0; i--) {
print('Checking item at $i');
Map task = jobQueue[i];
if (cuttoffTime.isAfter(task['dt'])) {
print('Removing item $i');
setState(() { // NOT WORKING
jobQueue = List<Map<String, dynamic>>.from(jobQueue)
..removeAt(i); // THIS WORKS
});
}
}
print('Nr of jobs AFTER: ${jobQueue.length}');
updateTaskListState(); // NOT WORKING
print('New Task-list state: $taskListState');
},
),
Where jobQueue is used as the source for building the ListView.
updateTaskListState looks like this:
void updateTaskListState() {
DateTime cuttoffTime = DateTime.now().add(Duration(minutes: 10));
if (jobQueue.length == 0) {
setState(() {
taskListState = TaskListState.empty;
});
return;
}
bool allDone = true;
bool foundOverdue = false;
for (Map task in jobQueue) {
if (task['result'] == null) allDone = false;
if (cuttoffTime.isAfter(task['dt'])) foundOverdue = true;
}
if (allDone) {
setState(() {
taskListState = TaskListState.done;
});
return;
}
if (foundOverdue) {
setState(() {
taskListState = TaskListState.needsCleaning;
});
return;
}
setState(() {
taskListState = TaskListState.ready;
});
}
TaskListState is simply an enum used to decide whether the job queue is ready to be submitted.
The "Submit" button should become active once the taskListState is set to TaskListState.ready. The AlertDialog button row uses the taskListState for that like this:
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
if (taskListState == TaskListState.ready)
ConfirmButton(
onTap: (isValid && isOnlineNow)
? () {
postAllInstructions().then((_) {
updateTaskListState();
// navigateBack();
});
: null),
From the console output I can see that that is happening but it isn't working. It would appear to be related to the same issue.
I don't seem to have this kind of problem when I have all the widgets built using a simple widget tree inside of build. But in this case I'm not able to update the display of the dialog to show the new list without the removed items.
This post is getting long but the ListView builder, inside the AleryDialog, looks like this:
Flexible(
child: ListView.builder(
itemBuilder: (BuildContext context, int itemIndex) {
DateTime itemTime = jobQueue[itemIndex]['dt'];
bool isPastCutoff = itemTime.isBefore(cuttoffTime);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text(
userDateFormat.format(itemTime),
style: TextStyle(
color:
isPastCutoff ? Colors.deepOrangeAccent : Colors.blue,
),
),
Icon(
isPastCutoff ? Icons.warning : Icons.cached,
color: isPastCutoff ? Colors.red : Colors.green,
)
],
);
},
itemCount: jobQueue.length,
),
),
But since the Row() with buttons also doesn't react to setState I doubt that the problem lies within the builder function itself.
FWIW all the code, except for a few items like "GeneralButton" which is just a boilerplate widget, resides in the State class for the Screen.
My gut-feeling is that this is related to the fact that jobQueue is not passed to any of the widgets. The builder function refers to jobQueue[itemIndex], where it accesses the jobQueue attribute directly.
I might try to extract the AlertDialog into an external Widget. Doing so will mean that it can only access jobQueue if it is passed to the Widget's constructor....
Since you are writing that this is happening while using a dialog, this might be the cause of your problem:
https://api.flutter.dev/flutter/material/showDialog.html
The setState call inside your dialog therefore won't trigger the desired UI rebuild of the dialog content. As stated in the API a short and easy way to achieve a rebuild in another context would be to use the StatefulBuilder widget:
showDialog(
context: context,
builder: (dialogContext) {
return StatefulBuilder(
builder: (stateContext, setInnerState) {
// return your dialog widget - Rows in ListView in Container
...
// call it directly as part of onTap of a widget of yours or
// pass the setInnerState down to another widgets
setInnerState((){
...
})
}
);
EDIT
There are, as in almost every case in the programming world, various approaches to handle the setInnerState call to update the dialog UI. It highly depends on the general way of how you decided to manage data flow / management and logic separation. As an example I use your GeneralButton widget (assuming it is a StatefulWidget):
class GeneralButton extends StatefulWidget {
// all your parameters
...
// your custom onTap you provide as instantiated
final VoidCallback onTap;
GeneralButton({..., this.onTap});
#override
State<StatefulWidget> createState() => _GeneralButtonState();
}
class _GeneralButtonState extends State<GeneralButton> {
...
#override
Widget build(BuildContext context) {
// can be any widget acting as a button - Container, GestureRecognizer...
return MaterialButton(
...
onTap: {
// your button logic which has either been provided fully
// by the onTap parameter or has some fixed code which is
// being called every time
...
// finally calling the provided onTap function which has the
// setInnerState call!
widget.onTap();
},
);
}
If you have no fixed logic in your GeneralButton widget, you can write: onTap: widget.onTap
This would result in using your GeneralButton as follows:
...
GeneralButton(
...
onTap: {
// the desired actions like provided in your first post
...
// calling setInnerState to trigger the dialog UI rebuild
setInnerState((){});
},
)
Related
I work on flutter project . when i click to modify icon to edit name for example ==> the screen is roaleded automatically . How i can stop refresh screen after click on edit button ?
this piece of my Form code :
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Adresse email :',
style: TextStyle(
color: Color(0xFF4053FCF),
fontSize: 16,
fontWeight: FontWeight.w600
),
),
IconButton(
icon: Icon(CommunityMaterialIcons.pencil,
color: Colors.grey,
),
onPressed: () {
emailNode.requestFocus();
setState(() {
enableemail = true;
});
})
],
),
void editUserProfile() async {
setState(() {});
// if (_formKey.currentState.validate()) {
String name = _nameController.text;
String email = _emailController.text;
String adress = _adressController.text;
userApi.editUserProfile(name, email, adress).then((data) {
print(data);
if (data != null) {
// Navigator.pop(context);
/* Navigator.push(
context, MaterialPageRoute(builder: (context) => Profile()));*/
}
// setState(() {});
/* Navigator.push(
context, MaterialPageRoute(builder: (context) => BoxSettings()));*/
setState(() {
enableup = false;
enableadress = false;
enableemail = false;
});
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(data)));
// ScaffoldMessenger.of(context).showSnackBar(snackBar3);
}).catchError((error) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(error.toString())));
});
setState(() {});
}
and this my screen for more information :
How i can press on edit button without reload screen ?
There some workarounds to achieve this (i.e. update the state of one widget after tapping a completely different widget) like passing the callback function as a parameter etc.
But The best and neat solution here which will solve the above problem and keep your code neat is using Provider pattern.
If you are not aware of how a Provider pattern works, you can easily google search for articles regarding it. Here is one of them :
https://www.raywenderlich.com/6373413-state-management-with-provider
Read the above article before moving below.
Basically what we do is :
Create a ChangeNotifier class.
Wrap the parent of both widgets by a ChangeNotifierProvider widget.
Wrap the widget you want to update with Consumer widget.
Then in your onTap/onPressed function of Edit button you can call a function which will call the notifyListener() function. What this will do is it will notify the above ChangeNotifierProvider widget that some change has neen occured in it's widget tree. Then it will traverse the child whole widget tree below and will update the widget wrapped with Consumer widget.
So this way, you wont need to refresh your whole screen and you can easily update one widget by doing some action on a competely different widget.
Wrap the widgets you want to refresh inside stateful builder and make the whole screen a stateless widget and then call stateful builder
Using a stateful widget, I have this build function:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Icon(MdiIcons.atom),
Text("Message Channel")
],
)
),
body: compileWidget(context),
bottomSheet: Row(
children: [
Expanded(
child: DefaultTextFormField(true, null, hintText: "Message ...", controller: messageField)
),
IconButton(
icon: Icon(Icons.send),
onPressed: onSendPressed
)
],
),
);
}
As you can see in the body, there is a compileWidget function defined as:
FutureBuilder<Widget> compileWidget(BuildContext context) {
return FutureBuilder(
future: createWidget(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
print("DONE loading widget ");
return snapshot.data;
} else {
return Container(
child: Center(
child: CircularProgressIndicator()
),
);
}
}
);
}
In turn, the createWidget function looks like this:
List<Widget> bubbles;
Future<Widget> createWidget(BuildContext context) async {
await updateChatList();
// when working, return listview w/ children of bubbles
return Padding(
padding: EdgeInsets.all(10),
child: ListView(
children: this.bubbles,
),
);
}
// update chat list below
Future<void> updateChatList({Message message}) async {
if (this.bubbles != null) {
if (message != null) {
print("Pushing new notification into chat ...");
this.bubbles.add(bubbleFrom(message));
} else {
print("Update called, but nothing to do");
}
} else {
var initMessages = await Message.getMessagesBetween(this.widget.implicatedCnac.implicatedCid, this.widget.peerNac.peerCid);
print("Loaded ${initMessages.length} messages between ${this.widget.implicatedCnac.implicatedCid} and ${this.widget.peerNac.peerCid}");
// create init bubble list
this.bubbles = initMessages.map((e) {
print("Adding bubble $e");
return bubbleFrom(e);
}).toList();
print("Done loading bubbles ...");
}
}
The chat bubbles populate the screen in good order. However, once a new message comes-in and is received by:
StreamSubscription<Message> listener;
#override
void initState() {
super.initState();
this.listener = Utils.broadcaster.stream.stream.listen((message) async {
print("{${this.widget.implicatedCnac.implicatedCid} <=> ${this.widget.peerNac.peerCid} streamer received possible message ...");
if (this.widget.implicatedCnac.implicatedCid == message.implicatedCid && this.widget.peerNac.peerCid == message.peerCid) {
print("Message meant for this screen!");
await this.updateChatList(message: message);
setState(() {});
}
});
}
The "message meant for this screen!" prints, then within updateChatList, "Pushing new notification into chat ..." prints, implying that the bubble gets added to the array. Finally, setState is called, yet, the additional bubble doesn't get rendered. What might be going wrong here?
First of all, let me explain how setState works according to this link.
Whenever you change the internal state of a State object, make the change in a function that you pass to setState. Calling setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.
In your case, I would say it's just a convention. You can define a message object in your stateful class and update it inside the listener and your setstate like this:
setState(() { this.message = message; });
Caution:
Before any changes, you need to check this question
Usage of FutureBuilder with setState
Because using setstete with FutureBuilder can make an infinite loop in StatefulWidget which reduces performance and flexibility. Probably, you should change your approach to design your screen because FutureBuilder automatically updates the state each time tries to fetch data.
Update:
There is a better solution for your problem. Because you are using a stream to read messages, you can use StreamBuilder instead of the listener and FutureBuilder. It brings less implementation and more reliability for services that provide a stream of data. Every time the StreamBuilder receives a message from the Stream, it will display whatever you want with the snapshot of data.
You need to ensure that the ListView has a UniqueKey:
ScrollController _scrollController = ScrollController();
Stream<Widget> messageStream;
#override
void initState() {
super.initState();
// use RxDart to merge 2 streams into one stream
this.messageStream = Rx.merge<Message>([Utils.broadcaster.stream.stream.where((message) => this.widget.implicatedCnac.implicatedCid == message.implicatedCid && this.widget.peerNac.peerCid == message.peerCid), this.initStream()]).map((message) {
print("[MERGE] stream recv $message");
this.widget.bubbles.add(bubbleFrom(message));
// this line below to ensure that, as new items get added, the listview scrolls to the bottom
this._scrollController = _scrollController.hasClients ? ScrollController(initialScrollOffset: _scrollController.position.maxScrollExtent) : ScrollController();
return ListView(
key: UniqueKey(),
controller: this._scrollController,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 50),
children: this.widget.bubbles,
);
});
}
Note: As per Erfan's directions, the context is best handled with a StreamBuilder. As such, I changed to a StreamBuilder, thus reducing the amount of code needed
The UI of my app is not updating when I know for a fact the state is changing. I am using the watch method from Riverpod to handle this, but the changes don't take effect unless I do a hot reload.
I have a class HabitListStateNotifier with methods to add/remove habits from the list:
class HabitListStateNotifier extends StateNotifier<List<Habit>> {
HabitListStateNotifier(state) : super(state ?? []);
void startAddNewHabit(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (_) {
return NewHabit();
});
}
void addNewHabit(String title) {
final newHabit = Habit(title: title);
state.add(newHabit);
}
void deleteHabit(String id) {
state.removeWhere((habit) => habit.id == id);
}
}
And here is the provider for this:
final habitsProvider = StateNotifierProvider(
(ref) => HabitListStateNotifier(
[
Habit(title: 'Example Habit'),
],
),
);
Here is how the HabitList (the part of the UI not updating) is implemented:
class HabitList extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final habitList = watch(habitsProvider.state);
/////////////not updating/////////////
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
return HabitCard(
habit: habitList[index],
);
},
itemCount: habitList.length,
);
/////////////not updating/////////////
}
}
And finally, the HabitCard (what the HabitList is comprised of):
class HabitCard extends StatelessWidget {
final Habit habit;
HabitCard({#required this.habit});
#override
Widget build(BuildContext context) {
/////////////function in question/////////////
void deleteHabit() {
context.read(habitsProvider).deleteHabit(habit.id);
}
/////////////function in question/////////////
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(devHeight * 0.03),
),
color: Colors.grey[350],
elevation: 3,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
HabitTitle(
title: habit.title,
),
Consumer(
builder: (context, watch, child) => IconButton(
padding: EdgeInsets.all(8),
icon: Icon(Icons.delete),
/////////////function in question/////////////
onPressed: deleteHabit,
/////////////function in question/////////////
),
),
],
),
],
),
);
}
}
When I press the delete icon in a HabitCard, I know the Habit is being removed from the list, but the change is not reflecting in the UI. However, when I do a hot reload, it disappears as expected. What am I doing wrong here?
Since StateNotifierProvider state is immutable, you need to replace the data on CRUD operations by using state = - which is what will trigger UI updates as per docs.
Then assign new data using state = <newData>
Add and Delete Rewrite
You need to write your add & delete like this:
void addNewHabit(String title) {
state = [ ...state, Habit(title: title)];
}
void deleteHabit(String id) {
state = state.where((Habit habit) => habit.id != id).toList();
}
You need to exchange your old list with a new one for Riverpod to fire up.
Update code (for others)
Whilst your query does not need to update data, here is an example of how an update could be done to retain the original sort order of the list;
void updateHabit(Habit newHabit) {
List<Habit> newState = [...state];
int index = newState.indexWhere((habit) => habit.id == newHabit.id);
newState[index] = newHabit;
state = newState;
}
I don't know if this is the right way to handle things, but I figured it out. In the HabitListStateNotifier, for addNewHabit and deleteHabit, I added this line of code: to the end: state = state; and it works exactly how I want it to.
I used to solve this Issue by putting state = state; at the end, but now for some reason maybe flutter or Riverpod update, It doesn't work anymore.
Anyway this is how I managed to solve it now.
void addNewHabit(String title) {
List<Habit> _habits = [...state];
final newHabit = Habit(title: title);
_habits.add(newHabit);
state = _habits ;
}
the explanation as I understand it. when state equals an object, in order to trigger the consumer to rebuild. state must equal a new value of that object, but updating variables of the state object itself will not work.
Hope this helps anyone.
I am trying to run a function(with arguments) inside two-levels down StateFul widget, by clicking a button in the parent of the parent of that child(after having all widgets built, so not inside the constructor). just like in the image below:
More details is that I created a Carousal which has Cards inside, published here.
I created it with StreamBuilder in mind(this was the only use case scenario that I used it for so far), so once the stream send an update, the builder re-create the whole Carousal, so I can pass the SELECTED_CARD_ID to it.
But now I need to trigger the selection of the carousal's Cards programmatically, or in another word no need for two construction based on the snapshot's data like this:
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return SelectableCarousal(
selectedId: snapshot.data.toInt(),
onTap: //Update the stream
//some props...,
);
} else {
return SelectableCarousalLoading(
selectedId: null,
onTap: //Update the stream
//some props...,
);
}
},
);
But instead, I'm trying to have something like this so I can use it for others use cases:
Widget myCarousal = SelectableCarousal(
selectedId: null,
onTap: //Update the stream
//some props...,
);
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
// Then when data ready I can update
// the selection by calling two-level down function
if (snapshot.hasData) {
myCarousal.selectById(3);
}
// Build same carousal in all cases.
return myCarousal;
},
);
so this led me to my original question "How to run a function(with arguments) inside two-levels down StateFul widget?".
I appreciate any help. thanks a lot.
I was able to solve that challenge using the BLoC & Stream & StreamSubscription, see the image below:
Inside the Homepage screen:
///...Inside Homepage screen level-0
RaisedButton(
child: Text('Update value in the BLoC'),
onPressed: () {
bloc.changeSelectedState(isSel);
},
),
//...
inside the BLoC:
class Bloc {
final BehaviorSubject<bool> _isSelectedStreamController = BehaviorSubject<bool>();
// Retrieve data from stream
Stream<bool> get isSelectedStream => _isSelectedStreamController.stream;
// Add data to stream
Function(bool) get changeSelectedState => _isSelectedStreamController.sink.add;
void dispose() {
_isSelectedStreamController.close();
}
}
final bloc = Bloc();
Inside any widget in any level as long as it can reach the bloc:
// This inside the two-levels down stateful widget..
StreamSubscription isSelectedSubscription;
Stream isSelectedStream = bloc.isSelectedStream;
isSelectedSubscription = isSelectedStream.listen((value) {
// Set flag then setState so can show the border.
setState(() {
isSelected = value;
});
});
//...other code
#override
Widget build(BuildContext context) {
return Container(
decoration: isSelected
? BoxDecoration(
color: Colors.deepOrangeAccent,
border: Border.all(
width: 2,
color: Colors.amber,
),
)
: null,
//...other code
);
}
so the new design of my widget includes the BLoC as a main part of it, see the image:
and...works like a charm with flexible and clean code and architecture ^^
I have a TextField and an IconButton in a row like so.
I would like the IconButton to be enabled only when there is text in the TextField. I am using the provider package for state management.
Here is the ChangeNotifier implementation.
class ChatMessagesProvider with ChangeNotifier{
List<ChatMessage> chatMessages = <ChatMessage>[];
bool messageTyped = false;
ChatMessagesProvider(this.chatMessages);
void newMessage(String textMessage){
ChatMessage message = ChatMessage(textMessage);
this.chatMessages.add(message);
notifyListeners();
}
int messageCount() => chatMessages.length;
void updateMessageTyped(bool typed){
this.messageTyped = typed;
// notifyListeners(); Un-comennting this makes the Text disappear everytime I type something on the text field
}
}
Here is the actual widget:
class TextCompose extends StatelessWidget {
final TextEditingController _composeTextEditingController = new TextEditingController();
TextCompose(this.chatMessagesProvider);
final ChatMessagesProvider chatMessagesProvider;
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Flexible(
child: new TextField(
controller: _composeTextEditingController,
onSubmitted: (String text) {
_onMessageSubmitted(text, context);
},
onChanged: (String text){
if(text.length > 0){
chatMessagesProvider.updateMessageTyped(true);
print(text);
}
else{
chatMessagesProvider.updateMessageTyped(false);
print("No text typed");
}
},
decoration: new InputDecoration.collapsed(
hintText: "Enter message"
),
),
),
new Container(
margin: new EdgeInsets.all(8.0),
child: new IconButton(
color: Theme.of(context).accentColor,
icon: new Icon(Icons.send),
disabledColor: Colors.grey,
onPressed:chatMessagesProvider.messageTyped // This dosen't work
? () => _onMessageSubmitted(_composeTextEditingController.text, context)
: null,
),
)
],
),
);
}
void _onMessageSubmitted(String text, BuildContext context){
if(chatMessagesProvider.messageTyped) { // This works fine.
// clear the message compose text box
_composeTextEditingController.clear();
// add the message to provider.
chatMessagesProvider.newMessage(text);
// set the message typed to false
chatMessagesProvider.messageTyped = false;
}
I am using messageTyped from ChatMessageProvider to check to see if there is any text in the TextField. It seems to work fine when I check it in the _onMessageSubmitted method but not when I check its value in the onPressed property for the IconButton.
I know this because I can see the IconButton remains disabled(colour doesn't change from grey) when I type text, whereas I can hit the submit button on the virtual keyboard and the text is cleared from the TextField(as per call to _composeTextEditingController.clear())
Question:
why does chatMessagesProvider.messageTyped return the right value when called from the _onMessageSubmitted but not when it is called from the onPrssed attribute in the IconButton?
How would I debug something like this in Flutter, I would really like to drop a breakpoint in onPressedAttribute and see the value for chatMessagesProvider.messageTyped
Let me know if you need to see any more of my code.
onPressed:chatMessagesProvider.messageTyped this line is being executed during widget build time so it is always default value and it will never get refreshed until unless you rebuild the widget using notify listener or stateful widget.
Store the currently being typed message in your provider and make your send button enable/disable depends on whether currently being typed message is empty or not.
You say you are using 'provider_package' but you actually have no Provider in your layout. Instead you have a custom built ChangeNotifier with no listeners - you are indeed calling notifyListeners() but there are actually no listeners, so no rebuild is being triggered. A rebuild is needed in order for the button to change its onPressed function reference and implicitly its color.
As for debugging, you can set a breakpoint on the line with onPressed, but it will only be hit during a rebuild.
The most important thing to understand is that the function reference you give to onPressed will be invoked correctly, but a rebuild is needed for the widget to change visually.
Although your current ChangeNotifier implementation does not make much sense, simply wrapping your calls to updateMessageTyped within setState should solve the visual problem - and your breakpoint will also be hit after each typed/deleted character.
The simplest solution you can, first of all, make your widget StatefulWidget.
You need a boolean inside State class:
bool hasText = false;
Then create initState:
#override
void initState() {
_composeTextEditingController.addListener(() {
if (_composeTextEditingController.text.isNotEmpty) {
setState(() {
hasText = true;
});
} else {
setState(() {
hasText = false;
});
}
});
super.initState();
}
Also don't forget to dispose:
#override
void dispose() {
_composeTextEditingController.dispose();
super.dispose();
}
And finally your build method:
Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _composeTextEditingController,
)),
if (hasText) IconButton(icon: Icon(Icons.send), onPressed: () {})
],
),