Description:
I've encountered an issue in my Flutter application where tapping on a TextField
is causing an unexpected widget rebuild. This behavior is leading to some performance concerns and is not ideal for the user experience.
I've noticed that every time I interact with the TextField
, the widget tree appears to be rebuilding. This behavior is not something I would expect, and it's causing unnecessary overhead in my application.
I've attached an image [if possible, include an image showing the widget tree being rebuilt when tapping the TextField
] to illustrate this behavior.
I'm seeking advice on how to address this issue and prevent unnecessary rebuilds when interacting with the TextField
. Is there a specific way to handle TextField
interactions to avoid this problem?
There's relevant code for the described problem
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:logistics_flutter_client/component/go_flemingo_button.dart';
import 'package:logistics_flutter_client/component/select_hour.dart';
import 'package:logistics_flutter_client/provider_models/booking_form_provider.dart';
import 'package:logistics_flutter_client/provider_models/schedule_model_provider.dart';
import 'package:logistics_flutter_client/provider_models/user_model.dart';
import 'package:logistics_flutter_client/provider_models/vehicle_id_provider.dart';
import 'package:logistics_flutter_client/utils/network_repository.dart';
import 'package:marquee/marquee.dart';
import 'package:provider/provider.dart';
class VehicleDetailsForm extends StatefulWidget {
const VehicleDetailsForm({super.key});
@override
State<VehicleDetailsForm> createState() => _VehicleDetailsFormState();
}
class _VehicleDetailsFormState extends State<VehicleDetailsForm> {
static final _formKey = GlobalKey<FormState>();
List? vehicleList = [];
TextEditingController tonnageController = TextEditingController();
TextEditingController vehicleNumberController = TextEditingController();
TextEditingController driverNameController = TextEditingController();
TextEditingController driverNumberController = TextEditingController();
double selectedRating = 0;
bool isFetchMemberCalled = false;
late Map<String, dynamic>? userData; // Declare it here
@override
void initState() {
super.initState();
userData = Provider.of<UserModel>(context, listen: false).userData;
if (!isFetchMemberCalled) {
// Add this check
fetchMemberDetails(userData!['id']);
isFetchMemberCalled = true;
}
}
Future fetchMemberDetails(int id) async {
try {
final apiResponse = await networkRepository.getMember(memberId: id);
debugPrint('\x1b[97m App Details Response : $apiResponse');
setState(() {
//selectedScheduleList = selectedSchedule;
vehicleList = apiResponse['vehicle_set'];
});
// Process the apiResponse as needed
} catch (e) {
Fluttertoast.showToast(
msg: e.toString(),
backgroundColor: Colors.red,
);
}
}
String? selectedVehicleNumber;
@override
Widget build(BuildContext context) {
if (vehicleList != null && vehicleList!.isNotEmpty) {
double width = MediaQuery.of(context).size.width;
final vehicleIdProvider = Provider.of<VehicleIdProviderModel>(context,listen: false);
final scheduleModel = Provider.of<ScheduleProviderModel>(context,listen: false);
String source = scheduleModel.selectedSchedule!['source'];
String plant = scheduleModel.selectedSchedule!['plant'];
double contractRate= scheduleModel.selectedSchedule!['contract_rate'];
String formattedAmount = NumberFormat.currency(
locale: 'en_IN', // 'en_IN' for Indian English
symbol: '₹ ', // Indian Rupee symbol
decimalDigits: 0,
).format(contractRate);
String unit = scheduleModel.selectedSchedule!['unit'];
String goods = scheduleModel.selectedSchedule!['goods_type'];
double quantity = scheduleModel.selectedSchedule!['rem_qty'];
TextEditingController freightRateController =
TextEditingController(text: contractRate.toString());
String remQty =
'${quantity.toString()} $unit of $goods ($formattedAmount / $unit)';
// Accessing the user data
final bookingData = Provider.of<BookingFormDataModel>(context);
void handleRatingUpdate(double rating) {
selectedRating = rating;
}
bool validateIndianVehicleNumber(String vehicleNumber) {
// Regular expression pattern for Indian vehicle numbers
String pattern = r'^[A-Z]{2}\s?[0-9]{2}\s?[A-Z]{1,2}\s?[0-9]{4}$';
RegExp regex = RegExp(pattern);
return regex.hasMatch(vehicleNumber.toUpperCase());
}
Widget cardData({
required IconData iconData,
required String labelText,
required String data,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0, vertical: 12),
child: Row(
children: [
Icon(
iconData,
color: const Color(0xFF44BFF7),
size: 20,
),
const SizedBox(
width: 10,
),
Text(
labelText,
style: const TextStyle(fontWeight: FontWeight.w500),
),
const Spacer(),
SizedBox(
width: 200, // Adjust the width as needed
child: SizedBox(
height: 20,
child: Marquee(
text: data,
velocity: 25,
numberOfRounds: 1,
blankSpace: 55.0,
style: const TextStyle(
fontWeight: FontWeight.w500,
// Align the text to the right
),
),
),
),
],
),
);
}
Future<int> addVehicle(String vehicleNum, List vehicleList) async {
var data = await networkRepository.postVehicleDetails(
vehicleNumber: vehicleNum, memberId: userData!['id']);
return data['body']['id'];
}
Future<int> findVehicleId(
List vehicleList, String selectedVehicleNumber) async {
List vehicleIsolated = [];
for (var i = 0; i < vehicleList.length; i++) {
log(vehicleIsolated.toString());
vehicleIsolated.add(vehicleList[i]['vehicle_num']);
}
if (vehicleIsolated.contains(selectedVehicleNumber) == true) {
int index = vehicleList.indexWhere(
(vehicle) {
return vehicle['vehicle_num'] == selectedVehicleNumber;
},
);
return vehicleList[index]['id'];
} else {
log('else');
int id = await addVehicle(selectedVehicleNumber, vehicleList);
return id; // or any other value that indicates no match was found
}
}
log('rebuild');
return Scaffold(
appBar: AppBar(
title: const Text('Add Vehicle Details'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Form(
key: _formKey,
child: Column(
children: [
const SizedBox(
height: 5,
),
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(22),
),
child: Column(
children: [
cardData(
data: source,
iconData: Icons.factory,
labelText: 'From'),
cardData(
data: plant,
iconData: Icons.location_on,
labelText: 'To'),
cardData(
data: remQty,
iconData: FontAwesomeIcons.weightScale,
labelText: 'Quantity'),
const SizedBox(
height: 5,
),
],
),
),
const SizedBox(
height: 10,
),
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(22),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 30),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
),
child: TextFormField(
controller: tonnageController,
decoration: InputDecoration(
labelText: 'Vehicle Tonnage in $unit',
),
validator: (value) {
if (value == '') {
return 'Please enter the tonnage';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 30),
child: TextFormField(
controller: freightRateController,
decoration: InputDecoration(
labelText: 'Freight Rate / $unit',
),
validator: (value) {
if (value == '') {
return 'Please enter the freight rate';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 10),
child: TextFormField(
enabled: false,
initialValue: userData!['name'],
decoration: const InputDecoration(
labelText: 'Vehicle Owner',
),
validator: (value) {
if (value == '') {
return 'Please enter the owner name';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 25,
),
child: Autocomplete<String>(
optionsBuilder:
(TextEditingValue textEditingValue) {
return vehicleList!.where(
(vehicle) {
String vehicleNum = vehicle['vehicle_num'];
return vehicleNum.toLowerCase().contains(
textEditingValue.text.toLowerCase(),
);
},
).map<String>((vehicle) {
String vehicleNum = vehicle['vehicle_num'];
return vehicleNum.toUpperCase();
}).toList();
},
onSelected: (String selectedOption) {
selectedVehicleNumber = selectedOption;
},
optionsViewBuilder:
(context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: ClipRRect(
borderRadius: BorderRadius.circular(
15), // Adjust the radius as needed
child: Material(
elevation: 10.0,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 300,
maxWidth: width / 1.2),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context,
int index) {
final String option =
options.elementAt(index);
return InkWell(
onTap: () {
onSelected(option);
},
child: Builder(builder:
(BuildContext context) {
final bool highlight =
AutocompleteHighlightedOption
.of(context) ==
index;
if (highlight) {
SchedulerBinding.instance
.addPostFrameCallback(
(Duration timeStamp) {
Scrollable.ensureVisible(
context,
alignment: 0.5);
});
}
return Container(
color: highlight
? Theme.of(context)
.focusColor
: null,
padding: const EdgeInsets.all(
16.0),
child: Text(option),
);
}),
);
},
),
),
),
),
);
},
fieldViewBuilder: (BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted) {
return TextFormField(
controller: textEditingController,
textCapitalization:
TextCapitalization.characters,
focusNode: focusNode,
onFieldSubmitted: (String value) {
onFieldSubmitted();
},
onChanged: (value) {
selectedVehicleNumber = value;
},
decoration: InputDecoration(
labelText: 'Vehicle Number',
filled: true,
fillColor: const Color.fromARGB(
255, 233, 233, 233),
contentPadding: const EdgeInsets.only(
left: 15,
top: 18,
bottom: 18,
),
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(15),
),
),
validator: (value) {
if (!validateIndianVehicleNumber(value!)) {
return 'Please enter a valid Indian vehicle number';
} else {
return null;
}
},
);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 15),
child: TextFormField(
controller: driverNameController,
decoration: const InputDecoration(
labelText: 'Driver Name',
),
validator: (value) {
if (value == '') {
return 'Please enter the driver name';
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 20),
child: TextFormField(
controller: driverNumberController,
maxLength: 10,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Driver Number',
),
validator: (value) {
if (value!.length != 10) {
return 'Please enter the driver number';
}
return null;
},
),
),
const Align(
alignment: Alignment.topLeft,
child: Padding(
padding: EdgeInsets.only(left: 25),
child: Text('Estimated time (in Hours)'),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: SelectHoursButton(
onRatingUpdate: (value) =>
handleRatingUpdate(value),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 22.0,
),
child: Row(
children: [
GoFlamingoButton(
buttonText: 'CANCEL',
onPressed: () {
Navigator.pop(context);
},
customWidth: width / 2.5,
fullWidth: false,
),
const Spacer(),
GoFlamingoButton(
buttonText: 'BOOK',
onPressed: () async {
if (_formKey.currentState!.validate()) {
int id = await findVehicleId(
vehicleList!,
selectedVehicleNumber!);
var tonnage = tonnageController.text;
var freightRate =
freightRateController.text;
var driverName =
driverNameController.text;
var driverNumber =
driverNumberController.text;
// Update values in the provider
bookingData.updateField(
'freightRate', freightRate);
bookingData.updateField(
'vehicleOwner',
userData!['id'].toString());
bookingData.updateField(
'vehicleNum', id.toString());
bookingData.updateField(
'driverName', driverName);
bookingData.updateField(
'driverNumber', driverNumber);
bookingData.updateField(
'schedule', id.toString());
bookingData.updateField(
'disQty', tonnage);
bookingData.updateField(
'estimatedTime',
(selectedRating * 2).toString());
vehicleIdProvider.selectVehicleId(id);
if (!context.mounted) return;
Navigator.pushNamed(
context, '/selectBankForm');
}
},
customWidth: width / 2.5,
fullWidth: false,
),
],
),
),
),
),
],
),
),
),
const SizedBox(
height: 30,
),
],
),
),
),
),
);
} else {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
}
Here's what's shown on my console log
[log] rebuild
flutter: URL :http://139.144.8.11/api/newTodaySchOnSource/
flutter: http://139.144.8.11/api/todaySchOnSource/}
[log] rebuild
flutter: URL :http://139.144.8.11/api/newTodaySchOnSource/
flutter: http://139.144.8.11/api/todaySchOnSource/}
[log] rebuild
flutter: URL :http://139.144.8.11/api/newTodaySchOnSource/
flutter: http://139.144.8.11/api/todaySchOnSource/}
[log] rebuild
flutter: URL :http://139.144.8.11/api/newTodaySchOnSource/
flutter: http://139.144.8.11/api/todaySchOnSource/}
[log] rebuild
flutter: URL :http://139.144.8.11/api/newTodaySchOnSource/
flutter: http://139.144.8.11/api/todaySchOnSource/}
[log] rebuild
flutter: URL :http://139.144.8.11/api/newTodaySchOnSource/
flutter: http://139.144.8.11/api/todaySchOnSource/}
It's not just rebuilding this page but also calling API on other pages
Using sizer: ^2.0.15
instead of
double width = MediaQuery.of(context).size.width;
helped