Flutter Bloc does not change TextFormField initialValue
Asked Answered
D

3

11

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.

Dorri answered 11/8, 2020 at 23:41 Comment(2)
Just a quick reminder that keys are expensive. You should be careful to not rebuild and entire widget when a change of state is then only thing needed.Asinine
Please check out the solution of @Asinine or mine and upvote it or mark it as answer as the current one is just wrong and worse!Incubation
B
7

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

enter image description here

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);
                            },
                          )
                        ],
                      ),
                    ),
                  );
                }),
          ),
        ),
      ),
    );
  }
}
Bishop answered 12/8, 2020 at 1:1 Comment(1)
Absolutely the worst idea! Ever tried using keyboard on a textfield then? Field loses focus or jumps between characters on mobile. Don't use this!Incubation
I
1

A proper way to do it:

  1. Create a stateful widget that will host the form. This is necessary to ensure that the TextEditingControllers are not disposed / newly created on a rebuild => It ensures that the state is kept.
  2. Create a TextEditingController for EACH TextFormField and pass it
  3. Set the value for the TextEditingController only once after you have loaded your data.
  4. Update the state of your Bloc using the TextFormField methods (onChanged, onFieldSubmitted) to keep the TextEditingController and your state in sync.
  5. If you have some backend generated values or adjustments make sure to update, and only then, the TextEditingControllers with the values. For example triggering an initial Loading-Flow again, or switch states.

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));
Incubation answered 6/4, 2023 at 21:0 Comment(0)
J
0

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.

Jordaens answered 9/3, 2021 at 16:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.