せいゆう
せいゆう

Reputation: 177

Screen Not Updating and ElevatedButton Not Responding in Flutter when using Riverpod and TextEditingController

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.

Upvotes: 1

Views: 143

Answers (2)

Ruble
Ruble

Reputation: 4844

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

Upvotes: 3

せいゆう
せいゆう

Reputation: 177

Short Solution.

final textFieldControllerProvider =
    ChangeNotifierProvider<TextEditingController>(
  (ref) => TextEditingController(),
);

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    print('#build MyWidget');
    final controller = ref.watch(textFieldControllerProvider);
    final text =
        ref.watch(textFieldControllerProvider.select((value) => value.text));
    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: text.isNotEmpty
                  ? () {
                      print('Button pressed with value');
                    }
                  : null,
              child: const Text('Submit'),
            )
          ],
        ),
      ),
    );
  }
}

Upvotes: 0

Related Questions