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
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.