Related
I have a widget in my settings screen something like this:
Widget autoplay()
{
return ChangeNotifierProvider<AutoplayToggle>(
create: (context) => AutoplayToggle(),
child: Consumer<AutoplayToggle>(
builder: (context, provider, child) {
return Container(
color: provider.isPause ? accent : primary,
width: 45,
child: Switch.adaptive(
value: isPause,
onChanged: (value) async {
setState(() {
isPause= value;
});
await UserPrefs.setAutoplay(isPause);
provider.toggleAutoplay();
},
),
);
},
),
),
}
and this is my class:
class AutoplayToggle with ChangeNotifier{
bool isPause = false;
void toggleAutoplay()
{
isPause = !isPause;
print(isPause);
notifyListeners();
}
}
I printed couple of statements to debug and every time I toggle the switch the function is being called as the values will change from false to true, however, it is not notifying the change. Any idea on whats going wrong?
can you try add "lazy" : false to ChangeNotifierProvider ?
Don't use setState in onChange method
return ChangeNotifierProvider<AutoplayToggle>(
create: (context) => AutoplayToggle(),
child: Consumer<AutoplayToggle>(
builder: (context, provider, child) {
return Container(
color: provider.isPause ? accent : primary,
width: 45,
child: Switch.adaptive(
value: provider.isPause,
onChanged: (value) async {
await UserPrefs.setAutoplay(isPause);
provider.toggleAutoplay();
},
),
);
},
),
);
I have 2 text fields that checks if empty On value change but the problem is if I press the submit function and the text fields are empty it does not show the error. How do I implement checking values on submit and show the error on text field?
UI
Widget buildColumn() => Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: buildTitle(),
),
SizedBox(
height: 10,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: buildDesription(),
),
buildSubmit()
],
));
Widget buildTitle() => StreamBuilder<String>(
stream: manager.title,
builder: (context, snapshot) {
debugPrint(snapshot.toString());
return TextField(
onChanged: manager.inTitle.add,
decoration: InputDecoration(
labelText: 'Title',
labelStyle: TextStyle(fontWeight: FontWeight.bold),
errorText:
snapshot.error == null ? null : snapshot.error.toString()),
);
});
Widget buildDesription() => StreamBuilder<String>(
stream: manager.description,
builder: (context, snapshot) {
return TextField(
onChanged: manager.inDescription.add,
decoration: InputDecoration(
labelText: 'Description',
labelStyle: TextStyle(fontWeight: FontWeight.bold),
errorText:
snapshot.error == null ? null : snapshot.error.toString()),
);
});
Widget buildSubmit() => StreamBuilder<Object>(
stream: manager.isFormValid,
builder: (context, snapshot) {
return ElevatedButton(
onPressed: () {
if (snapshot.hasData) {
manager.submit();
debugPrint("YEs");
}
},
child: Text("SEND"));
});
}
Manager
On submit does not check if title and description is empty
class CreatePostManager with Validation {
final _repository = PostRepository();
final _postsFetcher = PublishSubject<Post>();
Stream<Post> get addPost => _postsFetcher.stream;
final _title = BehaviorSubject<String>();
Stream<String> get title => _title.stream.transform(validateTitle);
Sink<String> get inTitle => _title.sink;
final _description = BehaviorSubject<String>();
Stream<String> get description =>
_description.stream.transform(validateDescription);
Sink<String> get inDescription => _description.sink;
final _loading = BehaviorSubject<bool>();
Stream<bool> get loading => _loading.stream.transform(checkLoading);
Sink<bool> get inLoading => _loading.sink;
Stream<bool> get isFormValid =>
Rx.combineLatest2(title, description, (a, b) => true);
Future submit() async {
inLoading.add(true);
String title = _title.value;
String description = _description.value;
Post itemModel = await _repository.addPost(title, description);
_postsFetcher.sink.add(itemModel);
inLoading.add(false);
return itemModel;
}
dispose() {
_postsFetcher.close();
}
}
Validator
mixin Validation {
final validateTitle =
StreamTransformer<String, String>.fromHandlers(handleData: (value, sink) {
if (value.isEmpty) {
sink.addError("Title cannot be empty");
}else {
sink.add(value);
}
});
final validateDescription =
StreamTransformer<String, String>.fromHandlers(handleData: (value, sink) {
if (value.isEmpty) {
sink.addError("Description cannot be empty");
}else {
sink.add(value);
}
});
final checkLoading =
StreamTransformer<bool, bool>.fromHandlers(handleData: (value, sink) {
sink.add(value);
});
}
On submit I want to check if textfields are empty and show the Error message. Currently it only shows when user types.
On submission, the TextField can be validated using its Controllers.
_textFieldController = TextEditingController();
...
TextField(
controller: _textFieldController,
)
The TextField value can then be fetched using TextEditingController().value.text. Add a checker against this value on submission.
Another way of validating TextFields is with the use of the validator in TextFormField
TextFormField(
// When a String is returned, it's displayed as an error on the TextFormField
validator: (String? value) {
return (value == null || value == '') ? 'Field should not be empty.' : null;
},
)
i want to build Dropdown list from Future,here is my function for simple list view which is working,,but how to populate drop down list from it, i am really confuse about this list map etc in flutter coming from php background,
child: FutureBuilder(
future:userApi.getUsers(),
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.data == null){
return Container(
child: Center(
child: Text("Loading...")
)
);
} else {
return Container(
child: DropdownButton(
items: snapshot.data.map((item) {
return DropdownMenuItem(child: Text(item.title));
}).toList(),
onChanged: (value){},
)
);
}
},
),
class UserApi{
Future<List<User>>getUsers() async {
var data = await http.get("https://jsonplaceholder.typicode.com/albums/");
var jsonData = json.decode(data.body);
List<User> users = [];
for(var u in jsonData){
User user = User(u["id"], u["title"]);
users.add(user);
}
return users;
}
class User {
final int id;
final String title;
User(this.id,this.title);
}
Ok I just read the comment left above if your problem is getting your data then this might not help you but if snapshot has data this will work
//initialize this outside your build method
String dropDownValue;
FutureBuilder(
future:userApi.getUsers(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return snapshot.hasData
? Container(
child: DropdownButton<String>(
hint: Text(dropDownValue ?? 'Make a selection'),
items: snapshot.data.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(
value: item.title,
child: Text(item.title),
);
}).toList(),
onChanged: (value) {
setState(() {
dropDownValue = value;
print(value);
});
},
),
)
: Container(
child: Center(
child: Text('Loading...'),
),
);
},
),
you can call the map function on any list converting the elements of the list.
return DropdownButton(
items: snapshot.data.map((item) {
return DropdownMenuItem(child: Text(item.title));
}).toList(),
onChanged: (value){},
);
if you can see here we're converting the list snapshot.data to another list of DropdownMenuItem type by calling map on snapshot.data the map function takes another function that's being called on every element of snapshot.data , map returns an Iterable of type DropdownMenuItem (the returned type of the function being called on every element) and we convert the Iterable to a list by calling toList() on it
I wish that is explanatory enough, the map function is very useful.
I'm building an app with Flutter and Bloc like architecture. I'm trying to call submit func with not only login button but also keyboard done button with password; only when email and password are valid.
I could implement login button version with combineLatest, but I'm not sure keyboard version. I need to validate both email and password when keyboard done button pressed before calling submit. I could nest streamBuilder, but I feel it is not good practice.
Is there any way to get the latest value from combineLatest? BehaviorSubject<bool>().value Or any possible advice to implement this.
sample code:
final _emailController = BehaviorSubject<String>();
final _passwordController = BehaviorSubject<String>();
// Add data to stream
Stream<String> get email => _emailController.stream.transform(validateEmail);
Stream<String> get password =>
_passwordController.stream.transform(validatePassword);
Stream<bool> get submitValid =>
Observable.combineLatest2(email, password, (e, p) => true);
// change data
Function(String) get changeEmail => _emailController.sink.add;
Function(String) get changePassword => _passwordController.sink.add;
submit() {
final validEmail = _emailController.value;
final validPassword = _passwordController.value;
print('Email is $validEmail, and password is $validPassword');
}
import 'package:flutter/material.dart';
import '../blocs/bloc.dart';
import '../blocs/provider.dart';
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final bloc = Provider.of(context);
return Container(
margin: EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
emailField(bloc),
passwordField(bloc),
Container(
margin: EdgeInsets.only(top: 25.0),
),
submitButton(bloc),
],
),
);
}
Widget emailField(Bloc bloc) {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changeEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'ypu#example.com',
labelText: 'Email Address',
errorText: snapshot.error,
),
);
},
);
}
Widget passwordField(Bloc bloc) {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
obscureText: true,
onChanged: bloc.changePassword,
decoration: InputDecoration(
hintText: 'Password',
labelText: 'Password',
errorText: snapshot.error,
),
textInputAction: TextInputAction.done,
onSubmitted: () {}, // <- here
);
});
}
Widget submitButton(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: snapshot.hasData ? bloc.submit : null,
);
},
);
}
}
You can wrap Your Password streamBuilder with another streamBilder and onSubmitted call submit method.
Widget passwordField(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snap) {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
obscureText: true,
onChanged: bloc.changePassword,
decoration: InputDecoration(
hintText: 'Password',
labelText: 'Password',
errorText: snapshot.error,
),
textInputAction: TextInputAction.done,
onSubmitted: snap.hasData ? bloc.submit : null,
);
});
});
}
I have a Registration page with TextField Form Validators in my App with a button. The text field will display form validation error messages if business rules haven't been met and the "next" button will be tap-able once all criteria has been met. This is currently all working well in my app but I find that once I leave the page and return to it, the validation error messages stops displaying and the button stops working as well. Looking at the console logs in my IDE (android studio) the only relevant error message I am getting is
[VERBOSE-2:ui_dart_state.cc(148)] Unhandled Exception: Bad state:
Stream is already closed
#0 _SinkTransformerStreamSubscription._add
(dart:async/stream_transformers.dart:66:7)
#1 _EventSinkWrapper.add
(dart:async/stream_transformers.dart:15:11)
I'm not exactly sure what this means, does the stream close and not reopen once the page has been reloaded? If not, is there a way i could fix this or is there something i'm missing? This is what i'm experiencing
Stream Builder Code:
Widget emailField(authBloc) {
return StreamBuilder(
stream: authBloc.emailStream,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
return TextField(
onChanged: authBloc.updateEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.deepOrange
)
),
hintText: 'Enter Email',
labelText: 'Email Address',
errorText: snapshot.error
),
);
},
);
}
Widget passwordField( authBloc) {
return StreamBuilder(
stream: authBloc.passwordStream,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
return TextField(
onChanged: authBloc.updatePassword,
obscureText: true,
decoration: InputDecoration(
hintText: 'Enter password',
labelText: 'Password',
errorText: snapshot.error,
),
);
},
);
}
Widget checkPasswordField( authBloc) {
return StreamBuilder(
stream: authBloc.validatePasswordStream,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
return TextField(
onChanged: authBloc.updateValidatePassword,
obscureText: true,
decoration: InputDecoration(
hintText: 'Re-enter password',
labelText: 'Confirm Password',
errorText: snapshot.error,
),
);
},
);
}
Widget nextBtn(authBloc) {
return StreamBuilder(
stream: authBloc.submitValid,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
return RaisedButton(
child: Text('Next'),
color: Colors.deepOrange,
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7.0))
),
onPressed: snapshot.hasData
? () => Navigator.pushNamed(context, '/register')
: null,
);
}
);
}
Streams:
/// REGISTER VARIABLES
static final _emailController = BehaviorSubject<
String>(); //RxDart's implementation of StreamController. Broadcast stream by default
static final _passwordController = BehaviorSubject<String>();
static final _validatePasswordController = BehaviorSubject<
String>(); // Will check that the password entered the 2nd time is correct
/// REGISTER STREAM & METHODS
//Retrieve data from the stream
Stream<String> get emailStream => _emailController.stream
.transform(performEmailValidation); //Return the transformed stream
Stream<String> get passwordStream =>
_passwordController.stream.transform(performPasswordValidation);
Stream<String> get validatePasswordStream =>
_validatePasswordController.stream.transform(performIsPasswordSame);
//Merging email, password and validate password
Stream<bool> get submitValid => Observable.combineLatest3(
emailStream, passwordStream, validatePasswordStream, (e, p1, p2) => true);
//Add data to the stream
Function(String) get updateEmail => _emailController.sink.add;
Function(String) get updatePassword => _passwordController.sink.add;
Function(String) get updateValidatePassword =>
_validatePasswordController.sink.add;
// performing user input validations
final performEmailValidation = StreamTransformer<String, String>.fromHandlers(
handleData: (email, sink) async {
String emailValidationRule =
r'^(([^<>()[\]\\.,;:\s#\"]+(\.[^<>()[\]\\.,;:\s#\"]+)*)|(\".+\"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regExp = new RegExp(emailValidationRule);
if (await doesNameAlreadyExist("email", _emailController.value) == true)
sink.addError("That email already exists");
else if (regExp.hasMatch(email)) {
sink.add(email);
} else {
sink.addError(StringConstant.emailErrorMessage);
}
});
final performPasswordValidation =
StreamTransformer<String, String>.fromHandlers(
handleData: (_passwordController, sink) {
if (_passwordController.length >= 6) {
sink.add(_passwordController);
} else {
sink.addError(StringConstant.passwordErrorMessage);
}
});
final performIsPasswordSame = StreamTransformer<String, String>.fromHandlers(
handleData: (password, sink) {
if (password != _passwordController.value)
sink.addError(StringConstant.invalidPasswordMessage);
else
sink.add(password);
});
Entire Screen Code:
class SignUp extends StatefulWidget {
#override
_SignUpState createState() => _SignUpState();
}
class _SignUpState extends State<SignUp> {
AuthBloc _authBloc;
#override
void didChangeDependencies() {
super.didChangeDependencies();
_authBloc = AuthBlocProvider.of(context);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
SizedBox(height: 80,),
Text("Register", style: Style.appTextStyle),
SizedBox(height: 100,),
emailField(_authBloc),
SizedBox(height: 30),
passwordField(_authBloc),
SizedBox(height: 30),
checkPasswordField(_authBloc),
SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
cancelBtn(),
nextBtn(_authBloc),
],
)
// checkPasswordField(authBloc),
],
),
)
),
);
}
Widget emailField(authBloc) {
return StreamBuilder(
stream: authBloc.emailStream,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
return TextField(
onChanged: authBloc.updateEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.deepOrange
)
),
hintText: 'Enter Email',
labelText: 'Email Address',
errorText: snapshot.error
),
);
},
);
}
Widget passwordField( authBloc) {
return StreamBuilder(
stream: authBloc.passwordStream,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
return TextField(
onChanged: authBloc.updatePassword,
obscureText: true,
decoration: InputDecoration(
hintText: 'Enter password',
labelText: 'Password',
errorText: snapshot.error,
),
);
},
);
}
Widget checkPasswordField( authBloc) {
return StreamBuilder(
stream: authBloc.validatePasswordStream,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
return TextField(
onChanged: authBloc.updateValidatePassword,
obscureText: true,
decoration: InputDecoration(
hintText: 'Re-enter password',
labelText: 'Confirm Password',
errorText: snapshot.error,
),
);
},
);
}
Widget nextBtn(authBloc) {
return StreamBuilder(
stream: authBloc.submitValid,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
return RaisedButton(
child: Text('Next'),
color: Colors.deepOrange,
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7.0))
),
onPressed: snapshot.hasData
? () => Navigator.pushNamed(context, '/register')
: null,
);
}
);
}
Widget cancelBtn(){
return RaisedButton(
child: Text('Cancel'),
color: Colors.white30,
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7.0))
),
onPressed: () => Navigator.pop(context),
);
}
#override
void dispose() {
super.dispose();
_authBloc.dispose();
}
Bloc Code:
/// REGISTER VARIABLES
static final _emailController = BehaviorSubject<
String>(); //RxDart's implementation of StreamController. Broadcast stream by default
static final _passwordController = BehaviorSubject<String>();
static final _validatePasswordController = BehaviorSubject<
String>(); // Will check that the password entered the 2nd time is correct
/// REGISTER STREAM & METHODS
//Retrieve data from the stream
Stream<String> get emailStream => _emailController.stream
.transform(performEmailValidation); //Return the transformed stream
Stream<String> get passwordStream =>
_passwordController.stream.transform(performPasswordValidation);
Stream<String> get validatePasswordStream =>
_validatePasswordController.stream.transform(performIsPasswordSame);
//Merging email, password and validate password
Stream<bool> get submitValid => Observable.combineLatest3(
emailStream, passwordStream, validatePasswordStream, (e, p1, p2) => true);
//Add data to the stream
Function(String) get updateEmail => _emailController.sink.add;
Function(String) get updatePassword => _passwordController.sink.add;
Function(String) get updateValidatePassword =>
_validatePasswordController.sink.add;
// performing user input validations
final performEmailValidation = StreamTransformer<String, String>.fromHandlers(
handleData: (email, sink) async {
String emailValidationRule =
r'^(([^<>()[\]\\.,;:\s#\"]+(\.[^<>()[\]\\.,;:\s#\"]+)*)|(\".+\"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regExp = new RegExp(emailValidationRule);
if (await doesNameAlreadyExist("email", _emailController.value) == true)
sink.addError("That email already exists");
else if (regExp.hasMatch(email)) {
sink.add(email);
} else {
sink.addError(StringConstant.emailErrorMessage);
}
});
final performPasswordValidation =
StreamTransformer<String, String>.fromHandlers(
handleData: (_passwordController, sink) {
if (_passwordController.length >= 6) {
sink.add(_passwordController);
} else {
sink.addError(StringConstant.passwordErrorMessage);
}
});
final performIsPasswordSame = StreamTransformer<String, String>.fromHandlers(
handleData: (password, sink) {
if (password != _passwordController.value)
sink.addError(StringConstant.invalidPasswordMessage);
else
sink.add(password);
});
dispose() {
_emailController.close();
_passwordController.close();
_validatePasswordController.close();
}
Well, looking to full source and the GIF that you show I can see what is causing this problem.
Your mistake is call dispose() BLoC instance method in dispose() SingUp widget class method.
Why is a mistake?
In your specific case when you're in SingUp screen and go to a next route/screen dispose method from SingUp is called and in this moment the streams of your BLoC instance is going to be closed. But the next allows the user go back to SingUp screen and when this happen SingUp instance gets the same BLoC instance that was used before but this BLoC instance has the streams already closed.
How can I solve this in a simple way?
In SingUp class :
#override
void dispose() {
super.dispose();
/// DON'T CALL BLoC dispose here
/// _authBloc.dispose();
}
Don't dispose you BloC here because the user can back to this screen any moment. Since you're using InheritedWidget to get BLoC instance and this gives you access the same BLoC instance in different places I advise you call yourBloc.dispose() in the moment where the user is ending all the sing up process.