Search code examples
flutterdartflutter-futurebuilder

FutureBuilder Completely Unresponsive


My process is as follows. The screen has just two elements - TextFormField and an ElevatedButton.

  1. Get email address from user
  2. User clicks button
  3. Button validates input, then
  4. Calls FutureBuilder, which
  5. Tries to fetch client record from REST API
  6. Redirects to appropriate route

This is my first Flutter/Dart program FYI, so I might be making a beginner mistake.

Question: The very first line of the FutureBuilder isn't executed. No error, no messages, nothing. Why does this happen?

The user enters the email address, clicks the button, the fetchClientInfo function is executed, which returns a Future<ClientInfo> and that's that.

Could you help please?

@override
Widget build(BuildContext context) {
  final _formKey = GlobalKey<FormState>();

  return Scaffold(
      appBar: AppBar(
        title: Text("Register Profile"),
      ),
      body: Form(
          key: _formKey,
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Padding(
                    padding: EdgeInsets.all(20),
                    child: TextFormField(
                      controller: emailController,
                      validator: (email) {
                        if (email.isEmpty) {
                          return 'Please enter your email address.';
                        } else if (!EmailValidator.validate(email)) {
                          return 'Please enter a valid email address.';
                        }
                        return null;
                      },
                      decoration: InputDecoration(
                        border: new UnderlineInputBorder(borderSide: new BorderSide(color: Colors.red)),
                        labelText: 'Email',
                        hintText: 'Enter your email address',
                        contentPadding: EdgeInsets.all(20.0),
                      ),
                    )),
                ElevatedButton(
                    onPressed: () => {
                          if (_formKey.currentState.validate())
                            {
                              FutureBuilder<ClientInfo>(
                                future: fetchClientInfo(emailController.text),
                                builder: (BuildContext context, snapshot) {
                                  print("here");
                                  if (snapshot.data.outcome) {
                                    return Text("main screen");
                                  } else if (!snapshot.data.outcome) {
                                    Navigator.push(
                                        context,
                                        MaterialPageRoute(
                                            builder: (context) =>
                                                RegisterNewUser(emailAddress: emailController.text)));
                                  } else if (snapshot.hasError) {
                                    return Text("${snapshot.error}");
                                  }

                                  // Show a spinner
                                  return CircularProgressIndicator();
                                },
                              )
                            }
                        },
                    child: Text(
                      "Check Email",
                    ))
              ])));
}
Future<ClientInfo> fetchClientInfo(String emailAddress) async {
  var url = Uri.https(APIAccess.baseAPIURL, APIAccess.pathToClientAPI, {
    'client_id': '$emailAddress',
    'action': 'info',
    'key': '${APIAccess.key}'
  });

  final response = await http.get(url);

  if (response.statusCode == 200) {
    return ClientInfo.fromJson(jsonDecode(response.body));
  } else {
    throw Exception('Failed to load album');
  }
}

Solution

  • You're missing a couple of things:

    1. return statement (in your lambda you're creating a FutureBuilder but it's not being used anywhere)
    if(true) {
      return SizedBox.shrink();
    }
    
    1. correct lambda syntax (well, technically it's correct but it's not doing what you want): https://dart.dev/guides/language/language-tour#anonymous-functions
    // that's how compiler sees it
    Map<dynamic, dynamic> Function() foo = () => {
      
    };
    

    What you did reminds me of javascript, but in dart lambdas look a bit different

    return Button(
      onTap: () => doStuff(),
    );
    
    return Button(
      onTap: () {
        doStuff();
      }
    );
    
    // and if you want to return a value from block lambda
    return Builder(
      builder: (context) {
        return SizedBox.shrink();
      }
    );
    
    1. rendering widget on tap When handling tap events, it's best to redirect calls to a component that's handling business logic, and only listen for current state in the widget.

    What you want to read about is state management. The topic is highly opinionated, so you have to choose yourself the solution that's right for you. https://flutter.dev/docs/development/data-and-backend/state-mgmt

    I myself like using a slightly modified version of bloc. You can find the 'original' one here: https://pub.dev/packages/flutter_bloc

    A new de-facto standard if it comes to state management is Riverpod

    If you just want to make your code work, do something like this:

    class Demo extends StatefulWidget {
      @override
      _DemoState createState() => _DemoState();
    }
    
    class _DemoState extends State<Demo> {
    
      Future<ClientInfo?> clientInfo = Future.value(null);
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            ElevatedButton(
              onPressed: () {
                setState(() {
                  clientInfo = fetchClientInfo(emailController.text);
                });
              },
              child: _buildButtonContent(),
            ),
            FutureBuilder<ClientInfo>(
              initialData: null,
              future: clientInfo,
              builder: (BuildContext context, snapshot) {
                if (snapshot.data == null) {
                  return SizedBox.shrink();
                } else {
                  return Text(snapshot.data.toString());
                }
              },
            )
          ],
        );
      }
    }