I'm new to Flutter and I'm working on a localization feature using the easy_localizations package. I created a dropdown that shows languages you can switch to and it works perfectly fine when the dropdown is directly on the page. But since it will be on every page, I want to have the dropdown as a custom widget instead, and then just import it on the pages, like this:
const Padding(
padding: xPadding25,
child: DropDown(),
),
However, that does not work. I'm still able to click on the dropdown and choose a language, but it doesn't translate the pages anymore. I think it has to do something with the context it's translating, but I don't know how to make it so that it will take context of the pages and not its own if it makes sense.
Here's the code for the DropDown widget:
class DropDown extends StatefulWidget {
const DropDown({Key? key}) : super(key: key);
#override
State<DropDown> createState() => _DropDownState();
}
String dropdownValue = 'English';
class _DropDownState extends State<DropDown> {
#override
Widget build(BuildContext context) {
return (DropdownButton<String>(
icon: const Icon(
Icons.language,
color: scTealColor,
),
value: dropdownValue,
elevation: 16,
style: smBodyStyle,
underline: Container(
height: 2,
color: scTealColor,
),
onChanged: (String? newValue) {
setState(() {
dropdownValue = newValue ?? "";
if (dropdownValue == 'French') {
context.setLocale(const Locale('fr'));
} else {
context.setLocale(const Locale('en'));
}
});
},
items: <String>['English', 'French']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
));
}
}
Any ideas on how to implement it so that the translation works when imported to pages as a custom widget? Thanks!
Have you tried to pass context into your dropdown class ? for i.e.,
const Padding(
padding: xPadding25,
child: DropDown(context),
),
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),
)
],
);
}
}
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
I have a list of objects that I can display in a ListView. Now I wanted to implement a search feature and only display the search result. When I try to do it using onChanged on TextField(or even Controller) it doesn't work. I tried to debug and he gets the list updated correctly but he doesn't update the Widget. But when I removed the onChanged and added a button and then called the same method that I was calling on onChanged everything worked.
The goal is to update the widget as the user writes in the text field.
I would be happy to get some help
My full code :
import 'package:flutter/material.dart';
import 'package:hello_fridge/single_ingredient_icon.dart';
import 'package:string_similarity/string_similarity.dart';
import 'entities/ingredient.dart';
class IngredientsContainer extends StatefulWidget {
const IngredientsContainer({Key? key}) : super(key: key);
#override
_IngredientsContainerState createState() => _IngredientsContainerState();
}
class _IngredientsContainerState extends State<IngredientsContainer> {
late List<Ingredient> ingredients;
final searchController = TextEditingController();
#override
void dispose() {
// Clean up the controller when the widget is disposed.
searchController.dispose();
super.dispose();
}
void updateResults(String newValue) {
if (newValue.isEmpty) {
ingredients = Ingredient.getDummyIngredients();
} else {
print("new Value = $newValue");
ingredients = this.ingredients.where((ing) {
double similarity =
StringSimilarity.compareTwoStrings(ing.name, newValue);
print("$similarity for ${ing.name}");
return similarity > 0.2;
}).toList();
ingredients.forEach((element) {
print("found ${element.name}");
});
}
setState(() {});
}
Widget _searchBar(List<Ingredient> ingredients) {
return Row(
children: <Widget>[
IconButton(
splashColor: Colors.grey,
icon: Icon(Icons.restaurant),
onPressed: null,
),
Expanded(
child: TextField(
controller: searchController,
onChanged: (newValue) {
updateResults(newValue);
},
cursorColor: Colors.black,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.go,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 15),
hintText: "Search..."),
),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
icon: Icon(
Icons.search,
color: Color(0xff9ccc65),
),
onPressed: () {
updateResults(searchController.text);
},
),
),
],
);
}
#override
void initState() {
this.ingredients = Ingredient.getDummyIngredients();
super.initState();
}
#override
Widget build(BuildContext context) {
return Material(
child: Column(children: [
Expanded(flex: 1, child: _searchBar(this.ingredients)),
Expanded(flex: 4, child: IngredientsGrid(this.ingredients))
]),
);
}
}
class IngredientsGrid extends StatelessWidget {
List<Ingredient> ingredients;
IngredientsGrid(this.ingredients);
List<Widget> _buildIngredients() {
return this.ingredients.map((ing) => SingleIngredientIcon(ing)).toList();
}
// const IngredientsGrid({
// Key? key,
// }) : super(key: key);
#override
Widget build(BuildContext context) {
this.ingredients.forEach((ing) => print(ing.name! + ","));
return ListView(
children: <Widget>[
GridView.count(
crossAxisCount: 4,
// physics: NeverScrollableScrollPhysics(),
// to disable GridView's scrolling
shrinkWrap: true,
// You won't see infinite size error
children: _buildIngredients()),
// ...... other list children.
],
);
}
}
Moreover, I keep getting this Warning :
"Changing the content within the composing region may cause the input method to behave strangely, and is therefore discouraged. See https://github.com/flutter/flutter/issues/78827 for more details".
Visiting the linked GitHub page wasn't helpful
The problem is that while you are correctly filtering the list but your TextController is not getting assigned any value.
So, no value is getting assigned to your TextField as the initial value and hence the list again filters to have the entire list.
To solve this just assign the TextController the newValue like this.
void updateResults(String newValue) {
if (newValue.isEmpty) {
ingredients = Ingredient.getDummyIngredients();
} else {
print("new Value = $newValue");
ingredients = this.ingredients.where((ing) {
double similarity =
StringSimilarity.compareTwoStrings(ing.name, newValue);
print("$similarity for ${ing.name}");
return similarity > 0.2;
}).toList();
ingredients.forEach((element) {
print("found ${element.name}");
});
}
// change
searchController = TextEditingController.fromValue(
TextEditingValue(
text: newValue,
),
);
setState(() {});
}
If it throws an error then remove final from the variable declaration, like this :
var searchController = TextEditingController();
For a DropDown when I select any purticular option that value gets displayed in the dropDown.
How do I effectively change what is displayed once a purticular drop down menu item is clicked on.
As you can see from the below images. In the Brands Dropdown once I select an item its value gets displayed. However, I would like to change the value that is displayed.
How do I accomplish that? Thanks.
EDITED please pay attention on hint property and this.hintValue
You need to set State in onChanged event and associate value to new value grabbed from onchanged like this
onChanged: (String newValue) {
setState(() {
this.hintValue = newValue;
});
},
while:
return DropdownButton<String>(
value: dropdownValue,
hint: Text("${this.hintValue}"),
icon: Icon(Icons.arrow_downward),
iconSize: 24,
fullcode will be like this:
class DropDownWidget extends StatefulWidget {
DropDownWidget({Key key}) : super(key: key);
#override
_DropDownWidgetState createState() => _DropDownWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
class _DropDownWidgetState extends State<DropDownWidget> {
String dropdownValue = 'One';
String hintValue;
#override
Widget build(BuildContext context) {
return DropdownButton<String>(
value: dropdownValue,
hint: Text("${this.hintValue}"),
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: TextStyle(color: Colors.deepPurple),
underline: Container(
height: 2,
color: Colors.deepPurpleAccent,
),
onChanged: (String newValue) {
setState(() {
this.hintValue = newValue;
});
},
items: <String>['One', 'Two', 'Free', 'Four']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
}
}
reference from: flutter docs