8

I'm new to flutter development and trying to understand Riverpod.

I currently managed to make the following login form work with a StatefulWidget. Basically, the button becomes enabled if both fields are not empty and viceversa.

enter image description here

And this is the current code for it.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Login Demo',
      // theme: ThemeData(
      //   primarySwatch: Colors.blue,
      // ),
      home: Scaffold(
        body: Center(
          child: SizedBox(
            width: 400,
            child: MyLoginPage2(),
          ),
        ),
      ),
    );
  }
}

class MyLoginPage extends StatefulWidget {
  const MyLoginPage({Key? key}) : super(key: key);

  @override
  State<MyLoginPage> createState() => _MyLoginState();
}

class _MyLoginState extends State<MyLoginPage> {
  final emailController = TextEditingController(text: '');
  final passwordController = TextEditingController(text: '');

  @override
  void initState() {
    super.initState();
    emailController.addListener(() {
      setState(() {});
    });
    passwordController.addListener(() {
      setState(() {});
    });
  }

  @override
  void dispose() {
    emailController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        TextField(
          controller: emailController,
          keyboardType: TextInputType.emailAddress,
        ),
        TextField(
          controller: passwordController,
          obscureText: true,
        ),
        areFieldsEmpty()
            ? const ElevatedButton(
                child: Text('Login disabled'),
                onPressed: null,
              )
            : ElevatedButton(
                child: const Text('Login enabled'),
                onPressed: () => print("this is where login happens"),
              )
      ],
    );
  }

  bool areFieldsEmpty() {
    return emailController.text.toString().isEmpty ||
        passwordController.text.toString().isEmpty;
  }
}

What I don't like is how the listeners call setState directly just to trigger a widget refresh. Is this how would you accomplish such a behavior?

I read great things about Riverpod, but I don't seem to grasp how to model the above behavior inheriting from HookWidget instead of StatefulWidget. Specifically, how to add the listeners to the text editing controllers.

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Login Demo',
      // theme: ThemeData(
      //   primarySwatch: Colors.blue,
      // ),
      home: Scaffold(
        body: Center(
          child: SizedBox(
            width: 400,
            child: MyLoginPage2(),
          ),
        ),
      ),
    );
  }
}

class MyLoginPage2 extends HookWidget {
  final emailController = useTextEditingController(text: '');
  final passwordController = useTextEditingController(text: '');

  MyLoginPage2({Key? key}) : super(key: key);

  // Pending questions
  // 1. Where do I add the listeners to the controllers?
  // 2. They are supposed to be auto dispose, right?

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        TextField(
          controller: emailController,
          keyboardType: TextInputType.emailAddress,
        ),
        TextField(
          controller: passwordController,
          obscureText: true,
        ),
        areFieldsEmpty()
            ? const ElevatedButton(
                child: Text('Login disabled'),
                onPressed: null,
              )
            : ElevatedButton(
                child: const Text('Login enabled'),
                onPressed: () => print("this is where login happens"),
              )
      ],
    );
  }

  bool areFieldsEmpty() {
    return emailController.text.toString().isEmpty ||
        passwordController.text.toString().isEmpty;
  }
}

Any help or tips will be appreciated.

Robert Estivill
  • 12,369
  • 8
  • 43
  • 64
  • 1
    the question is: do you have to use `HookWidget` and `riverpod` at all? – pskink Nov 03 '21 at 10:27
  • Not really @pskink . I am a newbie on all flutter related topics and libraries. Riverpod seems to be a defacto good practice and I was wondering what was I missing while trying to understand the tradeoffs and approaches to the same solution. In this particular case, and given the current only answer, I don't think it's worth it. – Robert Estivill Nov 03 '21 at 10:33
  • @pskink that does look a lot simpler and easy to read. I'm curious about the AnimatedBuilder. Isn't there any other widget that rebuilds based on a ValueNotifier that is not related to UI animations? – Robert Estivill Nov 03 '21 at 11:26
  • 1
    sure, good question! `ValueListenableBuilder` - but actually `AnimatedBuilder`'s name is misleading - it can use any `Listenable` not only `Animation` – pskink Nov 03 '21 at 11:26
  • Yes. The second pastebin looks a lot clearer and simpler. The fact that you don't have to take care of adding and disposing the text editing controllers is a big plus. Thanks for your answers. – Robert Estivill Nov 03 '21 at 11:31
  • 2
    sure, your welcome, note that `ValueListenableBuilder` uses one additional parameter in the `builder` - you can use instead of `canLogin.value` – pskink Nov 03 '21 at 11:32

2 Answers2

7

To update the changes on HookWidget use useEffect(). We don't have to worry about dispose until create our custom hookWidget.


class MyLoginPage2 extends HookWidget {
  const MyLoginPage2({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final emailController = useTextEditingController(text: '');
    final passwordController = useTextEditingController(text: '');

    final _areFieldsEmpty =
        useState<bool>(true); // controll the button based on Text.isEmpty

    bool areFieldsEmpty() {
      return emailController.text.toString().isEmpty ||
          passwordController.text.toString().isEmpty;
    }

    useEffect(() {
      emailController.addListener(() {
        _areFieldsEmpty.value = areFieldsEmpty();
      });
      passwordController.addListener(() {
        _areFieldsEmpty.value = areFieldsEmpty();
      });
    }, [
      emailController,
      passwordController,
    ]);
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        TextField(
          controller: emailController,
          keyboardType: TextInputType.emailAddress,
        ),
        TextField(
          controller: passwordController,
          obscureText: true,
        ),
        _areFieldsEmpty.value
            ? const ElevatedButton(
                child: Text('Login disabled'),
                onPressed: null,
              )
            : ElevatedButton(
                child: const Text('Login enabled'),
                onPressed: () => print("this is where login happens"),
              )
      ],
    );
  }
}


Md. Yeasin Sheikh
  • 54,221
  • 7
  • 29
  • 56
  • 1
    Thanks for your answer! I'm struggling to understand why would I use a HookWidget in this case at all. This seems to be less intuitive at least on my eyes. A lot of stuff in the build method that is not technically building the widget. How do I ensure the effect is only executed once? Same thing with the TextEditingController instances. This is not against your solution, as I understand riverpod requires to do it this way. However, i find hard to justify all this framework complexity to model something quite simple. – Robert Estivill Nov 03 '21 at 09:57
  • Technically this is the answer to the original question, as I wanted to figure out how to accomplish this with Riverpod. However, I will encourage everyone to take a look at the question comments, as there is a much simpler, readable and maintainable solution that doesn't require riverpood or hooks at all. Thanks to @pskink for the solution. – Robert Estivill Nov 03 '21 at 11:34
0

You just need only useTextEditingController(); with .value method.

class MyLoginPage2 extends HookWidget {
  const MyLoginPage2({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    final emailController = useTextEditingController();
    final passwordController = useTextEditingController();
    final _areFieldsEmpty = emailController.value.text.isEmpty && 
    passwordController.value.text.isEmpty;

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        TextField(
          controller: emailController,
          keyboardType: TextInputType.emailAddress,
        ),
        TextField(
          controller: passwordController,
          obscureText: true,
        ),
        _areFieldsEmpty
            ? const ElevatedButton(
                child: Text('Login disabled'),
                onPressed: null,
              )
            : ElevatedButton(
                child: const Text('Login enabled'),
                onPressed: () => print("this is where login happens"),
              )
      ],
    );
  }
}
Quyen Anh Nguyen
  • 1,204
  • 13
  • 21