Using combineLatest to enable/disable a button, but button gets enabled even if some stream contains an error - dart

Want to enable/disable a button based on user input. If all text input matches a certain condition, only then enable the "insert" button.
Normally button gets only enabled, if all the input field is correct. But if one or more incorrect, and user moves to another input field, and change it to correct/incorrect, button gets enabled, even if some field has wrong input. Check image:
Code for login bloc:
import 'package:rxdart/rxdart.dart';
class LoginScreenBloc {
final _firstCtrl = BehaviorSubject<String>();
final _lastCtrl = BehaviorSubject<String>();
final _userNameCtrl = BehaviorSubject<String>();
final _passwordCtrl = BehaviorSubject<String>();
Function(String) get changeFirst => _firstCtrl.sink.add;
Function(String) get changeLast => _lastCtrl.sink.add;
Function(String) get changeUser => _userNameCtrl.sink.add;
Function(String) get changePass => _passwordCtrl.sink.add;
final fieldSize = StreamTransformer<String, String>.fromHandlers(
handleData: (value, sink) {
if (value.length > 3) {
sink.add(value);
} else {
sink.addError("Can't be Empty!");
}
},
);
Stream<String> get firstname => _firstCtrl.stream.transform(fieldSize);
Stream<String> get lastname => _lastCtrl.stream.transform(fieldSize);
Stream<String> get username => _userNameCtrl.stream.transform(fieldSize);
Stream<String> get password => _passwordCtrl.stream.transform(fieldSize);
void insertValue() {
print("${_firstCtrl.value}");
print("${_lastCtrl.value}");
print("${_userNameCtrl.value}");
print("${_passwordCtrl.value}");
}
Stream<bool> get insertButton {
return CombineLatestStream(
[firstname, lastname, username, password],
(values) {
return true;
},
);
}
dispose() {
_firstCtrl.close();
_lastCtrl.close();
_userNameCtrl.close();
_passwordCtrl.close();
}
}
Code for the button:
Widget insertValue(BuildContext context, LoginScreenBloc bloc) {
return StreamBuilder<Object>(
stream: bloc.insertButton,
builder: (context, snapshot) {
return RaisedButton(
child: Text("Insert"),
onPressed: snapshot.hasData ? bloc.insertValue : null,
);
},
);
}

try to used opacity connect for button enable and put the condition for click and visiblity.. like this way..
Container(
child: Opacity(opacity: isValid ? 1.0 : 0.7,
child: RaisedButton(
color: Colors.red,
onPressed: _loginPressed,
child: Text('Sign In',
style: TextStyle(fontSize: 15.0, color: Colors.white)),
),
),
),

Something Similar you can find here
Stream<bool> get insertButton {
return CombineLatestStream(
[firstname, lastname, username, password],
(values) {
for(String i in values) {
if(i.length < 3) return false;
}
return true;
},
);
}
combineLatest only checks for event. It doesn't takes care of the error which gets out from the stream.

I found a solution that worked for me.
I had the same issue while building a login form.
This is my code:
class LoginFormManager with FormValidatorMixin {
final _email = BehaviorSubject<String>();
Stream<String> get email$ => _email.stream.transform(emailValidator);
Function(String) get setEmail => _email.sink.add;
final _password = BehaviorSubject<String>();
Stream<String> get password$ => _password.stream.transform(passwordValidator);
Function(String) get setPassword => _password.sink.add;
Stream<bool> get isFormValid$ =>
CombineLatestStream.combine2<String, String, bool>(email$, password$,
(email, password) {
if (email == _email.value && password == _password.value) {
return true;
} else
return false;
});
void submit() {
print(_email.value);
print(_password.value);
}
void dispose() {
_email.close();
_password.close();
}
}
The subjects _email and the _password store the last value received including errors.
The email$ and password$ streams only send data when the input received from the user passes the validators.
So in my case I just had to check if the email and password$ sent by email$ and password$ match the last value stored in the subjects of the _email and the _password.

Related

How to clear data in bloc?

As is it right now, when a user tries to click login, the button changes to a progress bar. The trouble i'm having is that I'm not able to get the progress bar turned back into a button using streams.
I'm kinda copying a tutorial I found online and I'm trying to modify it to fit what I need so I'm not 100% understanding bloc yet.
This is the tutorial I'm kinda following
https://medium.com/flutterpub/when-firebase-meets-bloc-pattern-fb5c405597e0
This is the login button
Widget loginButton() {
return StreamBuilder(
stream: _bloc.signInStatus,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (!snapshot.hasData || snapshot.hasError) {
return button();
} else {
return LinearProgressIndicator();
}
});
}
Widget button() {
return Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: MaterialButton(
minWidth: 200.0,
height: 42.0,
child: Text(
StringConstant.submit,
style: TextStyle(color: Colors.white),
),
color: ThemeSettings.RaisedButton,
onPressed: () {
if (_bloc.validateFields()) {
authenticateUser();
} else {
showAlertDialog(context, "Verification Failed",
"The Email / Number you entered couldn't be found or your password was incorrect. Please try again.");
}),
);
}
void authenticateUser() {
_bloc.showProgressBar(true);
_bloc.authenticateUser().then((value) {
//Username and password ARE correct
if (value) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => EventsList(_bloc.emailAddress)));
} else {
print("this one");
showAlertDialog(context, "Verification Failed",
"The Email / Number you entered couldn't be found or your password was incorrect. Please try again.");
// I believe I need to clear what is inside of snapshot here?
}
});
}
and inside of my login_bloc.dart here is what I think is important to know?
class LoginBloc {
final _repository = Repository();
final _email = BehaviorSubject<String>();
final _password = BehaviorSubject<String>();
final _firstName = BehaviorSubject<String>();
final _phoneNumber = BehaviorSubject<int>();
final _profilePicture = BehaviorSubject<String>();
final _isSignedIn = BehaviorSubject<bool>();
Observable<String> get email => _email.stream.transform(_validateEmail);
Observable<String> get password =>
_password.stream.transform(_validatePassword);
Observable<bool> get signInStatus => _isSignedIn.stream;
String get emailAddress => _email.value;
// Change data
Function(String) get changeEmail => _email.sink.add;
Function(String) get changePassword => _password.sink.add;
Function(bool) get showProgressBar => _isSignedIn.sink.add;
final _validateEmail =
StreamTransformer<String, String>.fromHandlers(handleData: (email, sink) {
if (email.contains('#')) {
sink.add(email);
} else {
sink.addError(StringConstant.emailValidateMessage);
}
});
final _validatePassword = StreamTransformer<String, String>.fromHandlers(
handleData: (password, sink) {
if (password.length >= 6) {
sink.add(password);
} else {
sink.addError(StringConstant.passwordValidateMessage);
}
});
Future<bool> authenticateUser() {
return _repository.authenticateUser(_email.value, _password.value);
}
Future<AuthenticatedStatus> doesUserExist() {
return _repository.doesUserExist(_email.value);
}
Future<void> addUserToDatabase() {
return _repository.addUserToDatabase(_email.value, _password.value,
_firstName.value, _phoneNumber.value, _profilePicture.value);
}
bool validateFields() {
if (_email.value != null &&
_email.value.isNotEmpty &&
_password.value != null &&
_password.value.isNotEmpty &&
_email.value.contains('#') &&
_password.value.length >= 6) {
return true;
} else {
return false;
}
}
void dispose() async {
await _email.drain();
_email.close();
await _password.drain();
_password.close();
await _firstName.drain();
_firstName.close();
await _phoneNumber.drain();
_phoneNumber.close();
await _profilePicture.drain();
_profilePicture.close();
await _isSignedIn.drain();
_isSignedIn.close();
}
I have yet to try the sample provided in the blog post, but going through the snippets, it looks like a simple _bloc.showProgressBar(false); should do the trick. Have you tried doing so?

Flutter BloC Pattern: Update BloC Streams Based Another BloC's Stream

SCENARIO
I'm trying to create a Flutter app that has two screens: ContactsScreen and EditContactScreen. In ContactsScreen, the user will be presented with a DropdownButton and Text. The DropdownButton holds a list of Contact objects that have been fetched through an api. Whenever a user selects a Contact from the DropdownButton, the Text object will show information regarding that particular contact. Moreover, upon Contact selection, a RaisedButton will appear, which when clicked, will direct the user to the EditContactScreen to edit the selected Contact. I'm using the BloC pattern. I created two BloCs, one for each screen: ContactsScreenBloc and EditContactScreenBloc. ContactsScreenBloc holds a Stream<Contact> and a Sink<Contact> for managing the selected Contact. Whereas EditContactScreenBloc holds streams and sinks for the Contact fields. Finally, I have a GlobalBloc that holds the list of Contacts. The GlobalBloc is an InheritedWidget that wraps up the MaterialApp. The app is oversimplified and is part of a larger one, for that reason, I can't merge ContactsScreenBloc and EditContactScreenBloc, and there should be a GlobalBloc that has the list of Contacts.
QUESTION
I'm actually fairly new to Flutter so I'm not sure if my approach is sound. If it is, then when the user navigates to the EditContactScreen and successfully updates the Contact, how can I reflect it back in the selected Contact in ContactsScreen?
CODE SNIPPITS
contact.dart
class Contact {
final String id;
final String firstName;
final String lastName;
final String phoneNumber;
Contact({this.id, this.firstName, this.lastName, this.phoneNumber});
Contact.fromJson(Map<String, dynamic> parsedJson)
: id = parsedJson['id'],
firstName = parsedJson['firstName'],
lastName = parsedJson['lastName'],
phoneNumber = parsedJson['phoneNumber'];
copyWith({String firstName, String lastName, String phoneNumber}) => Contact(
id: id,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
phoneNumber: phoneNumber ?? this.phoneNumber
);
#override
bool operator ==(other) => other.id == this.id;
#override
int get hashCode => id.hashCode;
}
global.bloc.dart
class GlobalBloc {
final _repo = Repo();
final _contacts = BehaviorSubject<List<Contact>>(seedValue: []);
Stream<List<Contact>> get contacts => _contacts.stream;
Function(List<Contact>) get updateContacts => _contacts.sink.add;
Future refreshContacts() async{
final contacts = await _repo.getContacts();
updateContacts(contacts);
}
}
contacts_screen.bloc.dart
class ContactsScreenBloc {
final _selectedContact = BehaviorSubject<Contact>(seedValue: null);
Stream<Contact> get selectedContact => _selectedContact.stream;
Function(Contact) get changeSelectedContact => _selectedContact.sink.add;
}
edit_contacts_screen.bloc.dart
class ContactsScreenBloc {
final _selectedContact = BehaviorSubject<Contact>(seedValue: null);
Stream<Contact> get selectedContact => _selectedContact.stream;
Function(Contact) get changeSelectedContact => _selectedContact.sink.add;
}
global.provider.dart
class GlobalProvider extends InheritedWidget {
final bloc = GlobalBloc();
static GlobalBloc of(BuildContext context) => (context.inheritFromWidgetOfExactType(GlobalProvider) as GlobalProvider).bloc;
bool updateShouldNotify(_) => true;
}
contacts.screen.dart
class ContactsScreen extends StatelessWidget {
final bloc = ContactsScreenBloc();
#override
Widget build(BuildContext context) {
final globalBloc = GlobalProvider.of(context);
return Column(
children: <Widget>[
StreamBuilder<List<Contact>>(
stream: globalBloc.contacts,
builder: (context, listSnapshot) {
return StreamBuilder<Contact>(
stream: bloc.selectedContact,
builder: (context, itemSnapshot) {
return DropdownButton<Contact>(
items: listSnapshot.data
?.map(
(contact) => DropdownMenuItem<Contact>(
value: contact,
child: Text(contact.firstName + ' ' + contact.lastName),
),
)
?.toList(),
onChanged: bloc.changeSelectedContact,
hint: Text('Choose a contact.'),
value: itemSnapshot.hasData ? itemSnapshot.data : null,
);
},
);
},
), // end for DropdownButton StreamBuilder
StreamBuilder<Contact>(
stream: bloc.selectedContact,
builder: (context, snapshot) {
return snapshot.hasData
? Row(children: <Widget>[
Text(snapshot.data.firstName + ' ' + snapshot.data.lastName + ' ' + snapshot.data.phoneNumber),
FlatButton(
child: Text('Edit Contact'),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
EditContactScreen(contact: snapshot.data)));
},
),
],
)
: null;
}, // End for text description
)
],
);
}
}
edit_contact.screen.dart
class EditContactScreen extends StatelessWidget {
final bloc = EditContactScreenBloc();
final Contact contact;
EditContactScreen({this.contact});
#override
Widget build(BuildContext context) {
final globalBloc = GlobalProvider.of(context);
return Column(
children: <Widget>[
TextField(onChanged: (firstName) => bloc.updateContact(contact.copyWith(firstName: firstName))),
TextField(onChanged: (lastName) => bloc.updateContact(contact.copyWith(firstName: lastName))),
TextField(onChanged: (phoneNumber) => bloc.updateContact(contact.copyWith(firstName: phoneNumber))),
RaisedButton(child: Text('Update'), onPressed: () async {
await bloc.update();
await globalBloc.refreshContacts();
Navigator.of(context).pop();
},)
],
);
}
}
Okay, I was able to solve my issue:
In the contacts_screen.bloc.dart, I added the following method:
void updateContactInfo(List<Contact> contacts) {
final contact = _selectedContact.value;
if (contact == null) return;
final updatedContact = contacts.firstWhere((a) => a.id == contact.id);
if (updatedContact == null) return;
changeSelectedContact(updatedContact);
}
And updated the StreamBuilder<List<Contact>> for building the DropdownButton to be like this:
StreamBuilder<List<Contact>>(
stream: globalBloc.contacts,
builder: (context, listSnapshot) {
bloc.updateContactInfo(listSnapshot.data);
return StreamBuilder<Contact>(
stream: bloc.selectedContact,
builder: (context, itemSnapshot) {
return DropdownButton<Contact>(
items: listSnapshot.data
?.map(
(contact) => DropdownMenuItem<Contact>(
value: contact,
child: Text(
contact.firstName + ' ' + contact.lastName),
),
)
?.toList(),
onChanged: bloc.changeSelectedContact,
hint: Text('Choose a contact.'),
value: itemSnapshot.hasData ? itemSnapshot.data : null,
);
},
);
},
)

Flutter: inherited widget

I want to do a login app. I have a class user, which has an id and a username that I want to keep to display it later in the app, and I have a user_api class, where I do the http request.
I wanted to use Singleton to store the user once the user logins in, but I find out that inherited widget was a better idea. So now I'm struggling with them because I can't store the user object. After I login, my user becomes null and I can't figure out how it works. Here's my code: basically we have a root page that manages the cases in which the user is logged in or not:
void main() {
runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Model(
user: User(),
child: MaterialApp(
routes: {
'/login': (context) => new LoginView(),
'/homepage_view': (context) => new HomepageView(),
},
title: 'Flutter login demo',
home: RootPage(),
),
);
}
}
In the rootPage:
enum UserStatus {
notDetermined,
notSignedIn,
signedIn,
}
class RootPage extends StatefulWidget {
#override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
UserStatus userStatus = UserStatus.notDetermined;
#override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
var user = Model.of(context).user;
setState(() {
userStatus = user.id == null? UserStatus.notSignedIn : UserStatus.signedIn;
print((userStatus));
});
}
void _signedIn() {
setState(() {
userStatus = UserStatus.signedIn;
});
}
void _signedOut() {
setState(() {
userStatus = UserStatus.notSignedIn;
});
}
#override
Widget build(BuildContext context) {
switch (userStatus) {
case UserStatus.notDetermined:
return _buildWaitingScreen();
case UserStatus.notSignedIn:
return LoginView(
onSignedIn: _signedIn,
);
case UserStatus.signedIn:
return HomepageView(
onSignedOut: _signedOut,
);
}
return Container(
child: Text(("CHILD")),
);
}
}
Widget _buildWaitingScreen() {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: CircularProgressIndicator(),
),
);
}
the most important stuff of the login page::
var user = Model.of(context).user;
user = await getUserByIdClient()
if (user.loginError == false){
print (user);
widget.onSignedIn();
}
Here's my inherited widget:
class Model extends InheritedWidget {
Model({Key key, Widget child, this.user}) : super(key: key, child: child);
final User user;
#override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
static Model of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(Model) as Model);
}
}
From what I understand, it seems that you're looking in to storing user session in your app. One way of doing this is by storing user credentials in shared_preferences (i.e. userId). Depending on your use case, your backend might require users to re-authenticate, so keep an eye on that.
Store user credentials after login.
// Obtain shared preferences.
final prefs = await SharedPreferences.getInstance();
// Save user details on userId
String userId = ...;
await prefs.setString('userId', userId);
When has been signed out, you can remove the data.
await prefs.remove('userId');
For verifying user session, you can then check the stored value. If it's empty, logout the user.
final String? userId = prefs.getString('userId');
if(userId != null){
// User is logged-in
} else {
// User is signed-out
}

Flutter: Refresh another Widget State from Another Route

At the HompePage, am navigating to Settings page with;
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) => Settings()));
the Settings() page contains an int input to allow user specify the number of posts they want to see at the HomePage. When users input the number and form.save, the value is stored in SharedPreferences. But when the user go back to the HomePage, the initial number of post still there. I want the HomePagestate to refresh so that the number of post the user specify at the Settings Page will take effect immediately the form is saved.
Below is some snippets of my code;
This is the form _submit on Settings() page,
_submit() async {
final form = _formKey.currentState;
SharedPreferences prefs = await SharedPreferences.getInstance();
if (form.validate()) {
prefs.setInt('defaultField', newva);
form.save();
final mysb = SnackBar(
duration: Duration(seconds: 1),
content: new Text(
'Data Saved Successfully',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
backgroundColor: Colors.red,
);
_scaffoldKey.currentState?.showSnackBar(mysb);
myHomePageState.setState(() {
newSULength = newva;
});
print('Done for $newva');
}
}
This is my MyHomePage()
MyHomePageState myHomePageState = new MyHomePageState();
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => new MyHomePageState();
static MyHomePageState of(BuildContext context){
final MyHomePageState navigator = context.ancestorStateOfType(const TypeMatcher<MyHomePageState>());
assert(() {
if(navigator == null) {
throw new FlutterError('Error occoured');
}
return true;
}());
return navigator;
}
}
class MyHomePageState extends State<MyHomePage> {
int newSULength = 0;
void initState() {
// TODO: implement initState
super.initState();
loadDF();
}
set newle(String value) => setState(() => _string = value);
loadDF() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
newSULength = (prefs.getInt('defaultField') ?? 5);
for (int i = 0; i < newSULength; i++) {
\\todos
}
});
print('Done');
}
}
You can use callbacks to indicate the HomePage that the Settings page changed some value in shared preference. Refer this

Flutter Reloading List with Streams & RxDart

I have one question regarding how to reload a list after refresh indicator is called in Flutter, using Streams and RxDart.
Here is what I have , my model class:
class HomeState {
List<Event> result;
final bool hasError;
final bool isLoading;
HomeState({
this.result,
this.hasError = false,
this.isLoading = false,
});
factory HomeState.initial() =>
new HomeState(result: new List<Event>());
factory HomeState.loading() => new HomeState(isLoading: true);
factory HomeState.error() => new HomeState(hasError: true);
}
class HomeBloc {
Stream<HomeState> state;
final EventRepository repository;
HomeBloc(this.repository) {
state = new Observable.just(new HomeState.initial());
}
void loadEvents(){
state = new Observable.fromFuture(repository.getEventList(1)).map<HomeState>((List<Event> list){
return new HomeState(
result: list,
isLoading: false
);
}).onErrorReturn(new HomeState.error())
.startWith(new HomeState.loading());
}
}
My widget:
class HomePageRx extends StatefulWidget {
#override
_HomePageRxState createState() => _HomePageRxState();
}
class _HomePageRxState extends State<HomePageRx> {
HomeBloc bloc;
_HomePageRxState() {
bloc = new HomeBloc(new EventRest());
bloc.loadEvents();
}
Future<Null> _onRefresh() async {
bloc.loadEvents();
return null;
}
#override
Widget build(BuildContext context) {
return new StreamBuilder(
stream: bloc.state,
builder: (BuildContext context, AsyncSnapshot<HomeState> snapshot) {
var state = snapshot.data;
return new Scaffold(
body: new RefreshIndicator(
onRefresh: _onRefresh,
child: new LayoutBuilder(builder:
(BuildContext context, BoxConstraints boxConstraints) {
if (state.isLoading) {
return new Center(
child: new CircularProgressIndicator(
backgroundColor: Colors.deepOrangeAccent,
strokeWidth: 5.0,
),
);
} else {
if (state.result.length > 0) {
return new ListView.builder(
itemCount: snapshot.data.result.length,
itemBuilder: (BuildContext context, int index) {
return new Text(snapshot.data.result[index].title);
});
} else {
return new Center(
child: new Text("Empty data"),
);
}
}
}),
),
);
});
}
}
The problem is when I do the pull refresh from list, the UI doesn't refresh (the server is called, the animation of the refreshindicator also), I know that the issue is related to the stream but I don't know how to solve it.
Expected result : Display the CircularProgressIndicator until the data is loaded
Any help? Thanks
You are not supposed to change the instance of state.
You should instead submit a new value to the observable. So that StreamBuilder, which is listening to state will be notified of a new value.
Which means you can't just have an Observable instance internally, as Observable doesn't have any method for adding pushing new values. So you'll need a Subject.
Basically this changes your Bloc to the following :
class HomeBloc {
final Stream<HomeState> state;
final EventRepository repository;
final Subject<HomeState> _stateSubject;
factory HomeBloc(EventRepository respository) {
final subject = new BehaviorSubject(seedValue: new HomeState.initial());
return new HomeBloc._(
repository: respository,
stateSubject: subject,
state: subject.asBroadcastStream());
}
HomeBloc._({this.state, Subject<HomeState> stateSubject, this.repository})
: _stateSubject = stateSubject;
Future<void> loadEvents() async {
_stateSubject.add(new HomeState.loading());
try {
final list = await repository.getEventList(1);
_stateSubject.add(new HomeState(result: list, isLoading: false));
} catch (err) {
_stateSubject.addError(err);
}
}
}
Also, notice how loadEvent use addError with the exception. Instead of pushing a HomeState with a hasError: true.

Resources