skip to Main Content

I do not know English well, so I will hope for a translator)
P.S I am new to the world of flutter and dart, I am a .net and angular developer.
In my application, I refuse to use StatefullWidget in favor of Flutter_Bloc, so if I need to change the state in the stream, then it will be more like a StreamBuilder.
I ran into a problem: I have a widget that uses TextField or TextFormField, it doesn’t matter to me what to use, at the moment I have functionality in the first place, not design, as the deadlines are burning (for the university). I want to initialize the data from the Bloc that gets there from the BlocProvider after the event that receives the data from my backend application is fired. If you use a TextFormField, it has an initialValue property, if you write state.Title there, then I see the following picture through the debugger: initially the state has what I wrote for the block in Super, and there, in fact, everything is null, but after literally a moment (some then the number of milliseconds) its state is updated after the initialization event: bloc..add(event), but an empty value got into initialValue and is not redrawn, I don’t know how best to make this behavior for me so that the value is initialized in the text field and subsequently changed together with state update.
In my application, I did a thing that I don’t like – I set up a variable and inside the StreamBuilder through the TextEditingController I change the text inside the text field, and when I click on the save button, I send this value, but I want it to interact with the state and update every time. I’m attaching an example of the code where I’m facing the problem.

ParentWidget

    BlocProvider(
      create: (context) => PlanStageDetailBloc()..add(PlanStageDetailInitialEvent(planId, id)),
      child: BlocBuilder<PlanStageDetailBloc, PlanStageDetailInitialState>(
        builder: (context, state) {
          final bloc = context.read<PlanStageDetailBloc>();
          return RefreshIndicator(
            onRefresh: () async => bloc.add(PlanStageDetailInitialEvent(planId, id)),
            child: Scaffold(
                appBar: AppBar(
                  backgroundColor: ConstantColors.mainMenuBtn,
                  title: const Text('Test text'),
                body: const PlanStageDetailScreen(),

ChildWidget

class PlanStageDetailScreen extends StatelessWidget {
  const PlanStageDetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: IntrinsicHeight(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Flexible(
                child: FractionallySizedBox(
                    widthFactor: 0.9,
                    child: Padding(
                      padding: EdgeInsets.only(bottom: 10, left: 40),
                      child: _PlanDropdownButton(),
                    ))),
            const Flexible(
                child: FractionallySizedBox(
                    widthFactor: 0.9,
                    child: Padding(padding: EdgeInsets.only(bottom: 20, left: 40), child: _TitleTextField())))

TitleTextFieldWidget

class _TitleTextField extends StatelessWidget {
  const _TitleTextField();

  @override
  Widget build(BuildContext context) {

    return BlocBuilder<PlanStageDetailBloc, PlanStageDetailInitialState>(
      builder: (context, state) {
        return TextFormField(
            obscureText: false,
            initialValue: state.title,
            keyboardType: TextInputType.multiline,
            onChanged: (text) => context.read<PlanStageDetailBloc>().add(ChangeTitle(text)),
            onFieldSubmitted: (value) => context.read<PlanStageDetailBloc>().add(ChangeTitle(value)),
            decoration:
                TextFormFieldStyle.textFieldStyle(labelTextStr: 'Title*', hintTextStr: 'Enter title'));
      },
    );
  }
}

Partial PlanStageDetailBloc.dart

class PlanStageDetailBloc extends Bloc<PlanStageDetailEvent, PlanStageDetailInitialState> {
  PlanStageDetailBloc() : super(PlanStageDetailInitialState._(id: 0)) {
    on<PlanStageDetailInitialEvent>(_onInit);
    on<ChangeDate>(_onChangeDate);
    on<ChangeTitle>(_onChangeTitle);
    on<ChangeData>(_onChangeData);
    on<ChangeStatus>(_onChangeStatus);
    on<ChangePriority>(_onChangePriority);
    on<ChangeParentPlan>(_onChangeParentPlan);
  }

I accept constructive criticism, as well as good suggestions for improving my code), but most importantly I want to see options for solving this problem

I expect that after initializing the widget, my Bloc will receive all the necessary data and display them in my fields if the data corresponding to the field exists, in order to further correct or change them along with the Bloc state change, and send the changed data to my backend so that my data has been updated in the database.

2

Answers


  1. The problem is that the TextFormField is trying to get its initial value from the Bloc state, but the Bloc state is not yet initialized when the TextFormField is created. To fix this, you can use the initState() method of the State class to initialize the Bloc state before the TextFormField is created.

    Here is an example of how you can do this:

    class _TitleTextFieldState extends State<_TitleTextField> {
      @override
      void initState() {
        super.initState();
        context.read<PlanStageDetailBloc>().add(PlanStageDetailInitialEvent(planId, id));
      }
    
      @override
      Widget build(BuildContext context) {
        return BlocBuilder<PlanStageDetailBloc, PlanStageDetailInitialState>(
          builder: (context, state) {
            return TextFormField(
              obscureText: false,
              initialValue: state.title,
              keyboardType: TextInputType.multiline,
              onChanged: (text) => context.read<PlanStageDetailBloc>().add(ChangeTitle(text)),
              onFieldSubmitted: (value) => context.read<PlanStageDetailBloc>().add(ChangeTitle(value)),
              decoration: TextFormFieldStyle.textFieldStyle(
                labelTextStr: 'Title*',
                hintTextStr: 'Enter title',
              ),
            );
          },
        );
      }
    }
    
    Login or Signup to reply.
  2. The desired behavior can be achieved by directly accessing the public state of the TextFormField and updating it in response to the Bloc event. We can update it in response to a Bloc event by using a BlocListener instead of a BlocBuilder.

    Step by step:

    1. Define the Bloc events. In this scenario, I will define a single InitializeApi event to trigger some asynchronous work.

    Code:

    /// Base class for [ApiBloc] events.
    abstract class ApiEvent {
      const ApiEvent();
    }
    
    /// This event is used to trigger API initialization.
    class InitializeApi extends ApiEvent {
      const InitializeApi();
    }
    
    1. We need the Bloc states that our TextFormField will respond to. This includes an uninitialized state ApiPendingInitialization (not necessary, only included for clarity), and a ApiInitialized state for when the asynchronous operation is completed.

    Code:

    /// Base class for [ApiBloc] states.
    abstract class ApiState {
      const ApiState();
    }
    
    /// Intermediary state for unitialized [ApiBloc] instances.
    class ApiPendingInitialization extends ApiState {
      const ApiPendingInitialization();
    }
    
    /// State indicating that the [ApiBloc] is ready/initialized.
    class ApiInitialized extends ApiState {
      ApiInitialized(this.title);
    
      final String title;
    }
    
    1. A simple ApiBloc which is configured for our previous classes. It defaults to ApiPendingInitialization until the InitializeApi event is added to the Bloc.

    Code:

    class ApiBloc extends Bloc<ApiEvent, ApiState> {
      ApiBloc() : super(const ApiPendingInitialization()) {
        on<InitializeApi>(_initializeApi);
      }
      
      /// Initialize the API. Simulate asynchronous work.
      Future<void> _initializeApi(
        InitializeApi event,
        Emitter<ApiState> emit,
      ) async {
        await Future.delayed(const Duration(seconds: 1));
        emit(ApiInitialized("API Response Title"));
      }
    }
    
    1. Now that the Bloc boilerplate is complete, we will create our Widget to hook it up with the TextFormField. This will simply be called ApiConsumer.

    Code:

    class ApiConsumer extends StatelessWidget {
      const ApiConsumer({super.key});
    
      @override
      Widget build(BuildContext context) {
        /// This key will be used to access the TextFormField state.
        ///
        /// The TextFormField state is of type FormFieldState<String>, so that is
        /// the generic type of this GlobalKey.
        final textFormFieldKey = GlobalKey<FormFieldState<String>>();
    
        final textFormField = BlocListener<ApiBloc, ApiState>(
          listener: (context, state) {
            /// If the state is not ApiInitialized, then there is no data. Return.
            if (state is! ApiInitialized) return;
    
            /// The state must be ApiInitialized here, and ApiInitialized has data:
            /// the title property.
            textFormFieldKey.currentState?.didChange(state.title);
          },
          child: TextFormField(
            key: textFormFieldKey, // Make sure to pass the key.
            obscureText: false,
            keyboardType: TextInputType.multiline,
            decoration: const InputDecoration(
              label: Text('Title*'),
              hintText: 'Enter title',
            ),
          ),
        );
    
        return MaterialApp(home: Scaffold(body: SafeArea(child: textFormField)));
      }
    }
    
    1. Finally, the entry point should be self-explanatory.

    Code:

    runApp(
        BlocProvider(
          create: (_) => ApiBloc()..add(const InitializeApi()),
          child: const ApiConsumer(),
        ),
      );
    

    When you run this program, you will see that the TextFormField will update to the API response value "API Response Title" after the simulated asynchronous work inside _initializeApi is complete.

    Note:

    By using the BlocListener widget, the ApiConsumer widget itself never rebuilds. However, the TextFormField does rebuild, because it is a StatefulWidget and its didChange method internally calls setState.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search