Creating a Screen where I want to perform Flutter-FireBase Searching.But Visibility Toggle is not working as desired.
Desired Toggle Behaviour : When clicked on TextForm field , prefix icon and result card should be visible. Upon Clicking the Prefix Icon(Back Arrow) , Result List (Card) and the prefix icon itself should become invisible and TextField should unfocus .
Actual Behaviour : On clicking the prefix icon , Result set and prefix icon don't disappear , Prefix icon remains there and result set becomes invisible but occupies some space beneath the TextFormField
class AddAppointmentWidget extends StatefulWidget {
#override
_AddAppointmentWidgetState createState() => _AddAppointmentWidgetState();
}
class _AddAppointmentWidgetState extends State<AddAppointmentWidget> {
bool searchbartapped = false;
var queryResultSet = [];
var tempSearchStore = [];
// Search Function
initiateSearch(value) {
//body
}
#override
Widget build(BuildContext context) {
return ListView(
children: [
SizedBox(
height: 15,
),
Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Search',
style: TextStyle(fontSize: 35, fontWeight: FontWeight.bold)),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
flex: 5,
child: TextFormField(
style: TextStyle(color: Color(0xff2a2a2a), fontSize: 18),
keyboardType: TextInputType.name,
onChanged: (value) {
initiateSearch(value);
},
onTap: () {
setState(() {
searchbartapped = true;
});
},
cursorColor: Color(0xff2a2a2a),
cursorWidth: 1.5,
decoration: InputDecoration(
hintText: "Search by Name",
prefixIcon: Visibility(
visible: searchbartapped,
child: IconButton(
icon: Icon(Icons.arrow_back),
color: Colors.black54,
onPressed: () {
setState(() {
searchbartapped = !searchbartapped;
queryResultSet = [];
tempSearchStore = [];
});
FocusScope.of(context).unfocus();
}),
),
)),
),
],
),
),
Visibility(
visible: searchbartapped,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
padding: EdgeInsets.all(5.0),
primary: false,
shrinkWrap: true,
children: tempSearchStore.map((element) {
print(element['name']);
return buildResult(context, element);
}).toList()),
),
),
],
);
}
}
Note The buildResult widget is working perfectly fine.
Problem is only with the visibilty toggle
The issue: When you tap the prefixIcon:
onPressed is called, setting searchbartapped to false which is what you want.
The onTap method of your TextFormField is also called (since prefixIcon is inside it), setting searchbartapped to true.
So what you want is to prevent the second event from happening. I tried to prevent the notification from bubbling up the tree but I couldn't. So what I ended up doing is a bit more manual but works just as well.
Solution: Add a variable (for example hideSearchTapped) which is set to true when the prefixIcon is called. Then when the onTap method of your TextFormField is called, check this variable:
If hideSearchTapped is true, set it to false
Else change searchbartapped as you did
Here is a working example:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() async {
runApp(
MaterialApp(
home: Scaffold(
body: new AddAppointmentWidget(),
),
),
);
}
class AddAppointmentWidget extends StatefulWidget {
#override
_AddAppointmentWidgetState createState() => _AddAppointmentWidgetState();
}
class _AddAppointmentWidgetState extends State<AddAppointmentWidget> {
bool searchbartapped = false;
bool hideSearchTapped = false;
var queryResultSet = [];
var tempSearchStore = [];
// Search Function
initiateSearch(value) {
//body
}
#override
Widget build(BuildContext context) {
return ListView(
children: [
SizedBox(
height: 15,
),
Padding(
padding: const EdgeInsets.all(18.0),
child: Text('Search', style: TextStyle(fontSize: 35, fontWeight: FontWeight.bold)),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
flex: 5,
child: TextFormField(
style: TextStyle(color: Color(0xff2a2a2a), fontSize: 18),
keyboardType: TextInputType.name,
onChanged: (value) {
initiateSearch(value);
},
onTap: () {
setState(() {
if (hideSearchTapped) {
hideSearchTapped = false;
} else {
searchbartapped = true;
}
});
},
cursorColor: Color(0xff2a2a2a),
cursorWidth: 1.5,
decoration: InputDecoration(
hintText: "Search by Name",
prefixIcon: Visibility(
visible: searchbartapped,
child: IconButton(
icon: Icon(Icons.arrow_back),
color: Colors.black54,
onPressed: () {
hideSearchTapped = true;
searchbartapped = !searchbartapped;
queryResultSet = [];
tempSearchStore = [];
setState(() {
});
FocusScope.of(context).unfocus();
return true;
}),
),
)),
),
],
),
),
Visibility(
visible: searchbartapped,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
padding: EdgeInsets.all(5.0),
primary: false,
shrinkWrap: true,
children: tempSearchStore.map((element) {
print(element['name']);
}).toList()),
),
),
],
);
}
}
Note: you should use lowerCamelCase to name your variable. So searchbartapped would become searchBarTapped.
Related
Good morning, all, I've a problem. I've a text Form Field in my application when I tapped on it to type, the keyboard didn't show this video explain what happened exactly
I read questions that have same problem and try to do its answer but no way
solutions that I tried it.
1- convert the stateless to stateful and the oppsite
2- declare controller global (after imports - in cubit file - in shared file)
3-don't use Form Widget
4- don't use onFieldSubmitted properity
5- try to run flutter run --release but nothing shown in terminal
6- check crashes in google play console
probably I tried most of answers, but no one is working for me,
this is the code of my search screen
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({Key? key}) : super(key: key);
#override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
var width = size.width;
return BlocConsumer<HomeCubit, HomeStates>(
listener: (context, state) {},
builder: (context, state) {
var cubit = HomeCubit.get(context);
return Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Row(
children: [
// which zone text
SizedBox(
width: width * .30,
child: FittedBox(child: Text('اختر المنطقة',style: TextStyle(fontSize: 24,fontWeight:FontWeight.normal))))
const SizedBox(width: 25.0),
Expanded(
child: Center(
child: DropdownButton(
alignment: Alignment.center,
value: cubit.zonePopupValue,
hint: const Text('كل المناطق', textAlign: TextAlign.center),
style: TextStyle(
color: HexColor('#ECB365'),
fontSize: 24,
fontWeight: FontWeight.normal,
),
items: cubit.list,
onChanged: (value) => cubit.changePopupZoneValue(int.parse(value.toString())),
),
),
),
],
)
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextFormField(
textInputAction: TextInputAction.search,
onFieldSubmitted: (_) async => cubit.search(),
controller: cubit.searchController,
keyboardType: TextInputType.text,
maxLines: 1,
maxLength: 100,
textAlign: TextAlign.right,
textDirection: TextDirection.rtl,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(right: 20, left: 10),
labelText: "إبحث",
alignLabelWithHint: true,
labelStyle: TextStyle(
color: HexColor('#ECB365'),
fontSize: 24,
fontWeight: FontWeight.normal,
),
suffixIcon: cubit.isLoadingAuth? CircularProgressIndicator(color: HexColor('#ECB365')): IconButton(
onPressed: () => cubit.search(),
icon: Icon(Icons.search,color: HexColor('#ECB365')),
iconSize: 35,
color: HexColor('#081C31'),
),
border: OutlineInputBorder(borderSide:BorderSide(color: HexColor('#ECB365'))),
focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: HexColor('#ECB365'))),
enabledBorder: OutlineInputBorder(borderSide:BorderSide(color: HexColor('#ECB365'))),
fillColor: HexColor('#ECB365')
),
)
),
Container(
margin: const EdgeInsets.only(left: 10.0, right: 20.0),
child: const Divider(color: Colors.black,height: 36)
),
cubit.responseBodySearch == null
? const Center(child: Text("أبدأ البحث"))
: cubit.responseBodySearch.isEmpty
? const Center(child: Text("غير متوفر حاليا"))
: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: cubit.responseBodySearch!.length,
itemBuilder: (context, index) => buildSearchItem(cubit, context, index)
)
],
),
),
)
);
}
);
}
}
this is the code of shared component
class HomeCubit extends Cubit<HomeStates> {
HomeCubit() : super(HomeInitState());
static HomeCubit get(context) => BlocProvider.of(context);
String api = 'https://hadayekhof.com/dalel/api/';
var searchController = TextEditingController();
int? zonePopupValue;
void changePopupZoneValue(int value) {
zonePopupValue = value;
getCategoriesData(zoneIds[value].toString());
emit(HomeChangePopupButtonState());
}
var responseBodySearch;
Future search() async {
emit(HomeSearchLoadingState());
responseBodySubCategories = null;
var data = {
'zone_id': zonePopupValue == null ? '' : zoneIds[zonePopupValue!].toString(),
// 'parent_cat': categoryPopupValue == null ? '' : categoryIds[categoryPopupValue!].toString(),
'term': searchController.text.toString()
};
var uri = Uri.parse('${api}stores/searchStores');
var header = {'Authorization': 'Bearer $ciphertext'};
if (searchController.text == '') {
Fluttertoast.showToast(msg: 'من فضلك اكتب شئ');
} else {
await http.post(uri, body: data, headers:header ).then((value) async {
responseBodySearch = await jsonDecode(value.body);
if (value.body.toString().contains('[') == false) {
responseBodySearch = null;
Fluttertoast.showToast(msg: 'no stores found');
}
emit(HomeSearchSuccessState());
}).catchError((error) {
debugPrint("search error is : $error");
emit(HomeSearchErrorState());
});
}
}
}
how can I solve this problem?
finally, I solve It by write this code in the path android\app\src\main\AndroidManifest.xml
<application
...
android: labelwareAccelerated="true"
...>
and this
<activity
...
android:hardwareAccelerated="true"
...>
It's working now
Recently implemented a tagForm widget at "+" button press, I want to delete those widgets now at "delete" button press, but right now, even when I press the "delete" button, nothing happens.
How can I solve this?
Any help appreciated!
code:
import 'package:flutter/material.dart';
import '../database/firestoreHandler.dart';
import '../models/todo2.dart';
import '../widgets/dialogs.dart';
class TodoEdit extends StatefulWidget {
String? doctitle;
String? doctdescription;
String? docimage;
String? docid;
List? doctags;
TodoEdit({Key? key, this.doctitle, this.doctdescription, this.docimage, this.docid,this.doctags}) : super(key: key);
#override
_TodoEditState createState() => _TodoEditState();
}
class _TodoEditState extends State<TodoEdit> {
final _formKey = GlobalKey<FormState>();
final tcontroller = TextEditingController();
final dcontroller = TextEditingController();
final icontroller = TextEditingController();
var textEditingControllers = <TextEditingController>[];
//-----------------the list where the form is stored----------
var textformFields = <Widget>[];
void _addformWidget(controller) {
setState(() {
textformFields.add(tagForm(controller));
});
}
//------------------------------------------------------------------------
Widget tagForm(controller){
return TextFormField(
controller: controller,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: "Tag",
labelStyle: TextStyle(color: Colors.white60),
fillColor: Colors.black,
filled: true,
suffixIcon: IconButton(
icon:Icon(Icons.delete, color: Colors.white,),
//--------------------- doesn't work?-------------------
onPressed: (){
setState(() {
textformFields.remove(tagForm(controller));
});
},
--------------------------------------------------------------
)
),
);
}
//-----------------------------------------------------------
#override
void initState() {
super.initState();
tcontroller.text = widget.doctitle.toString();
dcontroller.text = widget.doctdescription.toString();
icontroller.text = widget.docimage.toString();
widget.doctags?.forEach((element) {
var textEditingController = new TextEditingController(text: element);
textEditingControllers.add(textEditingController);
//return textformFields.add(tagForm(textEditingController)
return _addformWidget(textEditingController);
//);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[900],
appBar: AppBar(
actions: [
IconButton(onPressed: (){
showDialog(
barrierDismissible: false,
context: context,
builder: (context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text('Delete TODO'),
actions: [
TextButton(
child: Text('Cancel'),
onPressed: () {
Navigator.pop(context);
},
),
TextButton(
child: Text('Delete'),
onPressed: () {
deleteData(widget.docid.toString(), context);
setState(() {
showSnackBar(context, 'todo "${widget.doctitle}" successfully deleted!');
});
},
),
],
);
},
);
},
icon: Icon(Icons.delete))
],
backgroundColor: Colors.grey[900],
title: Text("${widget.doctitle}"),
),
body: Container(
child: SafeArea(
child: Form(
key: _formKey,
child: Column(
children: [
SizedBox(height: 10),
TextFormField(
controller: tcontroller,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: "Title",
labelStyle: TextStyle(color: Colors.white60),
fillColor: Colors.black,
filled: true,
),
),
SizedBox(height: 10),
TextFormField(
controller: dcontroller,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: "Description",
labelStyle: TextStyle(color: Colors.white60),
fillColor: Colors.black,
filled: true,
),
),
SizedBox(height: 10),
TextFormField(
controller: icontroller,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: "Image url",
labelStyle: TextStyle(color: Colors.white60),
fillColor: Colors.black,
filled: true,
),
),
SizedBox(height: 10),
Row(children: [
Text("Tags:", style:TextStyle(color: Colors.white)),
IconButton(onPressed: (){
var textEditingController = new TextEditingController(text: "tag");
textEditingControllers.add(textEditingController);
_addformWidget(textEditingController);
print(textformFields.length);
},
icon: Icon(Icons.add,color: Colors.white,),
)
],),
/*SingleChildScrollView(
child: new Column(
children: textformFields,
)
),*/
Expanded(
child: SizedBox(
height: 200.0,
child: ListView.builder(
itemCount: textformFields.length,
itemBuilder: (context,index) {
return textformFields[index];
}),
)
),
],
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: (){
List<String> test = [];
textEditingControllers.forEach((element) {
test.add(element.text);
});
if(tcontroller == '' && dcontroller == '' && icontroller == ''){
print("not valid");
}else{
var todo = Todo2(
title: tcontroller.text,
description: dcontroller.text,
image: icontroller.text,
tags: test,
);
updateData(todo, widget.docid.toString(),context);
setState(() {
showSnackBar(context, 'todo ${widget.doctitle} successfully updated!');
});
}
},
child: Icon(Icons.update),
),
);
}
}
You can't remove anything from the list with objects from tagForm(controller), because these objects are newly created and therefore not the same as in the list (as long as the == operator is not overwritten)
If you still want to have the widgets in a list instead of just storing the controllers and without having to change much, you could remove the widgets like this:
onPressed: (){
setState(() {
controller.dispose();
textEditingControllers.remove(controller);
textformFields.removeWhere((w) => w.controller = controller));
});
},
and change the type of your List: var textformFields = <TextFormField>[]; and of the method TextFormField tagForm(controller).
In general, you can of course optimize the state management, but with this solution it should work for now.
Dont't store Widget, it is bad way. Insteads store there property, render by List then remove by index when you need.
ps: some code syntax can wrong, i write this on browser.
class _TodoEditState extends State<TodoEdit> {
...
var textformFields = <String>[];
...
void _addformWidget([String? initValue]) {
setState(() => textformFields.add(initValue ?? ""));
}
...
Widget tagForm(String value, void Function(String) onChange, void Function() onRemove){
var openEditor = () {
// Open dialog with text field to edit from [value] call onChange with
// new value
OpenDialog().then((newvalue) {
if(newvalue != null) onChange(newvalue);
}
};
var delete = () {
// Open confirm dialog then remove
OpenConfirmDialog("your message").then((continue) {
if(continue) onRemove();
});
};
return InkWell(
onTap: openEditor,
child: Text(value), // render your tag value
);
}
...
#override
void initState() {
...
textformFields = List.filled(widget.doctags ?? 0, ""); // or List.generate/map if you want replace by own value.
}
...
#override
Widget build(BuildContext context) {
...
ListView.builder(
itemCount: textformFields.length,
itemBuilder: (context,index) => tagForm(
textformFields[index],
(newvalue) => setState(() => textformFields[index] = newvalue),
() => setState(() => textformFields = textformFields..removeAt(index));,
),
),
...
);
}
I have a Form in flutter, containing a column with rows with textFormFields.
Each row has a iconButton at the end for deleting said row. The rows is built with a Consumer.
I have a notifier class containing data that the consumer listens for.
When the delete button is pressed a function in the notifier class is called, responsible for deleting the row with the given index. The same function is printing out the dataset befor deletion and again after deletion. The print looks good, the result is as expected.
The Consumer also rebuilds the dataset with one row less. But the data that is presented in the ui is not as expected.
In the Widget that is responsible for building the rows I print out the data that is put in the initialValue of the textFormField. The print looks good, again as expected. But the ui does not render what the print is showing.
See pictures and code.
Ready for deleting the second row:
After deletion:
Code that deletes the row and prints the data.
void deletePoint({int switchpointIndex, int pointIndex, Point point}) {
for (Switchpoint switchpoint in _settings.switchpoints) {
for (Point point in switchpoint.points) {
print("Tag: ${point.tag}. And Type: ${point.type}");
}
}
_settings.switchpoints[switchpointIndex].points.removeAt(pointIndex);
for (Switchpoint switchpoint in _settings.switchpoints) {
for (Point point in switchpoint.points) {
print("Tag: ${point.tag}. And Type: ${point.type}");
}
}
notifyListeners();
}
Result of the print (as expected):
Tag: a. And Type: a
Tag: b. And Type: b
Tag: c. And Type: c
Tag: a. And Type: a
Tag: c. And Type: c
Code that prints and renders the data in the row Widget:
#override
Widget build(BuildContext context) {
print("PointIndex: $pointIndex, value: ${point.tag} and ${point.type}");
return Row(
...
TextFormField(
...
initialValue: point.tag ?? ''
...
TextFormField(
...
initialValue: point.type ?? ''
...
This also prints the expected:
PointIndex: 0, value: a and a
PointIndex: 1, value: c and c
Anyone who understands whats going on?
-- EDIT --
No TextEditingController.
Form code:
return Container(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 50,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Switchpoint Layout',
style: TextStyle(
color: Colors.teal,
fontSize: 20,
fontWeight: FontWeight.bold),
),
TextButton.icon(
onPressed: () {
context
.read<SettingsNotifier>()
.addEmptySwitchpoint();
},
icon: Icon(Icons.add),
label: Text('Add Switchpoint')),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Consumer<SettingsNotifier>(
builder: (context, notifier, child) {
List<SwitchpointRow> switchpointRows = [];
int switchpointIndex = 0;
for (Switchpoint switchpoint
in notifier.switchPoints) {
switchpointRows.add(SwitchpointRow(
switchpoint: switchpoint,
switchpointIndex: switchpointIndex,
formKey: _formKey,
));
switchpointIndex++;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: switchpointRows),
],
);
}),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
onPressed: () async {
print(context
.read<SettingsNotifier>()
.settings
.toJson());
if (_formKey.currentState.validate()) {}
},
child: Text('Submit'),
),
),
],
),
),
);
SwitchpointRow code:
class SwitchpointRow extends StatelessWidget {
const SwitchpointRow({
Key key,
#required this.switchpoint,
#required this.switchpointIndex,
#required this.formKey,
}) : super(key: key);
final int switchpointIndex;
final Switchpoint switchpoint;
final formKey;
List<PointRow> loadPoints(List<Point> points) {
List<PointRow> pointRows = [];
int pointIndex = 0;
for (Point point in points) {
pointRows.add(PointRow(
point: point,
switchpointIndex: switchpointIndex,
pointIndex: pointIndex,
last: points.last == point ? true : false));
pointIndex++;
}
return pointRows;
}
#override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
backgroundColor: Colors.amberAccent,
title: TextFormField(
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.fromLTRB(0, 2, 0, 4),
isDense: true,
hintText: 'Switchpoint name',
labelText: 'Switchpoint name',
),
initialValue: switchpoint.name ?? '',
onChanged: (value) {
context
.read<SettingsNotifier>()
.switchPoints[switchpointIndex]
.name = value;
},
validator: (value) {
if (value.isEmpty) {
return 'mandatory';
}
return null;
}),
actions: [
Padding(
padding: const EdgeInsets.only(right: 18.0),
child: TextButton.icon(
onPressed: () {
context.read<SettingsNotifier>().deleteSwitchpoint(
switchpointIndex: switchpointIndex);
},
icon: Icon(Icons.delete),
label: Text('Delete Switchpoint'),
),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: TextButton.icon(
onPressed: () {
context
.read<SettingsNotifier>()
.addPoint(switchpointIndex);
},
icon: Icon(Icons.add),
label: Text('Point'),
),
),
],
),
Padding(
padding: const EdgeInsets.only(left: 50.0),
child: Column(children: loadPoints(switchpoint.points)),
),
],
),
),
);
}
}
PointRow code:
class PointRow extends StatelessWidget {
const PointRow(
{Key key,
#required this.point,
#required this.switchpointIndex,
#required this.pointIndex,
#required this.last})
: super(key: key);
final Point point;
final int pointIndex;
final int switchpointIndex;
final bool last;
#override
Widget build(BuildContext context) {
print("PointIndex: $pointIndex, value: ${point.tag} and ${point.type}");
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
flex: 3,
child: TextFormField(
decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 2, 0, 4),
isDense: true,
hintText: 'Tag',
labelText: 'Point tag',
),
onChanged: (value) {
context
.read<SettingsNotifier>()
.switchPoints[switchpointIndex]
.points[pointIndex]
.tag = value;
},
initialValue: point.tag ?? '',
validator: (value) {
if (value.isEmpty) {
return 'mandatory';
}
return null;
}),
),
SizedBox(width: 30.0),
Expanded(
flex: 3,
child: TextFormField(
decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 2, 0, 4),
isDense: true,
hintText: 'Type',
labelText: 'Type of point',
),
onChanged: (value) {
context
.read<SettingsNotifier>()
.switchPoints[switchpointIndex]
.points[pointIndex]
.type = value;
},
initialValue: point.type ?? '',
),
),
Expanded(
child: IconButton(
padding: EdgeInsets.all(0.0),
icon: Icon(Icons.delete),
onPressed: () {
context.read<SettingsNotifier>().deletePoint(
switchpointIndex: switchpointIndex,
pointIndex: pointIndex,
point: point);
},
),
)
],
);
}
}
The whole Form picture:
So, after some trial and error the solution had to be a TextEditingController which I wanted to avoid since eventually there will be several fields.
So the solution is a inline controller like this:
TextFormField(
...
controller: TextEditingController()..text = 'the value',
...
);
In this way the UI behaves as I want.
I'm trying to create a dynamic form so I used the idea of using a listview builder to create it. I was able to successfully create it but I faced that I cannot discard changes made to the form by popping it off after editing it. The two textFormField Job name and rate per hour were able to discard changes as they were using onsaved but on the checkbox I can't do that as it has onChanged which wraps setstate to change its state.
You can take a look at the video at this link to see how it functions as of now - https://vimeo.com/523847256
As you can see that it is retaining the data even after popping the page and coming back which I don't want it to. I'm looking for a way to prevent that and make the form the same as before if the user didn't press save.
I have tried to reassign the variables() in onpressed of back button but that didn't work. I also tried push replacement to the same page to reset it but that also didn't work. I think the cuprit here is the sublist and the initialValueTextFormField and initialValueCheckbox which are used declared under ListView.builder but I don't know how to fix that without affecting the dynamic list functionality.
class EditJobPage extends StatefulWidget {
const EditJobPage({Key key, this.job}) : super(key: key);
final Job job;
static Future<void> show(BuildContext context, {Job job}) async {
await Navigator.of(context, rootNavigator: true).pushNamed(
AppRoutes.editJobPage,
arguments: job,
);
}
#override
_EditJobPageState createState() => _EditJobPageState();
}
class _EditJobPageState extends State<EditJobPage> {
final _formKey = GlobalKey<FormState>();
String _name;
int _ratePerHour;
List<dynamic> _subList = [];
Set newSet = Set('', false);
#override
void initState() {
super.initState();
if (widget.job != null) {
_name = widget.job?.name;
_ratePerHour = widget.job?.ratePerHour;
_subList = widget.job?.subList;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 2.0,
title: Text(widget.job == null ? 'New Job' : 'Edit Job'),
leading: IconButton(
icon: Icon(Icons.clear),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: <Widget>[
FlatButton(
child: const Text(
'Save',
style: TextStyle(fontSize: 18, color: Colors.white),
),
onPressed: () => _submit(),
),
],
),
body: _buildContents(),
backgroundColor: Colors.grey[200],
);
}
Widget _buildContents() {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _buildForm(),
),
),
),
);
}
Widget _buildForm() {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildFormChildren(),
),
);
}
List<Widget> _buildFormChildren() {
print(_subList);
return [
TextFormField(
decoration: const InputDecoration(labelText: 'Job name'),
keyboardAppearance: Brightness.light,
initialValue: _name,
validator: (value) =>
(value ?? '').isNotEmpty ? null : 'Name can\'t be empty',
onChanged: (value) {
setState(() {
_name = value;
});
},
),
TextFormField(
decoration: const InputDecoration(labelText: 'Rate per hour'),
keyboardAppearance: Brightness.light,
initialValue: _ratePerHour != null ? '$_ratePerHour' : null,
keyboardType: const TextInputType.numberWithOptions(
signed: false,
decimal: false,
),
onChanged: (value) {
setState(() {
_ratePerHour = int.tryParse(value ?? '') ?? 0;
});
},
),
Column(
children: <Widget>[
ListView.builder(
shrinkWrap: true,
itemCount: _subList?.length ?? 0,
itemBuilder: (context, index) {
String initialValueTextFormField =
_subList[index].subListTitle.toString();
bool initialValueCheckbox = _subList[index].subListStatus;
return Row(
children: [
Checkbox(
value: initialValueCheckbox,
onChanged: (bool newValue) {
setState(
() {
initialValueCheckbox = newValue;
_subList.removeAt(index);
_subList.insert(
index,
Set(initialValueTextFormField,
initialValueCheckbox));
},
);
},
),
Expanded(
child: TextFormField(
minLines: 1,
maxLines: 1,
initialValue: initialValueTextFormField,
autofocus: false,
textAlign: TextAlign.left,
onChanged: (title) {
setState(() {
initialValueTextFormField = title;
_subList.removeAt(index);
_subList.insert(
index,
Set(initialValueTextFormField,
initialValueCheckbox));
});
},
decoration: InputDecoration(
border: UnderlineInputBorder(),
labelStyle: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
),
filled: true,
hintText: 'Write sub List here',
),
),
),
],
);
},
),
TextButton(
onPressed: () {
setState(() {
_subList.add(newSet);
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.add),
Text('Add Sub Lists'),
],
),
),
],
),
];
}
void _submit() {
final isValid = _formKey.currentState.validate();
if (!isValid) {
return;
} else {
final database = context.read<FirestoreDatabase>(databaseProvider);
final id = widget.job?.id ?? documentIdFromCurrentDate();
final job = Job(
id: id,
name: _name ?? '',
ratePerHour: _ratePerHour ?? 0,
subList: _subList);
database.setJob(job);
Navigator.of(context).pop();
}
}
}
And this is the link to the full repository of the whole flutter app in case you want to look at any other part:- https://github.com/brightseagit/dynamic_forms . Thank you.
Note - This is the edited code of this repo - https://github.com/bizz84/starter_architecture_flutter_firebase.
When assigning the list we need to use _subList = List.from(widget.job.subList) instead of _subList = widget.job.subList.
Otherwise, the changes made in _subList will also be made in job.subList .
i want to change the indexvalue (pictogramindex) of one page when we click nextbutton on another screen.I will explain briefly , I have 2 screens in my scenario the first screen contains an image and it's name , a textfield and nextbutton (i have provided a dummy data contains a list of image and it's names) the logic behind this is , when we complete the textfield box and click next button(after validate) the textfield value checks with the correctvalue which i was given in the dummy data and show it's synonym which also provided. when we click the next button we will go to another page which contains the correct answer(passed from first page) and a textfield in this the user can write about the correct answer ( validated) when click next button in this page (till this my applicationworks perfectly) i want to load the first page with it's index updated (+1) which i initialised as 0 (var pictogramindex=0). But in my case when coming back to first page the index is not updating it will automatically stores the initialised value. what i want is i want to update index on the first page when i click next button in the Second page .
my source code of first screen is shown here
class Pictogramscreen extends StatefulWidget {
final int length;
const Pictogramscreen({Key key, this.length}) : super(key: key);
#override
_PictogramscreenState createState() => _PictogramscreenState();
}
class _PictogramscreenState extends State<Pictogramscreen> {
#override
final _Key = GlobalKey<FormState>();
Color defaultcolor = Colors.blue[50];
Color trueColor = Colors.green;
Color falseColor = Colors.red;
Widget defcorrect = Text('');
var pictogramindex = 0;
TextEditingController usertitleInput = TextEditingController();
nextPictogram() {
setState(() {
pictogramindex++;
});
}
fillColor() {
setState(() {
usertitleInput.text == pictdata[pictogramindex]['pictcorrectword']
? defaultcolor = trueColor
: defaultcolor = falseColor;
});
}
correctText() {
setState(() {
usertitleInput.text == pictdata[pictogramindex]['pictcorrectword']
? defcorrect = Text(pictdata[pictogramindex]['pictsynonym'])
: defcorrect = Text(pictdata[pictogramindex]['pictcorrectword']);
});
}
reset() {
setState(() {
defaultcolor = Colors.blue[50];
defcorrect = Text('');
usertitleInput.clear();
});
}
void description(BuildContext ctx) {
Navigator.of(context).pushNamed('/user-description', arguments: {
'id': pictdata[pictogramindex]['pictid'],
'word': pictdata[pictogramindex]['pictcorrectword']
});
}
Widget build(BuildContext context) {
int length = pictdata.length;
return Scaffold(
body: pictogramindex < pictdata.length
? ListView(
children: <Widget>[
Container(
margin: EdgeInsets.only(top: 20),
padding: EdgeInsets.all(15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Card(
margin: EdgeInsets.only(top: 20),
child: Image.network(
pictdata[pictogramindex]['pictimg']),
),
SizedBox(
height: 10,
),
Text(
pictdata[pictogramindex]['pictword'],
style: TextStyle(
fontSize: 25,
),
),
SizedBox(
height: 10,
),
//Card(
//color: Colors.blue,
// child: TextField(
// decoration: InputDecoration.collapsed(
// hintText: 'type here'),
//textAlign: TextAlign.center,
// onSubmitted: (value) {
// usertitleInput = value;
// print(usertitleInput);
// },
// ),
//),
Form(
key: _Key,
child: TextFormField(
controller: usertitleInput,
validator: (usertitleInput) {
if (usertitleInput.isEmpty) {
return 'Answer cannot be empty';
} else {
return null;
}
},
textAlign: TextAlign.center,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Colors.blueAccent),
borderRadius: BorderRadius.all(
Radius.circular(15),
)),
labelText: 'Type your Answer',
filled: true,
fillColor: defaultcolor,
),
onFieldSubmitted: (value) {
usertitleInput.text = value;
fillColor();
correctText();
print(usertitleInput.text);
}),
),
SizedBox(
height: 10,
),
defcorrect,
SizedBox(
height: 10,
),
RaisedButton(
onPressed: () {
if (_Key.currentState.validate()) {
description(context);
// nextPictogram();
reset();
}
//
//if (_Key.currentState.validate() == correctText()) {
// nextPictogram;
// }
},
child: Text('Next'),
)
],
),
),
],
)
: Center(
child: Text('completed'),
));
}
}
my source code of the second screen is show here
class Userinputscreen extends StatefulWidget {
final String id;
final String word;
const Userinputscreen({Key key, this.id, this.word}) : super(key: key);
#override
_UserinputscreenState createState() => _UserinputscreenState();
}
class _UserinputscreenState extends State<Userinputscreen> {
final _Keey = GlobalKey<FormState>();
TextEditingController userdescription = TextEditingController();
var pictogramindex;
void nextpict(BuildContext context) {
Navigator.of(context).pushNamed('/main-screen');
}
// void nextpict(BuildContext context, int index) {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (ctx) => Pictogramscreen(
// index: i = 0,
// )));
// }
#override
Widget build(BuildContext context) {
final routeArgs =
ModalRoute.of(context).settings.arguments as Map<String, String>;
final correctWord = routeArgs['word'];
return MaterialApp(
home: Scaffold(
body: ListView(children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 50),
child: Center(
child: Container(
padding: EdgeInsets.all(20),
margin: EdgeInsets.only(top: 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
correctWord,
style: TextStyle(fontSize: 26),
),
SizedBox(
height: 10,
),
Form(
key: _Keey,
child: TextFormField(
controller: userdescription,
validator: (userdescription) {
if (userdescription.isEmpty) {
return 'Answer cannot be empty';
} else {
return null;
}
},
textAlign: TextAlign.center,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueAccent),
borderRadius: BorderRadius.all(
Radius.circular(15),
)),
labelText: 'Type your Answer',
filled: true,
),
onFieldSubmitted: (value) {
userdescription.text = value;
print(userdescription.text);
}),
),
SizedBox(
height: 10,
),
RaisedButton(
onPressed: () {
if (_Keey.currentState.validate()) {
nextpict(context);
}
},
child: Text('Next'),
)
],
),
),
),
),
])),
);
}
}
If I get it right, you basically want to tell the initial page that it's state is updated(the index) elsewhere. You basically need to make your app "reactive".
As is said in Google Developers Tutorial:
One of the advantages of Flutter is that it uses reactive views, which you can take to the next level by also applying reactive principles to your app’s data model.
Use some sort of state management. You need to choose from and use either Bloc, InheritedWidget and InheritedModel, Provider(ScopedModel), or the like.
Check this article on flutter about state management, or this for a complete list of approaches