There are 3 buttons and I want to change the color of the clicked button when I click it.
I manage the _currentFilter variable using obx and change the _currentFilter value using the changeFilter() function every time I click the button.
And the UI section used Obx to detect the changed value and change the color.
However, clicking each button changes the value of _currentFilter, but does not update the UI.
Only Hot Reload will change the color of the button text.
Why doesn't Obx detect that the _currentFilter value has changed in the UI?
I don't know why it is reflected in the UI only when doing Hot Reload.
Please Help.
UI
class FilterButtons extends GetView<TodoController> {
const FilterButtons({Key? key}) : super(key: key);
Widget filterButton(FilterEnum filter) {
return TextButton(
onPressed: () => controller.changeFilter(filter),
child: Obx(() {
final color = controller.filter == filter ? Colors.blueAccent : Colors.grey;
return Text(filter.name, style: TextStyle(fontSize: 20, color: color));
}),
);
}
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
filterButton(FilterEnum.ALL),
filterButton(FilterEnum.ACTIVE),
filterButton(FilterEnum.COMPLETED),
],
);
}
}
Controller
class TodoController extends GetxController {
var _currentFilter = FilterEnum.ALL.obs;
get filter => _currentFilter;
void changeFilter(FilterEnum filter) {
_currentFilter = filter.obs;
}
}
if you assign new observable to _currentFilter, GetX won't get notified because previous observable that view is listening to didn't change.
if you change it to:
_currentFilter.value = filter;
GetX fires a method called refresh() that refreshes the view.
Related
I want to change the value of an integer in another class and rebuild this class so the bottom navigation bar items will change according to the integer
Here's the main class were the bottom navigation bar exists and the condition to hide or show the items :
class Home extends StatefulWidget {
static int showCard = 0;
#override
_HomeState createState() => _HomeState();
}
#override
Widget build(BuildContext context) {
...
//the items to show and hide according to int showcard
if (Home.showCard == 0)
BottomNavigationBarItem(
icon: Icon(
Icons.person_outlined,
),
title: Text(
'Profile',
),
),
if (Home.showCard != 0)
BottomNavigationBarItem(
icon: Icon(Icons.settings),
title: Text(
'Settings',
),
)
And here's the second class where i want to change the value of showcard to hide the profile section ad show the setting , i'm calling set state with on pressed in this class it's changing the value but not rebuilding the main class :
class Homehome extends StatefulWidget {
const Homehome({Key? key}) : super(key: key);
#override
_HomehomeState createState() => _HomehomeState();
}
class _HomehomeState extends State<Homehome> {
#override
Widget build(BuildContext context) {
...
child: RaisedButton(
onPressed: () {
setState(() {
Home.showCard = 1;
});
Navigator.of(context)
.push(MaterialPageRoute(
builder: (context) => Green(),
));
},
any suggestions ?
You can use callback functions:
have a look at this solution.
https://stackoverflow.com/a/59832932/16479524
upvote if helpful :)
You don't need to change the state. You should only update the state when you want to change something inside a widget, for instance a Text value or something like that.
Every time you change a value, it changes. If you want to change a 'visible value', then you have to change the state by setState method.
It sounds like you need to get a better understanding of state.
Widgets in Flutter live in a hierarchy. You should have a state in one widget, and then child widgets update based on this state. You can pass state changes down to child widgets using parameters.
Also, prefer to use proper data types for the situation. Don’t use a numerical data type like an int for showing or hiding a widget; use a bool (true or false).
I'm trying to implement some shortcuts on a desktop app. I've been looking into these links:
Understanding Flutter's focus system
Focus and text fields
Using Actions and Shortcuts
I would like to do an action when the user presses on the key a (for example).
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
int count = 0;
KeyEventResult onKey(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.keyA) {
setState(() {
count++;
});
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Focus(
onKey: onKey,
child: Column(
children: [
Text('count: $count'),
const TextField(),
TextButton(
child: Text('button'),
onPressed: () {},
),
],
),
),
),
);
}
}
When the user clicks on a, the count increments.
The issue with this code is the user being unable to type a in the text field anymore because it is being handled by the focus node.
The first link states:
Key events start at the focus node with primary focus. If that node doesn’t return KeyEventResult.handled from its onKey handler, then its parent focus node is given the event. If the parent doesn’t handle it, it goes to its parent, and so on, until it reaches the root of the focus tree.
and
Focus key events are processed before text entry events, so handling a key event when the focus widget surrounds a text field prevents that key from being entered into the text field.
I would like my Focus widget to handle the key event only if the subtree didn't handle it itself including TextFields (and therefore text entry events).
I tried to always return KeyEventResult.ignored in the onKey method, but the OS triggers a sound meaning there is no action available every time the user clicks on a.
Is there a way to implement what I am trying to do? If yes, how?
You could wrap your text field in a focus that returns KeyEventResult.skipRemainingHandlers in onKeyEvent. This will prevent it affecting the other handlers while the text field is focused. Although this is inelegant and I'd love a correction with a better solution.
const unhandledKeys = [
LogicalKeyboardKey.delete,
LogicalKeyboardKey.backspace,
LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight
];
final focusNode = useFocusNode(onKeyEvent: (_a, _b) {
if (unhandledKeys.contains(event.logicalKey)) {
return KeyEventResult.ignored;
}
return KeyEventResult.skipRemainingHandlers;
});
return Focus(
focusNode: focusNode,
child: TextField()
);
I have an input page to create a new reminder. On this page you will select several different variables (reminder type, start date, etc.) - at this stage I am just trying to get it work for two variables.
I have a button object that I create, which takes some text, a "isSelected" value (which changes the color to show it is selected) and an onPress callback. The plan is to use a loop to create one of these buttons for each of the necessary selection options and then feed that into a ListView, so you have a scroll-able list of selection option. As you select the item the properties of the new reminder object will update and the color will change to selected.
When I click the button, the value is selected (the print statement shows this) but the button does not change to the new isSelected value, despite a SetState being used. What is it I am missing here? Is it possible to feed buttons into a ListView like this and still have their state update? Or do you need to find another work around?
class AddReminder extends StatefulWidget {
#override
_AddReminderState createState() => _AddReminderState();
}
class _AddReminderState extends State<AddReminder> {
String addReminder = "";
Reminder newReminder = Reminder();
#override
List<Widget> getReminderTypesButton(
String selectionName, List selectionOptions, var reminderVariable) {
// create new list to add widgets to
List<Widget> selectionOptionsWidgets = [];
// loop through selection options and create buttons
for (String selection in selectionOptions) {
bool isSelectedValue = false;
selectionOptionsWidgets.add(
FullWidthButton(
text: selection,
isSelected: isSelectedValue,
onPress: () {
setState(() {
reminderVariable = selection;
isSelectedValue = true;
});
print(reminderVariable);
},
),
);
}
;
// return list of widgets
return selectionOptionsWidgets;
}
Widget build(BuildContext context) {
List<List<Widget>> newList = [
getReminderTypesButton("Type", reminderTypesList, newReminder.type),
getReminderTypesButton(
"Frequency", repeatFrequencyTypesList, newReminder.repeatFrequency)
];
List<Widget> widgetListUnwrap(List<List<Widget>> inputList) {
//takes list of list of widgets and converts to widget list (to feed into list view)
List<Widget> widgetsUnwrapped = [];
for (var mainList in inputList) {
for (var widgets in mainList) {
widgetsUnwrapped.add(widgets);
}
}
return widgetsUnwrapped;
}
return SafeArea(
child: Container(
color: Colors.white,
child: Column(
children: [
Hero(
tag: addReminder,
child: TopBarWithBack(
mainText: "New reminder",
onPress: () {
Navigator.pop(context);
},
)),
Expanded(
child: Container(
child: ListView(
children: widgetListUnwrap(newList),
shrinkWrap: true,
),
),
),
],
),
),
);
}
}
Here are the lists that I reference
List<String> reminderTypesList = [
"Appointment",
"Check-up",
"Other",
];
List<String> repeatFrequencyTypesList = [
"Never",
"Daily",
"Weekly",
"Monthly",
"Every 3 months",
"Every 6 months",
"Yearly",
];
List<List<String>> selectionOptions = [
reminderTypesList,
repeatFrequencyTypesList
];
The reason your state not changing is that every time you call setState(), the whole build function will run again. If you initiate a state (in this case isSelectedValue) within the build method (since the getReminderTypesButton() got called within the build), the code will run through the below line again and again, resetting the state to the initial value.
bool isSelectedValue = false;
This line will always set the isSelectedValue to false, no matter how many time you call setState.
In order to avoid this, you need to place the state outside of the build method, ideally in the FullWidthButton like this:
class FullWidthButton extends StatefulWidget {
const FullWidthButton({Key? key}) : super(key: key);
#override
_FullWidthButtonState createState() => _FullWidthButtonState();
}
class _FullWidthButtonState extends State<FullWidthButton> {
bool isSelectedValue = false;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () => setState(() => isSelectedValue = !isSelectedValue),
// ...other lines
);
}
}
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: () {})
],
),
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((){});
},
)