I'm new to RiverPod and generally it's been working for me, but I've run into a problem updating a DropdownButton. It updates the actual value appropriately, but then will not display it in the UI. If I refresh and get it to rebuild, then the correct value is displayed, but it's not rebuilding on its own. I'm sure there's an issue in the way I've set it up, I'm just not sure what it is. This is my DropdownButton:
Container editState(
String Function() state, Function update, bool Function() editable) =>
Container(
height: 35.0,
decoration:
BoxDecoration(border: Border.all(color: Colors.grey, width: 2.0)),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: state(),
icon: const Icon(Icons.arrow_downward),
onChanged: (String? newValue) {
if (newValue != null &&
newValue != '' &&
state() != newValue &&
editable()) {
update(newValue);
}
},
items: usStateList.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
);
This is all down the widget tree but still contained under a ConsumerWidget. That Widget starts...
class SinglePatientView extends ConsumerWidget {
const SinglePatientView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
const gap4 = Gap(4);
const gap32 = Gap(32);
final readNotifier = ref.read(singlePatientViewProvider.notifier);
final watchNotifier = ref.watch(singlePatientViewProvider.notifier);
bool editable() => ref.watch(boolProvider.notifier).state;
It then calls the following Widgets...
editAddress(
context,
street,
city,
watchNotifier.patientState,
zip,
readNotifier.updatePatientAddress,
editable,
)
List<Widget> editAddress(
BuildContext context,
TextEditingController streetAddressController,
TextEditingController cityController,
String Function() state,
TextEditingController zipCodeController,
Function update,
bool Function() editable,
) =>
[
editStateZip(
context,
state,
zipCodeController,
update,
editable,
),
];
Widget editStateZip(
BuildContext context,
String Function() state,
TextEditingController controller,
Function update,
bool Function() editable,
) =>
SizedBox(
child: Row(
children: [
editState(state, update, editable),
],
),
);
So as I said, it correctly updates the patient's State (geographical state, not app state), but doesn't redraw the Widget/UI automatically. I'm sure I'm missing something obvious, but any help would be appreciated.
More code (in case it's helpful):
StateNotifierProvider
final singlePatientViewProvider =
StateNotifierProvider<SinglePatientViewStateNotifier, Patient>(
(ref) => SinglePatientViewStateNotifier(ref));
The SinglePatientViewStateNotifier is constructed like this with the method being called shown:
class SinglePatientViewStateNotifier extends StateNotifier<Patient> {
SinglePatientViewStateNotifier(this.ref) : super(Patient());
final Ref ref;
void updatePatientAddress(String? geographicalState) {
state = state.copyWith(
address: updateAddressFromStrings(
address: state.address,
index: 0,
streetAddress: streetAddress,
city: city,
state: geographicalState,
postalCode: zipCode,
));
}
}
Related
I tried to figure it out, and read the documentation for both but I didn't find an answer, here is an example of what I mean:
List<String> items = ["item1", "item2", "item3", "item4"];
class HomeScreen extends StatelessWidget {
HomeScreen({super.key});
String selectedItem = items[0];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: DropdownButton(
value: selectedItem,
onChanged: (value) => selectedItem = value!,
items: items
.map(
(e) => DropdownMenuItem<String>(
value: e,
child: Text(e),
),
)
.toList(),
),
...
that's just a simple stateless widget with a DropdownButton at the center:
output of the code above
if we just change the widget to a DropdownButtonFormField with all else remains the same, changes to the selected item reflect in the UI: output of the same code after changing the widget to a DropdownButtonFormField
If you dig inside DropdownButtonFormField you will see it keeps a separate value for the menu inside its state. If you explore the code it says
onChanged: onChanged == null ? null : state.didChange,
state.didChange looks like:
#override
void didChange(T? value) {
super.didChange(value);
final DropdownButtonFormField<T> dropdownButtonFormField = widget as DropdownButtonFormField<T>;
assert(dropdownButtonFormField.onChanged != null);
dropdownButtonFormField.onChanged!(value);
}
and super.didChange looks like
void didChange(T? value) {
setState(() {
_value = value;
_hasInteractedByUser.value = true;
});
Form.of(context)?._fieldDidChange();
}
This changes the iternal value of the state and calls setState so that it refreshes the UI for it.
As a result, even if you change this line of your code:
onChanged: (value) => selectedItem = value!,
to
onChanged: (value){},
It still works visually, because it doesn't actually use selectedItem but the internal value.
I am using provider for state management, and I've given onTap a value in a function in the ChangeNotifier child class but my app is unresponsive, I mean, when i tap on the widget, it doesn't update state, however, it does change the values i need it to change tho, i know this coz I am debugPrinting in the onTap function and when i tap, it actually prints that the button got tapped, but state doesn't update, widget remains the same until i hot restart, then it updates everything, even hot reload doesn't update it, here's the function
class Storage extends ChangeNotifier{
static const _storage = FlutterSecureStorage();
static const _listKey = 'progress';
List _dataMaps = [];
List<DayTile> dayTileMain = [];
void createDataMap() {
for (int i = 1; i < 101; i++) {
final data = Data(number: i).toJson();
_dataMaps.add(data);
}
}
void createDayTiles() {
for(Map<String, dynamic> data in _dataMaps) {
bool isDone = data['i'];
final dayTile = DayTile(
number: data['n'],
isDone: isDone,
// This is where i need to rebuild the tree
onTap: () async {
data['i'] = true;
notifyListeners();
print(data['i']);
print(isDone);
await writeToStorage();
},
);
dayTileMain.add(dayTile);
}
print('data tiles created');
}
}
and here is the DayTile class
class DayTile extends StatelessWidget {
const DayTile({
Key? key,
required this.number,
required this.isDone,
required this.onTap,
}) : super(key: key);
final int number;
final VoidCallback onTap;
final bool isDone;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 50,
width: MediaQuery.of(context).size.width * .15,
decoration: BoxDecoration(
color: !isDone
? const Color(0xffedecea)
: const Color(0xffedecea).withOpacity(0.1),
borderRadius: BorderRadius.circular(5),
),
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
Center(
child: Text(
number.toString(),
style: const TextStyle(
color: Color(0xff576aa4),
),
),
),
Visibility(
visible: isDone,
child: const Divider(
color: Colors.black,
),
),
],
),
),
),
);
}
}
here is where I listen for the change
Wrap(
spacing: 13,
runSpacing: 13,
children: Provider.of<Storage>(context).dayTileMain,
),
when data['i'] is true, it should update the current instance of DayTile() that it's on in the loop, and in DayTile() I use the value of data['i'] to set the value of bool isDone and depending on whether isDone is true or false, the color of the widget changes and some other things, BUT, they don't change onTap, but they change after I hot restart, when it's read the storage and restored the saved data, could the secureStorage writing to storage at the same time be affecting it?
I solved it, turns out it's not a good idea to listen for events in the model class, it won't listen, so instead of generating the list of widgets in the model class, I moved it outta there, and instead generated it inside the wrap widget, and instead of listening for a list in the model class, i just had the list there in my wrap, if it was a listview i was tryna generated, i would've done this initially with a ListView.builder() i didn't know you could generate a list inside the children of the wrap widget, so i just stuck to defining it in the model, I came across this stack question Flutter: How to use Wrap instead of ListView.builder?
and that's how i knew how to build widgets inside a children property, i was actually just looking for a ListView.builder() version for the Wrap widget, all said, this is what my stuff is looking like
Model
class Storage extends ChangeNotifier {
static const _storage = FlutterSecureStorage();
static const _listKey = 'progress';
List _dataMaps = [];
List<DayTile> dayTileMain = [];
void createDataMap() {
for (int i = 1; i < 101; i++) {
final data = Data(number: i).toJson();
_dataMaps.add(data);
}
}
int get listLength {
return _dataMaps.length;
}
UnmodifiableListView get dataMaps {
return UnmodifiableListView(_dataMaps);
}
void pressed(Map<String, dynamic> map) async {
map['i'] = true;
await writeToStorage();
notifyListeners();
}
Future writeToStorage() async {
final value = json.encode(_dataMaps);
await _storage.write(key: _listKey, value: value);
}
Future<void> getTasks() async {
print('getTasks called');
final value = await _storage.read(key: _listKey);
final taskList = value == null ? null : List.from(jsonDecode(value));
if (taskList == null) {
print('getTasks is null');
createDataMap();
// createDayTiles();
} else {
print('getTasks is not null');
print(taskList);
_dataMaps = taskList;
// createDayTiles();
}
}
Future readFromStorage() async {
await getTasks();
notifyListeners();
}
}
Wrap Builder
class DayTiles extends StatelessWidget {
const DayTiles({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<Storage>(
builder: (_, storageData, __) => Wrap(
spacing: 13,
runSpacing: 13,
children: [
for(Map<String, dynamic> data in storageData.dataMaps)
DayTile(
number: data['n'],
onTap: () {
storageData.pressed(data);
},
isDone: data['i'],
),
],
),
);
}
}
and instead of using a wrap and listening for changes to it's children in the screen class, i just directly use the DayTiles() custom widget i created
I have the following Pop up which is essentially an AlertDialog with some data entry fields and two buttons. The widget tree is given below:
ProductPurchaseForm
class ProductPurchaseForm extends StatelessWidget {
ProductPurchaseForm({Key? key}) : super(key: key);
final _formGlobalKey = GlobalKey<FormBuilderState>();
#override
Widget build(BuildContext context) {
return AlertDialog(
scrollable: true,
content: FormBuilder(
key: _formGlobalKey,
child: Column(
children: [
const DateField(
name: 'date',
),
const TextInputField(
name: 'amount',
hintText: 'Enter the Amount',
),
FormFooter(
formKey: _formGlobalKey,
transaction: TransactionModel.productPurchase(
tDate: _formGlobalKey.currentState!.value['date'],
amount: _formGlobalKey.currentState!.value['amount'],
),
),
],
),
),
);
}
}
FormFooter
class FormFooter extends ConsumerWidget {
final TransactionModel transaction;
final GlobalKey<FormBuilderState> formKey;
const FormFooter({
Key? key,
required this.formKey,
required this.transaction,
}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final _databaseService = ref.watch(databaseServiceRiverpod);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CustomButton(
buttontext: 'Cancel',
buttonWidth: 300,
onTapFunc: () {
formKey.currentState!.reset();
VRouter.of(context).pop();
},
),
CustomButton(
buttontext: 'Add Transaction',
buttonWidth: 300,
onTapFunc: () => addTransactionFunction(
context: context,
transaction: transaction,
databaseService: _databaseService,
formKey: formKey,
),
),
],
);
}
}
addTransactionFunction
addTransactionFunction({
required BuildContext context,
required TransactionModel transaction,
required GlobalKey<FormBuilderState> formKey,
required DatabaseService databaseService,
}) async {
formKey.currentState!.save();
if (formKey.currentState!.validate()) {
final result = await databaseService.addTransaction(transaction);
formKey.currentState!.reset();
if (result.isError) {
debugPrint(result.message);
buildSnackBar(
context: context,
snackBarContent: 'ERROR: Please try again!',
);
} else {
VRouter.of(context).pop();
buildSnackBar(
context: context,
snackBarContent: 'SUCCESS: Transaction Added',
);
}
} else {
buildSnackBar(
context: context,
snackBarContent: 'Invalid Values',
);
}
}
As you can see, I am using a 3rd party package called flutter_form_builder and using a GlobalKey to get values from the FormBuilder. As I am passing the TransactionModel to the FormFooter, I use _formGlobalKey.currentState which does not have any value (or is null) at the time when the widget is first built. This is why I am getting the red screen error of Unexpected Null Value.
I know why this is happening and, technically, the TransactionModel.productPurchase is only "used" when the CustomButton is pressed and that is when I am sure that the values will NOT be null. But because of the way this widget tree is set up, the TransactionModel is referenced when the FormFooter is built rather than when it is clicked.
Is there a better way to set up my widget tree so that I can work around this null error?
So I know there are some similar questions about this issue but none of them worked for me. I have a ListView with different CheckboxListTiles and when I scroll down and choose an item, the ListView automatically jumps to the top. Is there a way to prevent this from happening? Thank you very much!
I've added a screenshot, so you can better understand.
This is my code:
class CheckboxWidget extends StatefulWidget {
const CheckboxWidget({
Key key,
this.item,
this.type,
this.state,
}) : super(key: key);
final Map<String, bool> item;
final String type;
final Map<String, dynamic> state;
#override
State<CheckboxWidget> createState() => _CheckboxWidgetState();
}
class _CheckboxWidgetState extends State<CheckboxWidget> {
#override
void initState() {
super.initState();
if (widget.state[widget.type].isEmpty) {
widget.item.updateAll((key, value) => value = false);
}
}
bool isChecked = false;
#override
Widget build(BuildContext context) {
final FilterProvider filterProvider = Provider.of<FilterProvider>(context);
return Expanded(
child: ListView(
key: UniqueKey(),
children: widget.item.keys.map(
(key) {
return CheckboxListTile(
contentPadding: EdgeInsets.only(left: 2, right: 2),
title: Text(
key,
style: TextStyle(fontSize: 19, fontWeight: FontWeight.w400),
),
value: widget.item[key],
activeColor: Color(0xffF6BE03),
checkColor: Color(0xff232323),
shape: CircleBorder(),
//contentPadding: EdgeInsets.all(0),
onChanged: (value) {
value
? filterProvider.multifiltervalue = [widget.type, key]
: filterProvider.multifiltervalue = [
widget.type,
key,
false
];
setState(
() {
widget.item[key] = value;
},
);
},
);
},
).toList(),
),
);
}
}
Probably because this line key: UniqueKey(), when you call setState the build function builds its widgets again, and the ListView will have a new UniqueKey so it will rebuild the list cause it thinks its a different widget
remove this line key: UniqueKey(), and it should work fine
CheckboxListTile is a stateless Widget... setState is redrawing the whole list.
You could wrap the CheckboxListTile into a Statefull Widget or into a StatefulBuilder ... if you call setState inside the StatefulBuilder only this part should be redrawed..
another way could be to save the scroll position... but i think redrawing only the part on screen you haved changed is smarter :-)
I have a list of reasons stored in my form controller using GetX
class formController extends GetxController {
String rejectreason1 = "Overheat";
List reasons = [
"Broken",
"Missing",
"Wet Wiring",
"Overheat",
"Orange",
"Obergene"
];
Widget Class:
class ReasonForm extends StatelessWidget {
formController formC = Get.put(formController());
ReasonForm(this.index, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
// TODO: implement build
return Form(
// key: formKey1,
child: Column(children: [
DropdownButtonFormField<String>(
decoration: const InputDecoration(icon: Icon(Icons.event)),
value: "Overheat",
icon: const Icon(Icons.arrow_downward),
style: const TextStyle(color: Colors.deepPurple),
items: formC.reasons.map((value) {
return DropdownMenuItem(
value: value.toString(),
child: Text(value),
);
}).toList(),
onChanged: (value) {
formC.rejectreason1 = value!;
print("Selected ${value}");
},
),
]),
);
}
}
and in the dropdown list with it being mapped when onChanged is called all the values can be stored and printed out but for some reason the item "Overheat" doesn't work. Other words such as "Others" also doesn't work either, but all the rest like orange, missing and others can be selected and printed out in the console.
I think its because you put value: "Overheat", so you can't change it because onTap function change formC.rejectreason1 but you dont put that value on your dropdown. Try to change it to value: formC.rejectreason1