Reputation: 2311
I'm using Bloc library and noticed after yielding a new state my TextFormField
initialValue does not change.
My app is more complicated than this but I did a minimal example. Also tracking the state it is changing after pushing the events.
Bloc is supposed to rebuild the entire widget right. Am I missing something?
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:developer' as developer;
void main() {
runApp(MyApp());
}
enum Event { first }
class ExampleBloc extends Bloc<Event, int> {
ExampleBloc() : super(0);
@override
Stream<int> mapEventToState(Event event) async* {
yield state + 1;
}
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => ExampleBloc(),
child: Builder(
builder: (contex) => SafeArea(
child: BlocConsumer<ExampleBloc, int>(
listener: (context, state) {},
builder: (context, int state) {
developer.log(state.toString());
return Scaffold(
body: Form(
child: Column(
children: [
TextFormField(
autocorrect: false,
initialValue: state.toString(),
),
RaisedButton(
child: Text('Press'),
onPressed: () {
context.bloc<ExampleBloc>().add(Event.first);
},
)
],
),
),
);
}),
),
),
),
);
}
}
pubspec.yaml
name: form
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
bloc: ^6.0.0
flutter_bloc: ^6.0.0
Edit
As @chunhunghan noted adding a UniqueKey solves this. I should have also mentioned that my case. the app emits events from the onChanged
method of two TextFormField
. This causes the Form to reset and remove the keyboard. autofocus does not work because there are two TextFormField
wgich emit events.
Upvotes: 11
Views: 8149
Reputation: 2258
A proper way to do it:
Please keep in mind that it is absolutely key that you understand your own BLoc-Logic / Flow and how TextEditingControllers do work. If you keep updating the TextEditingController the cursor will jump as setting the text will trigger a setState which causes a new build call. A way to handle this is to set the selection after a change but be aware if you screw up your BLoc-Flow you will end up in jumpy cursors and keyboards again. My word of advice then is take a step back and draw the flow on a sheet of paper to get an overview. Otherwise you will keep fighting against the framework.
The BLoC in this example has the following states:
Loading |> Ready |> Saving |> Saved |> Ready
After "Saved" I update my state back to ready with the updated record and the listener of the BlocConsumer will get triggered again which then will update the TextEditingController.
TL:DR Pass a TextEditingController, only use BlocBuilder for the part that really needs a rebuild. Don't use any key hacks.
Implementation Example:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Ensure that it is a stateful widget so it keeps the controller on rebuild / window resizes / layout changes!
class BodyAddEditForm extends StatefulWidget {
const BodyAddEditForm({
Key? key,
}) : super(key: key);
@override
State<BodyAddEditForm> createState() => _BodyAddEditFormState();
}
class _BodyAddEditFormState extends State<BodyAddEditForm> {
final _formKey = GlobalKey<FormState>();
// ensure that the controllers are only created once to avoid jumping cursor positions
final _weightEditingController = TextEditingController();
final _restingPulseEditingController = TextEditingController();
// [other controllers]
@override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<BodyAddEditBloc>(context);
return Form(
key: _formKey,
child: Column(
children: [
BlocConsumer<BodyAddEditBloc, BodyAddEditState>(
// ensure that the value is only set to the TextEditingController the first time the bloc is ready, after some internal loading logic is handled.
listenWhen: (previous, current) => !previous.isReady && current.isReady,
listener: (context, state) {
// there is a chance that the bloc is faster than the current build can finish rendering the frame.
// if we now would update the text field this would result in another build cycle which triggers an assertion.
WidgetsBinding.instance.addPostFrameCallback((_) {
// set the initial value of the text field after the data becomes available
// ensure that we do not set the text field to 'null'
weightEditingController.text = state.record.weight != null ? state.record.weight!.toString() : '';
});
},
builder: (context, state) {
return TextFormField(
controller: _weightEditingController, // provide a controller for each TextFormField to ensure that the text field is updated.
keyboardType: TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*(\.|,)?[0-9]*')),
],
onChanged: (value) {
// example to update value on change to correct user input
// this is not necessary if you only want to update the value
// but it showcases how to handle that without resulting the virtual keyboard nor the cursor to jump around
value = value.replaceAll(',', '.');
if (value.isEmpty) {
bloc.add(BodyAddEditWeightChanged(weight: null));
return;
}
if (double.tryParse(value) == null) {
bloc.add(BodyAddEditWeightChanged(weight: null));
return;
}
bloc.add(BodyAddEditWeightChanged(weight: double.parse(value)));
},
);
},
),
// [more fields]
BlocBuilder<BodyAddEditBloc, BodyAddEditState>(
builder: (context, state) {
return ElevatedButton(
onPressed: () {
if (!_formKey.currentState!.validate()) {
return;
}
bloc.add(BodyAddEditSaveRequested());
},
child: Text("Save"),
);
},
),
],
),
);
}
}
Example of how to set the cursor, for instance after an update:
_weightEditingController.selection = TextSelection.fromPosition(TextPosition(offset: _weightEditingController.text.length));
Upvotes: 1
Reputation: 733
I also had the exact same problem. While adding the Unique Key
the flutter keeps building the widget and my keyboard unfocus each time. The way I solved it is to add a debounce in onChanged Event of the TextField.
class InputTextWidget extends StatelessWidget {
final Function(String) onChanged;
Timer _debounce;
void _onSearchChanged(String value) {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 2000), () {
onChanged(value);
});
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: TextEditingController(text: value)
..selection = TextSelection.fromPosition(
TextPosition(offset: value.length),
),
onChanged: _onSearchChanged,
onEditingComplete: onEditingCompleted,
);
}
}
Hope if this help for someone, working with form, bloc and and has too update the form.
Edit: Although adding a debounce help show what. I have changed the code to be more robust. Here is the change.
InputTextWidget
(Changed)
class InputTextWidget extends StatelessWidget {
final Function(String) onChanged;
final TextEditingController controller;
void _onSearchChanged(String value) {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 2000), () {
onChanged(value);
});
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
onChanged: _onSearchChanged,
onEditingComplete: onEditingCompleted,
);
}
}
And on my presentation end
class _NameField extends StatelessWidget {
const _NameField({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final TextEditingController _controller = TextEditingController();
return BlocConsumer<SomeBloc,
SomeState>(
listenWhen: (previous, current) =>
previous.name != current.name,
listener: (context, state) {
final TextSelection previousSelection = _controller.selection;
_controller.text = state.name;
_controller.selection = previousSelection;
},
buildWhen: (previous, current) =>
previous.name != current.name,
builder: (context, state) => FormFieldDecoration(
title: "Name",
child: InputTextWidget(
hintText: "AWS Certification",
textInputType: TextInputType.name,
controller: _controller,
onChanged: (value) => context
.read< SomeBloc >()
.add(SomeEvent(
value)),
),
),
);
}
}
This edit is working perfectly.
Final Edit:
I added a key? key
on my bloc state and pass this key to the widget. If I needed to redraw the form again, I changed the key to UniqueKey
from the event. This is the by far easiest way I have implemented bloc and form together. If you needed explanation, please comment here, I will add it later.
Upvotes: 0
Reputation: 54377
You can copy paste run full code 1 and 2 below
You can provide UniqueKey()
to Scaffold
or TextFormField
to force recreate
You can referecne https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d for detail
if the key of the Element doesn’t match the key of the corresponding Widget. This causes Flutter to deactivate those elements and remove the references to the Elements in the Element Tree
Solution 1:
return Scaffold(
key: UniqueKey(),
body: Form(
Solution 2:
TextFormField(
key: UniqueKey(),
working demo
full code 1 Scaffold
with UniqueKey
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:developer' as developer;
void main() {
runApp(MyApp());
}
enum Event { first }
class ExampleBloc extends Bloc<Event, int> {
ExampleBloc() : super(0);
@override
Stream<int> mapEventToState(Event event) async* {
yield state + 1;
}
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
print("build");
return MaterialApp(
home: BlocProvider(
create: (_) => ExampleBloc(),
child: Builder(
builder: (contex) => SafeArea(
child: BlocConsumer<ExampleBloc, int>(
listener: (context, state) {},
builder: (context, int state) {
print("state ${state.toString()}");
developer.log(state.toString());
return Scaffold(
key: UniqueKey(),
body: Form(
child: Column(
children: [
TextFormField(
autocorrect: false,
initialValue: state.toString(),
),
RaisedButton(
child: Text('Press'),
onPressed: () {
context.bloc<ExampleBloc>().add(Event.first);
},
)
],
),
),
);
}),
),
),
),
);
}
}
full code 2 TextFormField
with UniqueKey
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:developer' as developer;
void main() {
runApp(MyApp());
}
enum Event { first }
class ExampleBloc extends Bloc<Event, int> {
ExampleBloc() : super(0);
@override
Stream<int> mapEventToState(Event event) async* {
yield state + 1;
}
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
print("build");
return MaterialApp(
home: BlocProvider(
create: (_) => ExampleBloc(),
child: Builder(
builder: (contex) => SafeArea(
child: BlocConsumer<ExampleBloc, int>(
listener: (context, state) {},
builder: (context, int state) {
print("state ${state.toString()}");
developer.log(state.toString());
return Scaffold(
body: Form(
child: Column(
children: [
TextFormField(
key: UniqueKey(),
autocorrect: false,
initialValue: state.toString(),
),
RaisedButton(
child: Text('Press'),
onPressed: () {
context.bloc<ExampleBloc>().add(Event.first);
},
)
],
),
),
);
}),
),
),
),
);
}
}
Upvotes: 6