I have a function on the Grandparent. I pass it down to the parent, and the parent passes it down to the child. The function is called on the child. The expected behaviour is that the function on the grandparent is executed, but it is not. Nothing happens. For testing purposes I also included a button on the parent, that works correctly as expected.
So, what should happen is: when the listTile on the child is tapped, 'print from parent' should appear on the console.
Grandparent (snippet):
StateLoaded(
vm: vm,
procesStatus: _procesStatus,
onTapChange: () => print('print from parent'),
),
Parent (snippet):
class StateLoaded extends StatelessWidget {
const StateLoaded({
#required this.vm,
#required ProcesStatus procesStatus,
#required this.onTapChange,
}) : _procesStatus = procesStatus;
final AuthViewModel vm;
final ProcesStatus _procesStatus;
final Function() onTapChange;
#override
Widget build(BuildContext context) {
return Visibility(
visible: vm.failureVm.failure == null &&
vm.isAuth &&
_procesStatus == ProcesStatus.loaded,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 8),
Avatar(),
Divider(),
UserName(vm: vm, onTapChange: onTapChange),
Child:
class UserName extends StatelessWidget {
const UserName({
#required this.vm,
#required this.onTapChange,
});
final AuthViewModel vm;
final Function() onTapChange;
#override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(
SimpleLineIcons.pencil,
color: Theme.of(context).primaryColor,
size: 18,
),
title: Text(
vm.userName ?? '',
style: linkTextStyle,
),
onTap: onTapChange(),
);
}
}
The onTap, on ListTile child, is wrong.
There's 2 ways to correct it :
onTap: onTapChange,
or
onTap: () => onTapChange(),
Related
I have a list of dynamic forms where I need to add and remove form fields between two fields dynamically. I am able to add/remove form fields from the bottom of the list properly.
However, when I try to add a form field in between two form fields the data for the field does not update correctly.
How can I correctly add a field in between the two fields and populate the data correctly?
import 'package:flutter/material.dart';
class DynamicFormWidget extends StatefulWidget {
const DynamicFormWidget({Key? key}) : super(key: key);
#override
State<DynamicFormWidget> createState() => _DynamicFormWidgetState();
}
class _DynamicFormWidgetState extends State<DynamicFormWidget> {
List<String?> names = [null];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dynamic Forms'),
),
body: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
itemBuilder: (builderContext, index) => Row(
children: [
Flexible(
child: TextFormField(
initialValue: names[index],
onChanged: (name) {
names[index] = name;
debugPrint(names.toString());
},
decoration: InputDecoration(
hintText: 'Enter your name',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8))),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: () {
setState(() {
if(index + 1 == names.length){
names.add( null); debugPrint('Added: $names');
} else {
names.insert(index + 1, null); debugPrint('Added [${index+1}]: $names');
}
});
},
color: Colors.green,
iconSize: 32,
icon: const Icon(Icons.add_circle)),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: (index == 0&& names.length == 1)
? null
: () {
setState(() {
names.removeAt(index);
});
debugPrint('Removed [$index]: $names');
},
color: Colors.red,
iconSize: 32,
icon: const Icon(Icons.remove_circle)),
),
],
),
separatorBuilder: (separatorContext, index) => const SizedBox(
height: 16,
),
itemCount: names.length,
),
);
}
}
Basically the problem is that Flutter is confused about who is who in your TextFormField list.
To fix this issue simply add a key to your TextFormField, so that it can be uniquely identified by Flutter:
...
child: TextFormField(
initialValue: names[index],
key: UniqueKey(), // add this line
onChanged: (name) {
...
If you want to learn more about keys and its correct use take a look at this.
The widget AnimatedList solves this problem, it keep track of the widgets as a list would do and uses a build function so it is really easy to sync elements with another list. If you end up having a wide range of forms you can make use of the InheritedWidget to simplify the code.
In this sample i'm making use of the TextEditingController to abstract from the form code part and to initialize with value (the widget inherits from the ChangeNotifier so changing the value will update the text in the form widget), for simplicity it only adds (with the generic text) and removes at an index.
To make every CustomLineForm react the others (as in: disable remove if it only remains one) use a StreamBuilder or a ListModel to notify changes and make each entry evaluate if needs to update instead of rebuilding everything.
class App extends StatelessWidget {
final print_all = ChangeNotifier();
App({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FormList(print_notifier: print_all),
floatingActionButton: IconButton(
onPressed: print_all.notifyListeners,
icon: Icon(Icons.checklist),
),
),
);
}
}
class FormList extends StatefulWidget {
final ChangeNotifier print_notifier;
FormList({required this.print_notifier, super.key});
#override
_FormList createState() => _FormList();
}
class _FormList extends State<FormList> {
final _controllers = <TextEditingController>[];
final _list_key = GlobalKey<AnimatedListState>();
void print_all() {
for (var controller in _controllers) print(controller.text);
}
#override
void initState() {
super.initState();
widget.print_notifier.addListener(print_all);
_controllers.add(TextEditingController(text: 'Inital entrie'));
}
#override
void dispose() {
widget.print_notifier.removeListener(print_all);
for (var controller in _controllers) controller.dispose();
super.dispose();
}
void _insert(int index) {
final int at = index.clamp(0, _controllers.length - 1);
_controllers.insert(at, TextEditingController(text: 'Insert at $at'));
// AnimatedList will take what is placed in [at] so the controller
// needs to exist before adding the widget
_list_key.currentState!.insertItem(at);
}
void _remove(int index) {
final int at = index.clamp(0, _controllers.length - 1);
// The widget is replacing the original, it is used to animate the
// disposal of the widget, ex: size.y -= delta * amount
_list_key.currentState!.removeItem(at, (_, __) => Container());
_controllers[at].dispose();
_controllers.removeAt(at);
}
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _list_key,
initialItemCount: _controllers.length,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
itemBuilder: (ctx, index, _) {
return CustomLineForm(
index: index,
controler: _controllers[index],
on_insert: _insert,
on_remove: _remove,
);
},
);
}
}
class CustomLineForm extends StatelessWidget {
final int index;
final void Function(int) on_insert;
final void Function(int) on_remove;
final TextEditingController controler;
const CustomLineForm({
super.key,
required this.index,
required this.controler,
required this.on_insert,
required this.on_remove,
});
#override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: TextFormField(
controller: controler,
),
),
IconButton(
icon: Icon(Icons.add_circle),
onPressed: () => on_insert(index),
),
IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () => on_remove(index),
)
],
);
}
}
Actually, I have a parent widget, and It has some of the child widgets in its Column.
like this
Container(
width: double.infinity,
color: Colors.white,
padding: EdgeInsets.fromLTRB(20.0, 50.0, 20.0, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Title(),
SizedBox(height: 80.0,),
confirmLoginType(),
SizedBox(height: 10.0),
LoginButton(),
PolicyTips(
key: IndexGlobalKey.policyTipsKey,
updateState: receiveMessageUpdateState
),
Bottom()
],
),
),
in the PolicyTips, I pass a key to it, and I want to get the key in the LoginButton , but It has always been null when I get currentState.
The code is below:LoginButton
class LoginButton extends StatefulWidget {
LoginButton({Key key}) : super(key: key);
#override
_LoginButtonState createState() => _LoginButtonState();
}
class _LoginButtonState extends State<LoginButton> {
#override
Widget build(BuildContext context) {
policyTipsKey = IndexGlobalKey.policyTipsKey.currentState;
return Container(
child: Text()
)
}
}
what can I do? help me please, thanks.
This is IndexGlobalKey code.
class IndexGlobalKey {
static final GlobalKey<_PolicyTipsState> policyTipsKey = GlobalKey<_PolicyTipsState>();
static GlobalKey<_FormState> phoneLoginKey = GlobalKey<_FormState>();
static GlobalKey<_FormForIdCardLoginState> idCardLoginKey = GlobalKey<_FormForIdCardLoginState>();
}
Build method of _LoginButtonState runs before PolicyTips renders and before IndexGlobalKey.policyTipsKey is actually set. The reason is LoginButton goes before PolicyTips in column. Thats why you get null when you call IndexGlobalKey.policyTipsKey.currentState from build of _LoginButtonState.
To solve this you need to call IndexGlobalKey.policyTipsKey.state right where you use it. For example, when you need to get policy tips state on button tap just use it inside onPressed callback:
class _LoginButtonState extends State<LoginButton> {
#override
Widget build(BuildContext context) {
// An example of your button
return TextButton(
onPressed: () {
final policyTipsState = IndexGlobalKey.policyTipsKey.currentState;
// Here you can use policyTipsState
},
child: Text('button'),
);
}
}
I'm using riverpod to manage states of some variables in my app like opacity, stroke width and color for my coloring app.
Here's my opacity class inside notifier.dart:
class OpacityChangeNotifier extends ChangeNotifier {
OpacityChangeNotifier([this.opacity = 1.0]);
double opacity;
void changeOpacity(double providedOpacity) {
opacity = providedOpacity;
notifyListeners();
}
void printOpacity() {
print(opacity);
}
}
This is from my OpacityPicker.dart:
final _opacityProvider = ChangeNotifierProvider<OpacityChangeNotifier>((ref) {
return OpacityChangeNotifier();
});
class OpacityPicker extends ConsumerWidget {
const OpacityPicker({Key key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
onPressed: () {
context.read(_opacityProvider).changeOpacity(0.1);
context.read(_opacityProvider).printOpacity();
},
icon: Icon(Icons.opacity, size: 20),
),
IconButton(
onPressed: () {
context.read(_opacityProvider).changeOpacity(0.5);
context.read(_opacityProvider).printOpacity();
},
icon: Icon(Icons.opacity, size: 30),
),
IconButton(
onPressed: () {
context.read(_opacityProvider).changeOpacity(1.0);
context.read(_opacityProvider).printOpacity();
},
icon: Icon(Icons.opacity, size: 40),
),
],
),
);
}
}
finally this is my menu_items.dart:
final _opacityChangeProvider =
ChangeNotifierProvider<OpacityChangeNotifier>((ref) {
return OpacityChangeNotifier();
});
class UtilityItems extends ConsumerWidget {
const UtilityItems({Key key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
final opacityNotifier = watch(_opacityChangeProvider);
return SingleChildScrollView(
child: Container(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
onPressed: () {
print(opacityNotifier.opacity);
},
icon: Icon(Icons.dock_rounded),
)
],
),
OpacityPicker(),
],
),
),
);
}
}
Everything is working fine inside OpacityPicker.dart. When I'm pressing the opacity button the selected opacity is getting printed. But when I'm pressing the Icons.dock_rounded in menu_items.dart shouldn't I get the updated value?
It's showing the default value for opacity which is 1.0
I've looked it up and all I got was some issues related to changeNotifier not working back in September 2020.
What am I missing here?
N.B: I've imported all the files correctly. And I want the value of opacity to change to the user selected one on pressed. So I need my menu_item.dart widget to know that.
Building off #puelo comment, you shouldn't be redefining your ChangeNotifierProvider. The way you have it is two isolated providers with two separate ChangeNotifiers that have no knowledge of eachother.
I would recommend making provider a static member of your ChangeNotifier like so:
class OpacityChangeNotifier extends ChangeNotifier {
OpacityChangeNotifier([this.opacity = 1.0]);
static final provider = ChangeNotifierProvider<OpacityChangeNotifier>((ref) {
return OpacityChangeNotifier();
});
double opacity;
void changeOpacity(double providedOpacity) {
opacity = providedOpacity;
notifyListeners();
}
void printOpacity() {
print(opacity);
}
}
To access:
context.read(OpacityChangeNotifier.provider);
This helps to keep your imports clean as well as avoiding redundant naming and reduces the likelihood someone else working on the project would end up creating another provider for that notifier.
Use that provider instead of defining it twice and that should solve your primary issue.
You should also always use context.read inside function handlers like onPressed. In your menu_items.dart you should refactor as follows:
class UtilityItems extends ConsumerWidget {
const UtilityItems({Key key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
return SingleChildScrollView(
child: Container(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
onPressed: () {
final opacity = context.read(OpacityChangeNotifier.provider).opacity;
print(opacity);
},
icon: Icon(Icons.dock_rounded),
)
],
),
OpacityPicker(),
],
),
),
);
}
}
I have created a stateless widget that has a Flutter switch widget I implement this widget in the parent and pass in the required parameters but it won't change value when I press the switch.
I thought it might have been due to the fact that the child widget wasn't stateful but that made no difference.
Here is a brief example of code from my two widget files
class SettingsButton extends StatelessWidget {
final String text;
final bool initalValue;
final void Function(bool) onOffCallback;
SettingsButton({
this.text,
this.initalValue = false,
this.onOffCallback,
});
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: MediaQuery.of(context).size.width,
child: SubHeading(text),
),
Switch(
onChanged: isOnOff ? onOffCallback : null,
activeColor: Theme.of(context).accentColor,
value: initalValue,
)
]);
class _SettingsState extends State<Settings> {
bool test = true;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: ListView(
children: [
Column(
children: [
SettingsButton(
text: "Test",
onOffCallback: (test) => setState(() {
print("Called");
test = !test;
}),
initalValue: test,
),
],
)
],
),
);
}
You're setting the test variable recieved in the callback, rather than that defined in the _SettingsState class. What you should have is this for the callback:
onOffCallback: (newTest) => setState(() {
print("$newTest");
test = newTest;
// or (it shouldn't matter)
test = !test;
print("$test");
}),
I want to make a reusable button with a container in GestureDetector which will execute some function if I tap it and its color will become dark if I hold it. Any help, hint, tip would be very much appreciated.
I tried writing the GestureDetector in the custom widget file but it gives me errors.
When i try to extract widget on the GestureDetector it gives an Reference to an enclosing class method cannot be extracted error.
(the main page)
import 'package:flutter/material.dart';
import 'ReusableTwoLineList.dart';
import 'Text_Content.dart';
const mainTextColour = Color(0xFF212121);
const secondaryTextColour = Color(0xFF757575);
const inactiveBackgroundCardColor = Color(0xFFFFFFFF);
const activeBackgroundCardColor = Color(0xFFE5E5E5);
enum CardState {
active,
inactive,
}
class SettingsPage extends StatefulWidget {
#override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
CardState currentCardState = CardState.inactive;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Settings'),
),
body: ListView(
children: <Widget>[
GestureDetector(
onTapDown: (TapDownDetails details) {
setState(() {
currentCardState = CardState.active;
});
},
onTapCancel: () {
setState(() {
currentCardState = CardState.inactive;
});
},
onTap: () {
setState(() {
currentCardState = CardState.inactive;
//some random function
});
},
child: ReusableTwoLineList(
mainTextColor: mainTextColour,
secondaryTextColor: secondaryTextColour,
backgroundCardColor: currentCardState == CardState.active
? activeBackgroundCardColor
: inactiveBackgroundCardColor,
cardChild: TextContent(
mainLabel: 'First Day',
secondaryLabel: 'This is the first day of the week',
),
),
),
ReusableTwoLineList(
mainTextColor: mainTextColour,
secondaryTextColor: secondaryTextColour,
cardChild: TextContent(
mainLabel: '2nd day',
secondaryLabel: 'This is the end day',
),
),
ReusableTwoLineList(
mainTextColor: mainTextColour,
secondaryTextColor: secondaryTextColour,
),
],
),
);
}
}
ReusableTwoLineList.dart (the custom widget i am trying to make)
class ReusableTwoLineList extends StatelessWidget {
ReusableTwoLineList({
#required this.mainTextColor,
#required this.secondaryTextColor,
this.backgroundCardColor,
this.cardChild,
this.onPressed,
});
final Color mainTextColor, secondaryTextColor, backgroundCardColor;
final Widget cardChild;
final Function onPressed;
#override
Widget build(BuildContext context) {
return Container(
color: backgroundCardColor,
padding: EdgeInsets.symmetric(horizontal: 16),
height: 72,
width: double.infinity,
child: cardChild,
);
}
}
This is what i want but in a custom widget so i can use it over and over.
Normal-https://i.imgur.com/lVUkMFK.png
On Pressed-https://i.imgur.com/szuD4ZN.png
You can use extract method instead of extract widget. Flutter will add everything as it is, and instead of a class you will get a reusable function.