Flutter Bloc does not change TextFormField initialValue

Viewed 3634

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.

3 Answers

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);
                            },
                          )
                        ],
                      ),
                    ),
                  );
                }),
          ),
        ),
      ),
    );
  }
}

You should not be rebuilding the entire Form just because you want to update the value of the TextFormField, try using a TextEditingController and update the value on the listener.

TextEditingController _controller = TextEditingController();
BlocProvider(
    create: (_) => ExampleBloc(),
    child: Builder(
      builder: (contex) => SafeArea(
        child: BlocListener<ExampleBloc, int>(
            listener: (context, state) {
                _controller.text = state.toString();
            },
            child: Scaffold(
                body: Form(
                  child: Column(
                    children: [
                      TextFormField(
                        controller: _controller,
                        autocorrect: false,
                      ),
                      RaisedButton(
                        child: Text('Press'),
                        onPressed: () {
                          context.bloc<ExampleBloc>().add(Event.first);
                        },
                      )
                    ],
                  ),
                ),
              );
            }),

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.

Related