Flutter BlocBuilder displays previous state in TextFormField value - flutter

I am trying to create a TextFormField with increment and decrement buttons and TextFormField is editable "by hand" as well. But there is a small problem if I use BLoC with this - state "falls" one behind, meaning that when I tap "+" first time nothing changes, but when I tap it the second time it changes its value to 21 (and so on..).
I tried the same implementation with just a regular Text and it works as expected and updating properly.
I'm just wondering if my logic of how I am setting TextFormField is flawed:
Instantiating TextEditingController with default value amount (20);
On "+" tap:
Adding PlusEvent to increment current value
Getting amount value from state
Widget class:
class MyCalculation extends StatefulWidget {
const MyCalculation({Key? key}) : super(key: key);
#override
State<MyCalculation> createState() => _MyCalculationState();
}
class _MyCalculationState extends State<MyCalculation> {
late TextEditingController _controller;
late MyCalcBloc _bloc;
#override
void initState() {
super.initState();
_bloc = context.read();
_controller.text = _bloc.state.amount.toString();
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<MyCalcBloc, MyCalcState>(builder: (context, state) {
return MyCustomTextFormField(
controller: _controller,
onChanged: (value) {},
onPlusTap: () {
_bloc.add(PlusEvent());
_bloc.text = '${state.amount}';
},
onMinusTap: () {});
});
}
}
BLoC class:
class MyCalcBloc extends Bloc<MyCalcEvent, MyCalcState> {
MyCalcBloc() : super(const MyCalcState(amount: 20)) {
on<IncrementFromEvent>(_onPlusEvent);
}
void _onPlusEvent(PlusEvent event, Emitter<MyCalcState> emit) {
final newValue = state.amount + 1;
emit(MyCalcState(amount: newValue));
}
}

You should instantiate TextEditingController within BlocProvider, that way you'll get "current" state value displayed in TextFormField.
#override
Widget build(BuildContext context) {
return BlocBuilder<MyCalcBloc, MyCalcState>(builder: (context, state) {
_controller = TextEditingController(text: state.amount.toString());
return MyCustomTextFormField(
controller: _controller,
onChanged: (value) {},
onPlusTap: () {
_bloc.add(PlusEvent());
_bloc.text = '${state.amount}';
},
onMinusTap: () {});
});
}

Related

Create / Manage dynamic TextEditingControllers

I got a ListView.builder that generates n number of elements and I am looking at adding a controller for each of them. I have seen some approaches of adding a controller to a list of controllers and then access them by the index however I am just wondering how will this impact the performance of the screen if lets say you have 20 controllers? Are there some best practices for this scenario? Should you even go down this line or avoid it?
I suggest to introduce a Widget for all items in list.
Make sure you dispose in a correct place for the performance.
Also I request to store the user entered value with the object of item will help to restore on scrolls.
Eg:
class YourWidget extends StatefulWidget {
const YourWidget({Key? key, required this.item}) : super(key: key);
final YourItem item;
#override
State<YourWidget> createState() => _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
final controller = TextEditingController();
#override
void initState() {
super.initState();
controller.text = widget.item.enteredValue;
}
#override
Widget build(BuildContext context) {
return TextField(
controller: controller,
onChanged: (value){
widget.item.enteredValue = value;
},
...
);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
}
class YourItem {
String? id;
...
String enteredValue = '';
}

Call setState from class that extends StatefulWidget

If I update a variable using class object, the build method should get called, but I am unable to call setState from the StatefulWidget class.
class CustomErrorFormField extends StatefulWidget {
#override
_CustomErrorFormFieldState createState() {
return _CustomErrorFormFieldState();
}
List<String> errorList = []; //this variable will get updated using below function
void setErrorList(List<String> listOfError) {
errorList = listOfError;
}
}
class _CustomErrorFormFieldState extends State<CustomErrorFormField> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
print(widget.errorList); //this is not printing updated value
return .....
}
}
Now in some other class i will update errorList Variable
nameTextFild = CustomErrorFormField(
key: ValueKey(count),
labelName: "Name",
iContext: context,
onChanged: (String value) {
setState(() {
count++;
if (!value.contains(RegExp(r'[0-9]'))) {
nameTextFild!.setErrorList([]); //updating but changes not appearing (setState of this widget is not getting called)
} else {
nameTextFild!.setErrorList(["Invalid characters, use letters only."]);
}
});
},
);
It's not recommended that you change the state of a widget from outside the widget.
What you should do instead is pass the validation logic as a function and let the widget handle the state change.
CustomFormField:
import 'package:flutter/material.dart';
class CustomErrorFormField extends StatefulWidget {
//Take the validation logic as a parameter.
final List<String> Function(String value) validator;
const CustomErrorFormField({required this.validator});
#override
_CustomErrorFormFieldState createState() {
return _CustomErrorFormFieldState();
}
}
class _CustomErrorFormFieldState extends State<CustomErrorFormField> {
//Keep the state inside the widget itself
List<String> errorList = [];
//Update the state from inside the widget
void setErrorList(List<String> listOfError) {
setState(() {
errorList = listOfError;
});
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Form(
child: TextFormField(
validator: (String value){
//Use the validation logic to decide the error.
setErrorList(widget.validator(value))
}
}
),
);
}
}
I have used TextFormField as an example, you can use any widget that accepts a callback upon change.
If you're making everything from scratch you can attach the validator function to a callback that fires when the text is changed. Usually this is done with the help of a controller.
usage:
final nameTextFild = CustomErrorFormField(
key: ValueKey(count),
labelName: "Name",
iContext: context,
validator: (String value) {
if (!value.contains(RegExp(r'[0-9]'))) {
return [];
} else {
return ["Invalid characters, use letters only."];
}
},
);

SetState called during build()

I wrote logic with edit mode which allows user to make changes in input field, but when edit mode button is clicked again then input need back to value before editing. And there is a problem with that, because everything works fine but console is showing me this error:
════════ Exception caught by foundation library ════════════════════════════════
The following assertion was thrown while dispatching notifications for TextEditingController:
setState() or markNeedsBuild() called during build.
This Form widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: Form-[LabeledGlobalKey<FormState>#bcaba]
state: FormState#65267
The widget which was currently being built when the offending call was made was: ProfileInput
dirty
state: _ProfileInputState#32ea5
I know what this error means, but I can't find a place responsible for this. Could someone explain it to me?
class Profile extends StatefulWidget {
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
GlobalKey<FormState> _formKey = GlobalKey<FormState>();
User _user = User(
username: "name",
);
String? _tmpUsername;
bool _editMode = false;
void _createTemporaryData() {
_tmpUsername = _user.username;
}
void _restoreData() {
_user.username = _tmpUsername!;
}
void _changeMode() {
if (_editMode)
_restoreData();
else
_createTemporaryData();
setState(() {
_editMode = !_editMode;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
ElevatedButton(
onPressed: () => _changeMode(), child: Text("change mode")),
Form(
key: _formKey,
child: ProfileInput(
editMode: _editMode,
user: _user,
onChangeName: (value) {
_user.username = value;
},
),
),
],
),
);
}
}
class ProfileInput extends StatefulWidget {
final bool editMode;
final User user;
final void Function(String value)? onChangeName;
ProfileInput({
required this.editMode,
required this.user,
required this.onChangeName,
});
#override
_ProfileInputState createState() => _ProfileInputState();
}
class _ProfileInputState extends State<ProfileInput> {
TextEditingController _nameController = TextEditingController();
#override
void dispose() {
_nameController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
_nameController.text = widget.user.username;
return TextFormField(
onChanged: widget.onChangeName,
controller: _nameController,
enabled: widget.editMode,
);
}
}
Put the following line in the initState or use addPostFrameCallback.
_nameController.text = widget.user.username; // goes into initState
initState
#override
void initState() {
super.initState();
_nameController.text = widget.user.username;
}
addPostFrameCallback
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_nameController.text = widget.user.username;
}); // 1
SchedulerBinding.instance.addPostFrameCallback((_) {
_nameController.text = widget.user.username;
}); // 2
// use either 1 or 2.
// rest of the code, return statement.
}
Calling text setter on _nameController would notify all the listener and it's called inside the build method during an ongoing build that causes setState() or markNeedsBuild() called during build.
From Documentation:
Setting this will notify all the listeners of this TextEditingController that they need to update (it calls notifyListeners). For this reason, this value should only be set between frames, e.g. in response to user actions, not during the build, layout, or paint phases.

How to handle navigation using stream from inheritedWidget?

I'm using an inherited Widget to access a Bloc with some long running task (e.g. search).
I want to trigger the search on page 1 and continue to the next page when this is finished. Therefore I'm listening on a stream and wait for the result to happen and then navigate to the result page.
Now, due to using an inherited widget to access the Bloc I can't access the bloc with context.inheritFromWidgetOfExactType() during initState() and the exception as I read it, recommends doing this in didChangeDependencies().
Doing so this results in some weird behavior as the more often I go back and forth, the more often the stream I access fires which would lead to the second page beeing pushed multiple times. And this increases with each back and forth interaction. I don't understand why the stream why this is happening. Any insights here are welcome. As a workaround I keep a local variable _onSecondPage holding the state to avoid pushing several times to the second Page.
I found now How to call a method from InheritedWidget only once? which helps in my case and I could access the inherited widget through context.ancestorInheritedElementForWidgetOfExactType() and just listen to the stream and navigate to the second page directly from initState().
Then the stream behaves as I would expect, but the question is, does this have any other side effects, so I should rather get it working through listening on the stream in didChangeDependencides() ?
Code examples
My FirstPage widget listening in the didChangeDependencies() on the stream. Working, but I think I miss something. The more often i navigate from first to 2nd page, the second page would be pushed multiple times on the navigation stack if not keeping a local _onSecondPage variable.
#override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint("counter: $_counter -Did change dependencies called");
// This works the first time, after that going back and forth to the second screen is opened several times
BlocProvider.of(context).bloc.finished.stream.listen((bool isFinished) {
_handleRouting(isFinished);
});
}
void _handleRouting(bool isFinished) async {
if (isFinished && !_onSecondPage) {
_onSecondPage = true;
debugPrint("counter: $_counter - finished: $isFinished : ${DateTime.now().toIso8601String()} => NAVIGATE TO OTHER PAGE");
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
_onSecondPage = false;
} else {
debugPrint("counter: $_counter - finished: $isFinished : ${DateTime.now().toIso8601String()} => not finished, nothing to do now");
}
}
#override
void dispose() {
debugPrint("counter: $_counter - disposing my homepage State");
subscription?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder(
stream: BlocProvider.of(context).bloc.counter.stream,
initialData: 0,
builder: (context, snapshot) {
_counter = snapshot.data;
return Text(
"${snapshot.data}",
style: Theme.of(context).textTheme.display1,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
A simple Bloc faking some long running work
///Long Work Bloc
class LongWorkBloc {
final BehaviorSubject<bool> startLongWork = BehaviorSubject<bool>();
final BehaviorSubject<bool> finished = BehaviorSubject<bool>();
int _counter = 0;
final BehaviorSubject<int> counter = BehaviorSubject<int>();
LongWorkBloc() {
startLongWork.stream.listen((bool start) {
if (start) {
debugPrint("Start long running work");
Future.delayed(Duration(seconds: 1), () => {}).then((Map<dynamic, dynamic> reslut) {
_counter++;
counter.sink.add(_counter);
finished.sink.add(true);
finished.sink.add(false);
});
}
});
}
dispose() {
startLongWork?.close();
finished?.close();
counter?.close();
}
}
Better working code
If I however remove the code to access the inherited widget from didChangeDependencies() and listen to the stream in the initState() it seems to be working properly.
Here I get hold of the inherited widget holding the stream through context.ancestorInheritedElementForWidgetOfExactType()
Is this ok to do so? Or what would be a flutter best practice in this case?
#override
void initState() {
super.initState();
//this works, but I don't know if this is good practice or has any side effects?
BlocProvider p = context.ancestorInheritedElementForWidgetOfExactType(BlocProvider)?.widget;
if (p != null) {
p.bloc.finished.stream.listen((bool isFinished) {
_handleRouting(isFinished);
});
}
}
Personally, I have not found any reason not to listen to BLoC state streams in initState. As long as you remember to cancel your subscription on dispose
If your BlocProvider is making proper use of InheritedWidget you should not have a problem getting your value inside of initState.
like So
void initState() {
super.initState();
_counterBloc = BlocProvider.of(context);
_subscription = _counterBloc.stateStream.listen((state) {
if (state.total > 20) {
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return TestPush();
}));
}
});
}
Here is an example of a nice BlocProvider that should work in any case
import 'package:flutter/widgets.dart';
import 'bloc_base.dart';
class BlocProvider<T extends BlocBase> extends StatefulWidget {
final T bloc;
final Widget child;
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}) : super(key: key);
#override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context) {
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider =
context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
return provider?.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<BlocBase>> {
#override
Widget build(BuildContext context) {
return _BlocProviderInherited<T>(
bloc: widget.bloc,
child: widget.child,
);
}
#override
void dispose() {
widget.bloc?.dispose();
super.dispose();
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
final T bloc;
_BlocProviderInherited({
Key key,
#required Widget child,
#required this.bloc,
}) : super(key: key, child: child);
#override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
... and finally the BLoC
import 'dart:async';
import 'bloc_base.dart';
abstract class CounterEventBase {
final int amount;
CounterEventBase({this.amount = 1});
}
class CounterIncrementEvent extends CounterEventBase {
CounterIncrementEvent({amount = 1}) : super(amount: amount);
}
class CounterDecrementEvent extends CounterEventBase {
CounterDecrementEvent({amount = 1}) : super(amount: amount);
}
class CounterState {
final int total;
CounterState(this.total);
}
class CounterBloc extends BlocBase {
CounterState _state = CounterState(0);
// Input Streams/Sinks
final _eventInController = StreamController<CounterEventBase>();
Sink<CounterEventBase> get events => _eventInController;
Stream<CounterEventBase> get _eventStream => _eventInController.stream;
// Output Streams/Sinks
final _stateOutController = StreamController<CounterState>.broadcast();
Sink<CounterState> get _states => _stateOutController;
Stream<CounterState> get stateStream => _stateOutController.stream;
// Subscriptions
final List<StreamSubscription> _subscriptions = [];
CounterBloc() {
_subscriptions.add(_eventStream.listen(_handleEvent));
}
_handleEvent(CounterEventBase event) async {
if (event is CounterIncrementEvent) {
_state = (CounterState(_state.total + event.amount));
} else if (event is CounterDecrementEvent) {
_state = (CounterState(_state.total - event.amount));
}
_states.add(_state);
}
#override
void dispose() {
_eventInController.close();
_stateOutController.close();
_subscriptions.forEach((StreamSubscription sub) => sub.cancel());
}
}

Is there a better way to constantly rebuild a widget?

I have widget with data that changes regularly and I'm using a Timer.periodic to rebuild the widget. This starts out working smoothly but becomes choppy pretty quickly is there a better way to do this?
class _MainScreenState extends State<MainScreen> {
static const Duration duration = Duration(milliseconds: 16);
update(){
system.updatePos(duration.inMilliseconds/1000);
setState(() {});
}
#override
Widget build(BuildContext context) {
Timer.periodic(duration, (timer){
update();
});
return PositionField(
layoutSize: widget.square,
children: system.map
);
}
}
You are making a big mistake:
The build method must never have any side effects, because it is called again whenever setState is called (or when some higher up widget changes, or when the user rotates the screen...).
Instead, you want to create your Timer in initState, and cancel it on dispose:
class TimerTest extends StatefulWidget {
#override
_TimerTestState createState() => _TimerTestState();
}
class _TimerTestState extends State<TimerTest> {
Timer _timer;
int _foo = 0;
// this is only called once when the widget is attached
#override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (timer) => _update());
}
// stop the timer when the widget is detached and destroyed
#override
void dispose() {
_timer.cancel();
super.dispose();
}
void _update() {
setState(() {
_foo++;
});
}
#override
Widget build(BuildContext context) {
return Text('Foo: ${_foo}');
}
}