In my build, I have this function that basically stacks a button and a counter, i'm also passing a function (this let's me reuse the buildbuttoncolumn for all buttons instead of copying code arround
my build:
Widget build(BuildContext context) {
List<Widget> _layouts = [
_videoInfo(),
_channelInfo(),
_comment(),
_moreInfo(),
VideoList(
channel: widget.channel,
isMiniList: true,
currentVideoId: widget.detail.id
),
];
if (MediaQuery.of(context).orientation == Orientation.landscape) {
_layouts.clear();
}
return Scaffold(
body: Column(children: <Widget>[
_buildVideoPlayer(context),
Expanded(
child: ListView(
children: _layouts,
),
)
]));
}
my videoinfo:
Widget _videoInfo() {
return Column(
children: <Widget>[
ListTile(
title: Text(widget.detail.title),
subtitle: Text(widget.detail.viewCount + ' . ' + widget.detail.publishedTime),
trailing: Icon(Icons.arrow_drop_down),
),
Container(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buildButtomColumn(Icons.thumb_up, widget.detail.likeCount, function: _like(widget.detail.id, true)),
_buildButtomColumn(Icons.thumb_down, widget.detail.dislikeCount, function: _like(widget.detail.id, false)),
_buildButtomColumn(CupertinoIcons.share_up, "Partilhar"), //function: share(context, widget.detail.player)
_buildButtomColumn(Icons.add_to_photos, "Guardar"),
],
),
)
],
);
}
_buildButtomColumn(Icons.thumb_up, widget.detail.likeCount, function: _like(widget.detail.id, true)),
the method then does something like this:
Widget _buildButtomColumn(IconData icon, String text, {function}) {
return GestureDetector(
onTap: () => function
child: Column(.....
oh and here's the like:
_like(String videoId, bool liked) {
youtubeAPI.likeDislikeVideo(videoId, liked);
}
when I open the page, the onTap gets triggered without me actually pressing the button.
Where is the problem?
You are calling that function yourself, there is no problem with onTap callback and it is not fired somehow without user interaction
Next snippet performs _like function invocation and pass returned result to function:... arg (refer to lang tour)
function: _like(widget.detail.id, true)
You can prevent such situations if you declare function: argument type as Function and you will get static analysis type error before you run code
_buildButtomColumn(IconData icon, String text, {Function function})
Back to you code - how to fix it?
pass function argument directly to onTap argument
Widget _buildButtomColumn(IconData icon, String text, {VoidCallback function}) {
/// here I enforced type as VoidCallback - it is typedef for `void Function()`
return GestureDetector(
onTap: function, // <-- pass function, onTap type is VoidCallback
child: Column(.....
2.a. pass anonymous function with desired payload
_buildButtomColumn(Icons.thumb_up, widget.detail.likeCount,
function: () => youtubeAPI.likeDislikeVideo(videoId, liked), // <-- this will be invoked later
)
2.b. this variant is for sake of completeness
declare callable class and pass it's instance to function:... arg ()
class LikeCommand {
final String videoId;
final bool liked;
LikeCommand(this.videoId, this.liked);
void call() => youtubeAPI.likeDislikeVideo(videoId, liked);
}
_buildButtomColumn(Icons.thumb_up, widget.detail.likeCount,
function: LikeCommand(videoId, liked),
)
PS I recommend declaring types, since dart is a strongly typed language and specifying types will save you from typical problems in the future
PPS feel free to reach me in comments
Related
I have a List of String stored in a variable like this:
var question = [
'whats your favorite name',
'whats your favorite color',
'whats your favorite shape',
];
And have function that increase a variable each time I call it
void _Press(){
setState((){
_PressedNo = _PressedNo + 1;
});
print(_PressedNo );
}
And in my scaffold iam checking if the _PressedNo is smaller that List so it rebuild the scaffold with the new question each time i press the button till the questions finished like this :
return MaterialApp(
home: Scaffold(
body:_PressedNo < question.length ? Container(
child: RaisedButton(
child: Text('yes'),
onPressed:() {
_Press();
},
),
) : // here i want to go to another page
And after the else : i want to go to another Flutter page, but I can only add widgets here so any solution how to do that..
You'll have to take a bit of a different approach. The build method should only provide things that appear on the screen and not directly trigger a specific action or state change, such as navigating to another page.
You can handle this in the _Press() method:
void _Press() {
if(_PressedNo < questions.length) {
setState(() {
_PressedNo = _PressedNo + 1;
});
} else {
Navigator.of(context).push(...); // navigate to another page
// optionally set the _PressedNo back to 0
setState(() {
_PressedNo = 0;
});
}
}
Your build method should not change, since on this page it should always show the same button.
return MaterialApp(
home: Scaffold(
body: Container(
child: RaisedButton(
child: Text('yes'),
onPressed:() {
_Press();
},
),
),
);
Some other pointers:
use lowerCase for functions and variables: _press() and _pressedNo.
You can directly pass in the function to onPressed like onPressed: _press, (note that there are no parentheses since the function is not called but just passed along)
I add a button to add a list of widget in my screen, like this:
List<Widget> timeWidget = [];
buildTime() {
setState(
() {
timeWidget.add(Padding(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
HourScheduleItem(
enabled: true,
day: _day,
onRemove: () {
timeWidget.remove(data);
},
onChanged: (date, hour) {
setState(
() {
_day = date;
_hour = hour;
print(_hour);
},
);
},
)
],
),
));
},
);
}
When a click onTap button buildTime(); the return is a constructor into Column in my screen:
Column(children: timeWidget.map((data) {
return data;
}).toList()),
But when I choose an option in the next widget added, the option chosen is shown only in the first widget, I believe this must be due to the fact that it does not get an index?
enter image description here
This due to how states and widgets in the widget tree are linked together. The flutter YouTube channel has a great in depth explanation of this topic, but TL;DR flutter doesn't "know" which widget in the list you actually clicked on unless you give each widget a unique identifier to tell them apart!
In almost all cases, you can do this by adding a key: UniqueKey() parameter to the constructor of the widget you're putting in a list. In your case, you would pass it in to your HourScheduleItem which would then pass it into the super() constructor.
In my code I added a dropdown which looks like below, When I switched the selection in dropdown its not getting updated its showing exception, I have declared a variable in statefull widget , In my dropdown function I am assigning that as A value to the dropdown Button, and in Onchanged I am passing the json to another function there I am taking the value from a variable and assigning it to a opSelected variable inside the setState
class _ReportFilterState extends State<ReportFilter> {
String opSelected;
//declared string to hold the selected value in dropdown within the state class.
buildMainDropdown(List<Map<String, Object>> items, StateSetter setState) {
return Container(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 27.0,
vertical: 16.0,
),
child: Align(
alignment: Alignment.topLeft,
child: DropdownButtonHideUnderline(
child: DropdownButton(
isExpanded: true,
hint: Text("Choose Filters"),
value: opSelected, // Here assigning the value
items: items
.map((json) => DropdownMenuItem(
child: Text(json["displayName"]), value: json))
.toList(),
onChanged: (json) {
manageIntState(json, setState);
},
),
),
),
),
);
}
void manageIntState(Map<String, Object> jsonSelected, StateSetter setState) {
setState(() {
dispName = jsonSelected["displayName"];
//here I am setting the selected value
opSelected = dispName;
//Doing some operations
id = jsonSelected['id'];
type = jsonSelected['type'];
selectedFilterOption = jsonSelected;
if (jsonSelected.containsKey("data")) {
List<Map<String, Object>> tempList;
List<String> dailogContent = List<String>();
tempList = jsonSelected['data'];
tempList
.map((val) => {
dailogContent.add(val['displayId']),
})
.toList();
_showReportDialog(dailogContent);
}
});
}
But when I run I will end up with error
items==null||
items.isEmpty||value==null||itsems.where((DropdownMenuItem
item)=>item.value==value).length==1 is not true ..
Let me know what I have done wrong in code so its giving me like this, if I commented its not showing the selected dropdown value.
That error happens when the selected value of the DropdownButton is not one of the values of it's items.
In your case, your items values are json which is a Map<String, Object>, and the value of the DropdownButton is opSelected which is a String.
So you need to change the type of opSelected like this:
Map<String, Object> opSelected;
Also make sure you are passing a reference of the same list of items to buildMainDropdown(), because if you are creating a new list while calling buildMainDropdown() then the DropdownButton will have another reference of options and it's not allowed
Note: you may want yo use dynamic instead of Object for the Map, like this:
Map<String, dynamic> opSelected;
Here is why: What is the difference between dynamic and Object in dart?
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((){});
},
)