I have a parent widget which calls a custom Switch widget that I've made. I need the value of the switch(whether it's ON or OFF) in my parent widget. Can I create a controller in my switch widget which would return that value?
Currently, I am passing the function from my parent widget which changes the value of a boolean which is in my parent widget based on the value of the switch in the switch widget.
Parent widget:
bool isSwitchOn = false;
Switch(onSwitchToggle: (bool val) {
isSwitchOn = val;
})
Custom Switch widget:
class Switch extends StatefulWidget {
Widget build(BuildContext context) {
return CupertinoSwitch(
value: widget.value,
onChanged: (bool value) {
setState(() {
widget.value = value;
});
widget.onSwitchToggle(value);
},
),
}
The switch widget is used everywhere in the code whenever I need a switch and sometimes I don't need to know the state of the switch and I just need to execute a function when the switch is toggled but the way I've written the code, I'll need to pass bool everywhere whenever I call the switch. Looking for a better way to do it.
eg: Bool val is unnecessary because i won't need it.
Switch(onSwitchToggle: (bool val) {
print('abc')
})
You can easily solve it by using a ChangeNotifier. That's actually also the way it's solved in TextFieldController and ScrollController.
I have some sample code for you based on what you described:
class SwitchController extends ChangeNotifier {
bool isSwitchOn = false;
void setValue(bool value) {
isSwitchOn = value;
notifyListeners();
}
}
Now your widget that wraps the Switch:
class CustomSwitch extends StatefulWidget {
CustomSwitch({
required this.controller
});
final SwitchController controller;
#override
State<StatefulWidget> createState() {
return _CustomSwitchState();
}
}
class _CustomSwitchState extends State<CustomSwitch> {
#override
Widget build(BuildContext context) {
return CupertinoSwitch(
onChanged: (bool value) {
widget.controller.setValue(value);
},
value: widget.controller.isSwitchOn,
);
}
}
Just listen to the change event of the native switch and set the value of the controller. Then notify the observers about it.
Then you can create the widget and pass a controller you add a listener to:
class SwitchTest extends StatefulWidget {
#override
_SwitchTestState createState() => _SwitchTestState();
}
class _SwitchTestState extends State<SwitchTest> {
SwitchController controller = SwitchController();
#override
void initState() {
controller.addListener(() {
setState(() {
print(controller.isSwitchOn);
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
CustomSwitch(
controller: controller
),
Text(controller.isSwitchOn.toString()),
],
),
),
);
}
}
I have written a detailed tutorial on how to create such custom controllers on my blog: https://www.flutterclutter.dev/flutter/tutorials/create-a-controller-for-a-custom-widget/2021/2149/
Related
I'm trying to create a custom menu bar in my app. Right now, the biggest issue I'm having is passing a state for when it's expanded to it's children after a setState occurs.
I thought about inheritance, but from what I've tried all inheritance needs to be in-line. I can't create a widget where the children [] are fed into the constructor on an ad-hoc basis.
My current approach is to use a GlobalKey to update the State of the children widgets being inserted into the StateFul while updating them directly.
The children for my MenuBar are declared as:
List<MenuBarItem> menuItems;
MenuBarItem is an abstract interface class that I intend to use to limit the widgets that can be fed in as menuItems to my MenuBar.
abstract class iMenuItem extends Widget{}
class MenuBarItem extends StatefulWidget implements iMenuItem{
At some iterations of this script, I had a bool isExpanded as part of the iMenuItem, but determined it not necessary.
Here is my code at its current iteration:
My Main:
void main() {
// runApp(MainApp());
//runApp(InherApp());
runApp(MenuBarApp());
}
class MenuBarApp extends StatelessWidget{
#override
Widget build(BuildContext context){
return MaterialApp(
home: Scaffold(
body: MenuBar(
menuItems: [
// This one does NOT work and is where I'm trying to get the
// value to update after a setState
MenuBarItem(
myText: 'Outsider',
),
],
),
),
);
}
}
My Code:
import 'package:flutter/material.dart';
/// Primary widget to be used in the main()
class MenuBar extends StatefulWidget{
List<MenuBarItem> menuItems;
MenuBar({
required this.menuItems,
});
#override
State<MenuBar> createState() => MenuBarState();
}
class MenuBarState extends State<MenuBar>{
bool isExpanded = false;
late GlobalKey<MenuBarContainerState> menuBarContainerStateKey;
#override
void initState() {
super.initState();
menuBarContainerStateKey = GlobalKey();
}
#override
Widget build(BuildContext context){
return MenuBarContainer(
menuItems: widget.menuItems,
);
}
}
class MenuBarContainer extends StatefulWidget{
List<MenuBarItem> menuItems;
late Key key;
MenuBarContainer({
required this.menuItems,
key,
}):super(key: key);
#override
MenuBarContainerState createState() => MenuBarContainerState();
}
class MenuBarContainerState extends State<MenuBarContainer>{
bool isExpanded = false;
#override
void initState() {
super.initState();
isExpanded = false;
}
#override
Widget build(BuildContext context){
List<Widget> myChildren = [
ElevatedButton(
onPressed: (){
setState((){
this.isExpanded = !this.isExpanded;
});
},
child: Text('Push Me'),
),
// This one works. No surprise since it's in-line
MenuBarItem(isExpanded: this.isExpanded, myText: 'Built In'),
];
myChildren.addAll(widget.menuItems);
return Container(
child: Column(
children: myChildren,
),
);
}
}
/// The item that will appear as a child of MenuBar
/// Uses the iMenuItem to limit the children to those sharing
/// the iMenuItem abstract/interface
class MenuBarItem extends StatefulWidget implements iMenuItem{
bool isExpanded;
String myText;
MenuBarItem({
key,
this.isExpanded = false,
required this.myText,
}):super(key: key);
#override
State<MenuBarItem> createState() => MenuBarItemState();
}
class MenuBarItemState extends State<MenuBarItem>{
#override
Widget build(BuildContext context){
GlobalKey<MenuBarState> _menuBarState;
return Row(
children: <Widget> [
Text('Current Status:\t${widget.isExpanded}'),
Text('MenuBarState GlobalKey:\t${GlobalKey<MenuBarState>().currentState?.isExpanded ?? false}'),
Text(widget.myText),
],
);
}
}
/// To give a shared class to any children that might be used by MenuBar
abstract class iMenuItem extends Widget{
}
I've spent 3 days on this, so any help would be appreciated.
Thanks!!
I suggest using ChangeNotifier, ChangeNotifierProvider, Consumer and context.read to manage state. You have to add this package and this import: import 'package:provider/provider.dart';. The steps:
Set up a ChangeNotifier holding isExpanded value, with a setter that notifies listeners:
class MyNotifier with ChangeNotifier {
bool _isExpanded = false;
bool get isExpanded => _isExpanded;
set isExpanded(bool isExpanded) {
_isExpanded = isExpanded;
notifyListeners();
}
}
Insert the above as a ChangeNotifierProvider in your widget tree at MenuBar:
class MenuBarState extends State<MenuBar> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyNotifier(),
child: MenuBarContainer(
menuItems: widget.menuItems,
));
}
}
After this you can easily read and write the isExpanded value from anywhere in your widget tree under the ChangeNotifierProvider, for example:
ElevatedButton(
onPressed: () {
setState(() {
final myNotifier = context.read<MyNotifier>();
myNotifier.isExpanded = !myNotifier.isExpanded;
});
},
child: Text('Push Me'),
),
And if you want to use this state to automatically build something when isExpanded is changed, use Consumer, which will be notified automatically upon every change, for example:
class MenuBarItemState extends State<MenuBarItem> {
#override
Widget build(BuildContext context) {
return Consumer<MyNotifier>(builder: (context, myNotifier, child) {
return Row(
children: <Widget>[
Text('Current Status:\t${myNotifier.isExpanded}'),
Text(widget.myText),
],
);
});
}
}
In the code below I am using a callback to forward value from child to parent widget but I'm not calling setState() callback and yet I am able to print latest value? Can anyone Explain this because from My understanding a value is only updated when setState() is called.
Parent:
class One extends StatefulWidget {
const One({Key key}) : super(key: key);
#override
_OneState createState() => _OneState();
}
class _OneState extends State<One> {
String _text;
//CallBack
func(text) {
//No setState() is being called here.
_text = text;
}
#override
Widget build(BuildContext context) {
print('Build One');
return Scaffold(
body: SafeArea(
child: Column(
children: [
Two(func: func),
TextButton(
child: Text('Press Me'),
onPressed: () {
print(_text);
},
),
],
),
),
);
}
}
Child:
class Two extends StatefulWidget {
const Two({Key key, this.func}) : super(key: key);
final Function func;
#override
_TwoState createState() => _TwoState();
}
class _TwoState extends State<Two> {
String text;
#override
Widget build(BuildContext context) {
print('Build Two');
return TextField(
onChanged: (value) {
setState(() {
text = value;
});
//Callback is being executed after the setState yet when i press button in Parent widget it prints latest value.
widget.func(text);
},
);
}
}
setState updates the variable changes in the UI by rebuilding the widget.
Let's say you have a variable i = 0 in your code and on the press of a button this variable gets incremented i++.
int i = 0;
void increment(){
i++;
}
if you have used this variable i in the UI, then the increment function will update the value of i but no changes will reflect on the UI.
In order to update the changes in the UI we use setState like this
void increment(){
setState({
i++;
});
}
In a Flutter Desktop app, I want to know if, when a user clicks on a button with the mouse, they were also holding down a key (like Shift, Control, Alt etc).
How can this be done?
EDIT
My initial question wasn't clear enough.
I have a dynamic list of checkboxes and I want to use SHIFT+click to select everything between the last selected one and the one that was selected with SHIFT down.
I have looked at FocusNode but that seems to only work for 1 element.
This can be done with a FocusNode.
You'll need a stateful widget where you can use initialize the node. You need to attach the node and define the callback that is called on keyboard presses. Then you can request focus from the node with requestFocus so that the node receives the keyboard events.
You'll also need to call _nodeAttachment.reparent(); in your build method. You should also dispose the node in dispose.
The example below prints true or false for whether the shift key is pressed when the button is pressed. This can be easily expanded to other keys like control and alt with the isControlPressed and isAltPressed properties.
Full example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final FocusNode focus;
late final FocusAttachment _nodeAttachment;
bool isShiftPressed = false;
#override
void initState() {
super.initState();
focus = FocusNode(debugLabel: 'Button');
_nodeAttachment = focus.attach(context, onKey: (node, event) {
isShiftPressed = event.isShiftPressed;
});
focus.requestFocus();
}
#override
void dispose() {
focus.dispose();
super.dispose();
}
Widget build(BuildContext context) {
_nodeAttachment.reparent();
return TextButton(
onPressed: () {
print(isShiftPressed);
},
child: Text('Test'),
);
}
}
You can still use this solution for your more specific problem. Wrap the above example around your list of checkboxes. You can do a bit of simple logic to get your intended behavior. If what I have here is not exact, you should be able to easily modify it to your needs. This proves that you can use this method for your need, however, even if some details in the logic are not exact:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final FocusNode focus;
late final FocusAttachment _nodeAttachment;
bool isShiftPressed = false;
List<bool> checkboxStates = List.filled(5, false);
int lastClicked = -1;
#override
void initState() {
super.initState();
focus = FocusNode(debugLabel: 'Button');
_nodeAttachment = focus.attach(context, onKey: (node, event) {
isShiftPressed = event.isShiftPressed;
});
focus.requestFocus();
}
#override
void dispose() {
focus.dispose();
super.dispose();
}
Widget build(BuildContext context) {
_nodeAttachment.reparent();
return Column(
children: List.generate(checkboxStates.length, (index) => Checkbox(
value: checkboxStates[index],
onChanged: (val) {
if(val == null) {
return;
}
setState(() {
if(isShiftPressed && val) {
if(lastClicked >= 0) {
bool loopForward = lastClicked < index;
if(loopForward) {
for(int x = lastClicked; x < index; x++) {
checkboxStates[x] = true;
}
}
else {
for(int x = lastClicked; x > index; x--) {
checkboxStates[x] = true;
}
}
}
}
checkboxStates[index] = val;
});
if(val) {
lastClicked = index;
}
else {
lastClicked = -1;
}
print('Checkbox $index: $isShiftPressed');
}
)),
);
}
}
I have a list of choice widget and want to pass the selected choice to another widget.
Here is the list of choice widget
class ChoiceChipWidget extends StatefulWidget {
final List<String> reportList;
final Function(String item) onChoiceSelected;
ChoiceChipWidget(this.reportList, this.onChoiceSelected);
#override
_ChoiceChipWidgetState createState() => new _ChoiceChipWidgetState();
}
class _ChoiceChipWidgetState extends State<ChoiceChipWidget> {
String selectedChoice = "";
_buildChoiceList() {
List<Widget> choices = List();
widget.reportList.forEach((item) {
choices.add(Container(
child: ChoiceChip(
label: Text(item),
selected: selectedChoice == item,
onSelected: (selected) {
setState(() {
selectedChoice = item;
widget.onChoiceSelected(item);
print(selectedChoice); //DATA THAT NEEDS TO BE PASSED
});
},
),
));
});
return choices;
}
#override
Widget build(BuildContext context) {
return Wrap(
children: _buildChoiceList(),
);
}
}
I need to pass it to this widget
class AddCashPage extends StatefulWidget {
#override
_AddCashPageState createState() => _AddCashPageState();
}
class _AddCashPageState extends State<AddCashPage> {
void createTodo() async {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
String repetition = //DATA NEEDS TO GO HERE;
final addCash = AddCash(repetition);
setState(() {
id = addCash.id;
});
}
}
#override
Widget build(BuildContext context) {
return Container(
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
Row(
children: <Widget>[
ChoiceChipWidget(chipList, (item) {
selectedItem = item;
}),
],
),
RaisedButton(
child: Text("Update Cash Flow"),
onPressed: createTodo,
),
],
),
),
);
}
}
I tried making a constructor inside AddCashPage
like this
class AddCashPage extends StatefulWidget {
final ChoiceChipWidget choiceChipWidget;
AddCashPage({Key key, #required this.choiceChipWidget}) : super(key: key);
#override
_AddCashPageState createState() => _AddCashPageState();
}
I think you just missed to call setState() in here:
ChoiceChipWidget(chipList, (item) {
selectedItem = item;
}),
Like this:
ChoiceChipWidget(chipList, (item) {
setState(() => selectedItem = item);
}),
Then you could do this:
AddCash(selectedItem)
Make sure to declare the selectedItem variable inside _AddCashPageState, I don't see it on your code.
Your choice widget passes the data to the AddCashPage via the constructor you created, but you're missing something. You need to pass the data that AddCashPage has to its state (_AddCashState) so that you can use it there. Basically, you need to create one more constructor.
class AddCashPage extends StatefulWidget {
final ChoiceChipWidget choiceChipWidget;
AddCashPage({Key key, #required this.choiceChipWidget}) : super(key: key);
#override
_AddCashPageState createState() => _AddCashPageState(choiceChipWidget: choiceChipWidget);
}
And in _AddCashPageState:
class _AddCashPageState extends State<AddCashPage> {
final choiceChipWidget;
_AddCashPageState({Key key, #required this.choiceChipWidget});
}
To use your passed data inside _AddCashPageState class you can use widget property of the corresponding state of the related Stateful class.
For Ex : To use choice chip widget in your class you can use it like widget.ChoiceChipWidget
Any properties/methods provided in AddCashPage class can be accessed in its State class _AddCashPageState() using widget.ChoiceChipWidget property;
You can use this widget property inside methods only like, initState(), build(), dispose() etc.
im trying to build a Switch Widget witch changes the displayed Text. Therefore i have the Class with the Switch Widget:
class SwitchWidget extends StatelessWidget{
static bool switchOn = false;
void _onSwitchChanged(bool value) {
switchOn = false;
}
#override
Widget build(BuildContext context) {
return Switch(
onChanged: _onSwitchChanged,
value: switchOn,
);
}
}
switchOn says if the Switch is On or not.
Then i have the Widget witch puts the text of the my String text to "ON" if switchOn is true and to "OFF" if its false:
class SwichTextWidget extends StatelessWidget{
static String text = "OFF";
#override
Widget build(BuildContext context){
if (SwitchWidget.switchOn == true){
text = "ON";
}
else{
text = "OFF";
}
}
}
In another class i now use my Widgets:
class MatrixPageOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
...
Row( children:[
Text(" Clock Mode"),
SwitchWidget(),
Text(SwichTextWidget.text),
]//Row children
)
but if i use my Switch the text stays at OFF. I expected, that it wouldnt work like this, but whats the easiest way to make it work?
Thanks for every help!
Flutter way of doing that would be like that:
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool switch = false;
#override
Widget build(BuildContext context) {
return Center(
child: SwitchListTile(
title: Text('Clock Mode'),
value: switch,
onChanged: (bool value) {
setState(() {
switch = value;
});
},
secondary: Icon(Icons.access_time), // or Text(switch ? "ON" : "OFF") instead of an Icon
),
);
}
}
_onSwitchChanged of SwitchWidget has the value set to false irrespective of the passed value :
void _onSwitchChanged(bool value) {
switchOn = false;
}
Change it to :
void _onSwitchChanged(bool value) {
switchOn = value;
}
And the text will change.