fluttermobileobservablemobxcomputed-observable

FLUTTER MOBX: A build function returned null. The relevant error-causing widget was Observer


I'm trying to do a verification in two fields, email and password, using MOBX, and I'm computing the result of two functions, in a compuntig called formIsValid, but mobX has returned this error to me: A build function returned null. The relevant error-causing widget was Observer

I tried to do it in different ways and I can't, and besides that my email observables emailErrorLabel and passwordErrorLabel are not affecting the TextTormField errorText.

Here is my code ViewModel:

import 'package:covid_app/app/service/firebase/firebase_auth.dart';
import 'package:covid_app/app/service/firebase/firebase_auth_impl.dart';
import 'package:covid_app/app/ui/home/home_page.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';

part 'login_viewmodel.g.dart';

class LoginViewModel = LoginViewModelBase with _$LoginViewModel;

abstract class LoginViewModelBase with Store {
  @observable
  String email = "";

  @observable
  String password = "";

  @observable
  bool error = false;

  @observable
  bool emailErrorLabel = false;

  @observable
  bool passwordErrorLabel = false;

  final _auth = Auth();

  @action
  changeEmail(String newEmail) => email = newEmail;

  @action
  changePassword(String newPassword) => password = newPassword;

  @action
  setHasErrorOnEmail(bool value) => emailErrorLabel = value;

  @action
  setHasErrorOnPassword(bool value) => passwordErrorLabel = value;

  bool emailIsValid() {
    if (email.isNotEmpty && email.contains("@")) {
      return true;
    } else {
      setHasErrorOnEmail(true);
      return false;
    }
  }

  bool passwordIsValid() {
    if (password.isNotEmpty || password.length >= 8) {
      return true;
    } else {
      setHasErrorOnPassword(true);
      return false;
    }
  }

  @computed
  bool get formIsValid {
    return emailIsValid() && passwordIsValid();
  }

  @action
  Future<void> firebaseLogin(dynamic context) async {
    try {
      if (email.isNotEmpty && password.isNotEmpty) {
        var userId;
        await _auth.signIn(email, password).then((value) => userId = value);
        userId.length > 0 ? homeNavigator(context) : error = true;
      } else {
        error = true;
      }
    } catch (Exception) {
      error = true;
      print("Login Error: $Exception");
    }
  }

  void homeNavigator(context) {
    Navigator.push(
        context, MaterialPageRoute(builder: (context) => HomePage()));
  }
}

My LoginPage:

import 'package:covid_app/app/ui/login/login_viewmodel.dart';
import 'package:covid_app/app/widgets/KeyboardHideable.dart';
import 'package:covid_app/core/constants/colors.dart';
import 'package:covid_app/core/constants/dimens.dart';
import 'package:covid_app/core/constants/string.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import '../../widgets/button_component.dart';
import 'widgets/text_form_field_component.dart';

class LoginPage extends StatefulWidget {
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  var vm = LoginViewModel();
  var value = zero;
  var valueTextFields = sixtyEight;
  TextEditingController controllerEmail = TextEditingController();
  TextEditingController controllerPassword = TextEditingController();

  void animatedTest() async {
    Future.delayed(Duration(seconds: 0), () {
      setState(() {
        value = sixtyEight;
        valueTextFields = zero;
      });
    });
  }

  @override
  void initState() {
    super.initState();
    animatedTest();
  }

  @override
  Widget build(BuildContext context) {
    return KeyboardHideable(
      child: Scaffold(
        backgroundColor: darkPrimaryColor,
        body: SingleChildScrollView(
          child: Container(
            height: MediaQuery.of(context).size.height,
            child: SafeArea(
              child: Center(
                child: Padding(
                  padding: const EdgeInsets.all(sixteen),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Expanded(
                        child: Card(
                          elevation: twelve,
                          shape: RoundedRectangleBorder(
                            borderRadius:
                                BorderRadius.all(Radius.circular(twentyFour)),
                          ),
                          child: Padding(
                            padding: const EdgeInsets.all(thirtyTwo),
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: <Widget>[
                                Spacer(),
                                Expanded(
                                  flex: 8,
                                  child: AnimatedContainer(
                                      margin: EdgeInsets.only(bottom: value),
                                      duration: Duration(seconds: 1),
                                      child: Image.asset(
                                          "assets/images/logo_covid_app.png")),
                                ),
                                Expanded(
                                  flex: 7,
                                  child: AnimatedContainer(
                                    margin:
                                        EdgeInsets.only(top: valueTextFields),
                                    duration: Duration(seconds: 1),
                                    child: Column(
                                      children: <Widget>[
                                        Expanded(
                                          flex: 2,
                                          child: Observer(
                                            builder: (_) =>
                                                TextFormFieldComponent(
                                                    emailHintText,
                                                    false,
                                                    controllerEmail,
                                                    vm.changeEmail,
                                                    vm.emailErrorLabel,
                                                    emailErrorLabel),
                                          ),
                                        ),
                                        Expanded(
                                          flex: 2,
                                          child: Observer(builder: (_) {
                                            return TextFormFieldComponent(
                                                passwordHintText,
                                                true,
                                                controllerPassword,
                                                vm.changePassword,
                                                vm.emailErrorLabel,
                                                passwordErrorLabel);
                                          }),
                                        ),
                                      ],
                                    ),
                                  ),
                                ),
                                SizedBox(
                                  height: twentyEight,
                                ),
                                Expanded(
                                  flex: 2,
                                  child: Observer(
                                    builder: (_) => ButtonComponent(
                                      title: loginButtonLabel,
                                      fillColor: rosePrimaryColor,
                                      textColor: Colors.white,
                                      loginFun: vm.formIsValid
                                          ? () => vm.firebaseLogin(context)
                                          : null,
                                    ),
                                  ),
                                ),
                                SizedBox(
                                  height: twenty,
                                ),
                                Expanded(
                                  flex: 2,
                                  child: ButtonComponent(
                                      title: registerButtonLabel,
                                      fillColor: darkPrimaryColor,
                                      textColor: Colors.white,
                                      loginFun: () {}),
                                ),
                                Spacer()
                              ],
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

My TextFormField Component:

import 'package:covid_app/core/constants/colors.dart';
import 'package:covid_app/core/constants/dimens.dart';
import 'package:flutter/material.dart';

// ignore: must_be_immutable
class TextFormFieldComponent extends StatefulWidget {
  String hintText;
  bool hideText;
  TextEditingController genericControler;
  bool genericValidation;
  String errorMessage;
  Function onChangedGeneric;

  TextFormFieldComponent(this.hintText, this.hideText, this.genericControler,
      this.onChangedGeneric, this.genericValidation, this.errorMessage);

  @override
  _TextFormFieldComponentState createState() => _TextFormFieldComponentState();
}

class _TextFormFieldComponentState extends State<TextFormFieldComponent> {
  @override
  Widget build(BuildContext context) {
    return Theme(
      data:
          ThemeData(cursorColor: rosePrimaryColor, hintColor: darkPrimaryColor),
      child: TextFormField(
        onChanged: widget.onChangedGeneric,
        controller: widget.genericControler,
        obscureText: widget.hideText,
        decoration: InputDecoration(
          hintText: widget.hintText,
          errorText: widget.genericValidation == true ? widget.errorMessage : null,
          border: OutlineInputBorder(
            borderRadius: BorderRadius.all(Radius.circular(twentyFour)),
          ),
          enabledBorder: OutlineInputBorder(
            borderRadius: BorderRadius.all(Radius.circular(twentyFour)),
            borderSide: BorderSide(width: two, color: darkPrimaryColor),
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.all(Radius.circular(twentyFour)),
            borderSide: BorderSide(
              width: two,
              color: rosePrimaryColor,
            ),
          ),
        ),
      ),
    );
  }
}

My Button Component:

import 'package:covid_app/core/constants/colors.dart';
import 'package:covid_app/core/constants/dimens.dart';
import 'package:flutter/material.dart';

// ignore: must_be_immutable
class ButtonComponent extends StatefulWidget {
  var title;
  var fillColor;
  var textColor;
  Function loginFun;
  ButtonComponent({Key key, this.title, this.fillColor, this.textColor, this.loginFun});

  @override
  _ButtonComponentState createState() => _ButtonComponentState();
}

class _ButtonComponentState extends State<ButtonComponent> {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: hundredSeventyTwo,
      height: fortyFour,
      child: RaisedButton(
        disabledColor: Colors.grey,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(
            twentyFour,
          ),
        ),
        onPressed: widget.loginFun,
        color: widget.fillColor,
        child: Text(
          widget.title,
          style: TextStyle(
            color: widget.textColor,
          ),
        ),
      ),
    );
  }
}

Print of the Error: https://i.sstatic.net/UmQd5.png / https://i.sstatic.net/K13rN.png


Solution

  • Well, the error says it all, nothing to add really.

    Inside formIsValid computed you invoke 2 functions which in turn might modify emailErrorLabel or passwordErrorLabel and, since they both observable and are used in the same render, this is not allowed.

    computed should be pure function without side effects, it should just derive some value from other computed, observable or constant values.