Search code examples
flutterdarttextfieldflutter-blocflutter-state

Flutter TextField\TextFormField initialization value with flutter_bloc


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.


Solution

  • I've been searching for a solution to the problem for a while, talked to ChatGPT, and here's what I found. It seemed like a good solution to me, but I haven't tested it on systems other than Android yet. However, it works perfectly on Android.
    PlanStageDetail.dart

    class PlanStageDetailWidget extends StatelessWidget {
      const PlanStageDetailWidget({super.key, required this.planId, required this.title, required this.id});
    
      final num planId;
      final num id;
      final String title;
    
      @override
      Widget build(BuildContext context) {
        return BlocProvider(
          create: (context) => PlanStageDetailBloc()..add(PlanStageDetailInitialEvent(planId, id)),
          child: BlocBuilder<PlanStageDetailBloc, PlanStageDetailInitialState>(
            builder: (context, state) {
              final bloc = context.read<PlanStageDetailBloc>();
    
              TextEditingController titleController = TextEditingController(text: state.title);
              titleController.value = titleController.value.copyWith(
                  text: bloc.state.title,
                  selection: TextSelection.fromPosition(TextPosition(offset: bloc.state.title.length)));
    
              TextEditingController dataController = TextEditingController(text: state.data);
              dataController.value = dataController.value.copyWith(
                  text: bloc.state.data,
                  selection: TextSelection.fromPosition(TextPosition(offset: bloc.state.data.length)));
    
              return RefreshIndicator(
                onRefresh: () async => bloc.add(PlanStageDetailInitialEvent(planId, id)),
                child: Scaffold(
                    appBar: AppBar(
                      backgroundColor: ConstantColors.mainMenuBtn,
                      title: Row(children: [
                        Padding(
                          padding: const EdgeInsets.only(right: 15),
                          child: IconButton(
                              onPressed: () => context
                                  .read<HomeAuthNavCubit>()
                                  .changePageRoute(PlanStagesList(planId: planId), 'Планы', 4),
                              icon: const Icon(Icons.arrow_back_ios_rounded)),
                        ),
                        Text(title)
                      ]),
                    ),
                    body: Center(
                      child: SingleChildScrollView(
                        child: IntrinsicHeight(
                          child: Column(
                            children: <Widget>[
                              const Flexible(
                                  child: FractionallySizedBox(
                                      widthFactor: 0.9,
                                      child: Padding(
                                        padding: EdgeInsets.only(bottom: 10),
                                        child: _PlanDropdownButton(),
                                      ))),
                              Flexible(
                                  child: FractionallySizedBox(
                                      widthFactor: 0.9,
                                      child: Padding(
                                          padding: const EdgeInsets.only(bottom: 20),
                                          child: TextFormField(
                                              controller: titleController,
                                              obscureText: false,
                                              keyboardType: TextInputType.multiline,
                                              onChanged: (text) {
                                                bloc.add(ChangeTitle(text));
                                              },
                                              decoration: TextFormFieldStyle.textFieldStyle(
                                                  labelTextStr: 'Наименование *', hintTextStr: 'Введите наименование')))))