skip to Main Content

I’m working on a Flutter application using Riverpod for state management. I have a TextFormField with a TextEditingController to keep track of the entered value. The problem is when I enter a value into the TextFormField, the screen does not update accordingly. Moreover, the ElevatedButton in my application is not responsive (it can’t be pressed) even when the TextFormField is not empty.

Here is the snippet of my code:

final textFieldControllerProvider =
    StateProvider<TextEditingController>((ref) => TextEditingController());

class MyWidget extends ConsumerWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final controller = ref.watch(textFieldControllerProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text("Riverpod example"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            TextFormField(
              controller: controller,
              onChanged: (value) {
                ref.watch(textFieldControllerProvider.notifier).state.text =
                    value;
              },
            ),
            ElevatedButton(
              onPressed: controller.text.isNotEmpty
                  ? () {
                      print('Button pressed with value');
                    }
                  : null,
              child: const Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

Actual operation:

enter image description here

Our two main issues:

Button responsiveness: buttons do not work as expected, even though they are set to activate when the text from the controller is not empty.

Cursor movement: the cursor moves unexpectedly to the left whenever text is entered.

We suspect that both the button unresponsiveness and the cursor moving to the left may be related to the widget update process.

Any suggestions on how to resolve these issues would be appreciated.

2

Answers


  1. The biggest issue is you’re using ConsumerWidget which works as a StatelessWidget. So, it will not going to update your screen. To update state you’ve to use ConsumerStatefulWidget or StatefulHookConsumerWidget like below.

    ConsumerStatefulWidget Example

    final textFieldControllerProvider =
    Provider<TextEditingController>((ref) => TextEditingController());
    class MyWidget extends ConsumerStatefulWidget {
      const MyWidget({super.key});
    
      @override
      ConsumerState<ConsumerStatefulWidget> createState() => _MyWidgetState();
    }
    
    class _MyWidgetState extends ConsumerState<MyWidget> {
    
      @override
      Widget build(BuildContext context) {
        final controller = ref.watch(textFieldControllerProvider);
        return Scaffold(
          appBar: AppBar(
            title: const Text("Riverpod example"),
          ),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              children: [
                TextFormField(
                  controller: controller,
                ),
                ElevatedButton(
                  onPressed: controller.text.isNotEmpty
                      ? () {
                    print('Button pressed with value');
                  }
                      : null,
                  child: const Text('Submit'),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    StatefulHookConsumerWidget Example

    final textFieldControllerProvider =
    Provider<TextEditingController>((ref) => TextEditingController());
    
    class MyWidget extends StatefulHookConsumerWidget {
         const MyWidget({super.key});
    
         @override
         ConsumerState<ConsumerStatefulWidget> createState() => _MyWidgetState();
      }
      class _MyWidgetState extends ConsumerState<MyWidget> {
      @override
      Widget build(BuildContext context) {
        final controller = ref.watch(textFieldControllerProvider);
        return Scaffold(
          appBar: AppBar(
            title: const Text("Riverpod example"),
          ),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              children: [
                TextFormField(
                  controller: controller,
                ),
                ElevatedButton(
                  onPressed: controller.text.isNotEmpty
                      ? () {
                    print('Button pressed with value');
                  }
                      : null,
                  child: const Text('Submit'),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    Login or Signup to reply.
  2. There are a number of errors in your code. Now I will help you fix them:

    1. Never access a provider via ref.watch inside initState and callback. Use ref.read instead:
    onChanged: (value) {
      ref.read(textFieldControllerProvider.notifier).state.text = value;
    },
    
    1. Notice what object your TextEditingController is. If you walk through the inheritance tree, it will look like this:
    Inheritance:
    Object -> ChangeNotifier -> ValueNotifier<TextEditingValue> -> TextEditingController
    

    This means that neither Provider nor StateProvider is adequate here. Use ChangeNotifierProvider for this purpose:

    final textFieldControllerProvider =
        ChangeNotifierProvider<TextEditingController>(
      (ref) => TextEditingController(),
    );
    
    1. You also need to wrap your button in a Consumer to avoid rebuilding the entire tree:
    Consumer(
      builder: (context, ref, child) {
        final text = ref.watch(
          textFieldControllerProvider.select((value) => value.text),
        );
        print('#build ElevatedButton');
        return ElevatedButton(
          onPressed: text.isNotEmpty
              ? () {
                  print('Button pressed with value');
                }
              : null,
          child: const Text('Submit'),
        );
      },
    ),
    

    I used select to keep track of only the field I want. In this case, we are tracking text. Although this does not really matter here, because. text is part of the main state, which we don’t have access to.


    Your complete code will look like this:

    Wrapper to run in dartpad:

    import 'package:flutter/material.dart';
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    
    void main() => runApp(ProviderScope(child: MyApp()));
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData.dark().copyWith(
            scaffoldBackgroundColor: const Color.fromARGB(255, 18, 32, 47),
          ),
          debugShowCheckedModeBanner: false,
          home: const Scaffold(body: Center(child: MyWidget())),
        );
      }
    }
    

    Main code with all notes:

    final textFieldControllerProvider =
        ChangeNotifierProvider<TextEditingController>(
      (ref) => TextEditingController(),
    );
    
    class MyWidget extends ConsumerWidget {
      const MyWidget({super.key});
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        print('#build MyWidget');
        return Scaffold(
          appBar: AppBar(
            title: const Text("Riverpod example"),
          ),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              children: [
                Consumer(
                  builder: (context, ref, child) {
                    final controller = ref.watch(textFieldControllerProvider);
                    print('#build TextFormField');
                    return TextFormField(
                      controller: controller,
                      onChanged: (value) {
                        ref.read(textFieldControllerProvider).text = value;
                      },
                    );
                  },
                ),
                Consumer(
                  builder: (context, ref, child) {
                    final text = ref.watch(
                      textFieldControllerProvider.select((value) => value.text),
                    );
                    print('#build ElevatedButton');
                    return ElevatedButton(
                      onPressed: text.isNotEmpty
                          ? () {
                              print('Button pressed with value');
                            }
                          : null,
                      child: const Text('Submit'),
                    );
                  },
                ),
              ],
            ),
          ),
        );
      }
    }
    

    A simplified version would look like this (MyWidget will be rebuilt whenever the text changes):

    class MyWidget extends ConsumerWidget {
      const MyWidget({super.key});
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final controller = ref.watch(textFieldControllerProvider);
        print('#build MyWidget');
        return Scaffold(
          appBar: AppBar(
            title: const Text("Riverpod example"),
          ),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              children: [
                TextFormField(
                  controller: controller,
                  onChanged: (value) {
                    ref.read(textFieldControllerProvider).text = value;
                  },
                ),
                ElevatedButton(
                  onPressed: controller.text.isNotEmpty
                      ? () {
                          print('Button pressed with value');
                        }
                      : null,
                  child: const Text('Submit'),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    I also added print methods so you can see the rebuild moments in the console.

    Good coding!

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