The textfield cursor does not work correctly - flutter

The textfield cursor does not work correctly when I retrieve data from the database and display it in a textfield and then try to edit it.
I would like the position of the cursor to be respected each time I return the validate result of the change method.
Full code below.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'TextField Test',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController _nameController = TextEditingController();
TextEditingController _emailController = TextEditingController();
ApplicationBloc _bloc;
#override
void initState() {
super.initState();
_bloc = new ApplicationBloc();
_bloc.initData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TextField Test'),
),
body: ListView(
shrinkWrap: true,
children: <Widget>[_buildName(), _buildEmail(), _buildSubmit()],
),
);
}
Widget _buildName() {
return StreamBuilder(
stream: _bloc.name,
builder: (context, snapshot) {
if (snapshot.hasData) {
_nameController.text = snapshot.data;
}
return TextField(
controller: _nameController,
onChanged: _bloc.changeName,
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: "Name",
errorText: snapshot.error,
),
);
},
);
}
Widget _buildEmail() {
return StreamBuilder(
stream: _bloc.email,
builder: (context, snapshot) {
if (snapshot.hasData) {
_emailController.text = snapshot.data;
}
return TextField(
controller: _emailController,
onChanged: _bloc.changeEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "Email",
errorText: snapshot.error,
),
);
},
);
}
Widget _buildSubmit() {
return StreamBuilder(
stream: _bloc.submit,
builder: (context, snapshot) {
return RaisedButton(
onPressed: (!snapshot.hasData || !snapshot.data)
? null
: () {
_bloc.submitForm();
_nameController.text = '';
_emailController.text = '';
},
child: Text('Submit!'),
);
},
);
}
}
class ApplicationBloc {
BehaviorSubject<String> _nameController =
BehaviorSubject<String>(seedValue: '');
Observable<String> get name => _nameController.stream.transform(validateName);
Function(String) get changeName => _nameController.sink.add;
BehaviorSubject<String> _emailController =
BehaviorSubject<String>(seedValue: '');
Observable<String> get email =>
_emailController.stream.transform(validateEmail);
Function(String) get changeEmail => _emailController.sink.add;
Observable<bool> get submit => Observable.combineLatest2(
name, email, (e, e1) => e.isNotEmpty && e1.isNotEmpty);
initData() {
// This data from database
_nameController.sink.add('Test123');
_emailController.sink.add('test#email.com');
}
submitForm() {
//Send to api and wait
//Reset values
Future.delayed(const Duration(seconds: 1));
}
final validateName =
StreamTransformer<String, String>.fromHandlers(handleData: (name, sink) {
if (name.isEmpty || name.length > 4) {
sink.add(name);
} else if (name.isNotEmpty) {
sink.addError('Invalid Name!');
}
});
final validateEmail =
StreamTransformer<String, String>.fromHandlers(handleData: (email, sink) {
String p =
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(p);
if (email.isEmpty || (email.length > 4 && regExp.hasMatch(email))) {
sink.add(email);
} else {
sink.addError('Invalid email!');
}
});
//dispose/close all the streams when we call dispose() method
void dispose() {
_nameController.close();
_emailController.close();
}
}
I tried a code that changes the cursor position to final but its not work properly when i try edit the middle content of the textfield.

If you take a look at the text setter of TextEditingController, you'll see:
set text(String newText) {
value = value.copyWith(
text: newText,
selection: const TextSelection.collapsed(offset: -1),
composing: TextRange.empty,
);
}
Notice that selection is reset, so you cannot use this setter. Instead, inside your builder, do:
if (snapshot.hasData) {
_nameController.value = _nameController.value.copyWith(
text: snapshot.data,
);
}
Just the text is going to change and you don't have to worry about the other properties.

Related

How to automatically scroll through all the ListTiles in the Listview.seperated in Flutter?

Scroll automatically (without any user interaction) through all the ListTiles in the Listview using a Timer in flutter. The below method makes only one ListTile to animate but I want to animate all the ListTiles from top to bottom one by one and again from bottom to top one by one.
The below is the Listview:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: FutureBuilder(
future: fetchNews(),
builder: (context, snap) {
if (snap.hasData) {
news = snap.data;
return ListView.separated(
//controller: _controller,
scrollDirection: scrollDirection,
controller: controller,
itemBuilder: (context, i) {
final NewsModel _item = news[i];
return AutoScrollTag(
key: ValueKey(i),
controller: controller,
index: i,
child: ListTile(
title: Text('${_item.title}'),
subtitle: Text(
'${_item.description}',
// maxLines: 1,
//overflow: TextOverflow.ellipsis,
),
),
);
},
separatorBuilder: (context, i) => Divider(),
itemCount: news.length,
);
} else if (snap.hasError) {
return Center(
child: Text(snap.error.toString()),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
),
);
}
}
This is the automatic scrolling i have tried:
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 2), (Timer t) async {
await controller.scrollToIndex(1,
preferPosition: AutoScrollPosition.begin);
});
Here is a solution assuming that all your items in the ListView have the same itemExtent.
In this solution, I highlight the current Item as selected. You could also want to stop autoscrolling as soon as you reach the bottom of the list.
Full source code
import 'dart:async';
import 'package:faker/faker.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part '66455867.auto_scroll.freezed.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
Future<List<News>> _fetchNews() async => dummyData;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('News')),
body: FutureBuilder(
future: _fetchNews(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return NewsList(newsList: snapshot.data);
} else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
} else {
return Center(child: CircularProgressIndicator());
}
},
),
);
}
}
class NewsList extends StatefulWidget {
final List<News> newsList;
const NewsList({
Key key,
this.newsList,
}) : super(key: key);
#override
_NewsListState createState() => _NewsListState();
}
class _NewsListState extends State<NewsList> {
ScrollController _scrollController = ScrollController();
Timer _timer;
double _itemExtent = 100.0;
Duration _scrollDuration = Duration(milliseconds: 300);
Curve _scrollCurve = Curves.easeInOut;
int _autoScrollIncrement = 1;
int _currentScrollIndex = 0;
#override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 2), (_) async {
_autoScrollIncrement = _currentScrollIndex == 0
? 1
: _currentScrollIndex == widget.newsList.length - 1
? -1
: _autoScrollIncrement;
_currentScrollIndex += _autoScrollIncrement;
_animateToIndex(_currentScrollIndex);
setState(() {});
});
}
void _animateToIndex(int index) {
_scrollController.animateTo(
index * _itemExtent,
duration: _scrollDuration,
curve: _scrollCurve,
);
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ListView(
controller: _scrollController,
itemExtent: _itemExtent,
children: widget.newsList
.map((news) => ListTile(
title: Text(news.title),
subtitle: Text(
news.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
selected: widget.newsList[_currentScrollIndex].id == news.id,
selectedTileColor: Colors.amber.shade100,
))
.toList(),
);
}
}
#freezed
abstract class News with _$News {
const factory News({int id, String title, String description}) = _News;
}
final faker = Faker();
final dummyData = List.generate(
10,
(index) => News(
id: faker.randomGenerator.integer(99999999),
title: faker.sport.name(),
description: faker.lorem.sentence(),
),
);
Packages used in the solution:
freeze for the News Domain Class
build_runner to generate the freezed code
faker to generate the list of random news
UPDATE : Scroll only once
To stop the autoscrolling at the bottom of the listview, you just need to modify the initState method:
int _currentScrollIndex;
News _selectedNews;
#override
void initState() {
super.initState();
_currentScrollIndex = -1;
_timer = Timer.periodic(Duration(seconds: 2), (_) async {
setState(() {
if (_currentScrollIndex == widget.newsList.length - 1) {
_timer.cancel();
_selectedNews = null;
} else {
_selectedNews = widget.newsList[++_currentScrollIndex];
_animateToIndex(_currentScrollIndex);
}
});
});
}
We don't need the scroll direction defined as _autoScrollIncrement. However, I would introduce a new _selectedNews to easily unselect the last News item when we arrive at the bottom of the list. The selected flag of our ListTile would then become:
#override
Widget build(BuildContext context) {
return ListView(
[...]
children: widget.newsList
.map((news) => ListTile(
[...]
selected: _selectedNews?.id == news.id,
[...]
))
.toList(),
);
}

TextFormField enter key press getting setState() called during build -error

Hi I am trying to add some values from TextFormField to ListView when click on enter key .
But getting the error setState() or markNeedsBuild() called during build
My code is below .
class ScanBarcode extends StatefulWidget {
#override
_ScanBarcodeState createState() => _ScanBarcodeState();
}
class _ScanBarcodeState extends State<ScanBarcode> {
List<String> litems = [];
final TextEditingController eCtrl = new TextEditingController();
#override
Widget build(BuildContext ctxt) {
return new Scaffold(
appBar: new AppBar(title: new Text("Dynamic Demo"),),
body: new Column(
children: <Widget>[
new TextFormField(
controller: eCtrl,
maxLines: null,
autovalidate: true,
textInputAction: TextInputAction.none,
// ignore: missing_return
validator: (value) {
if (value.isEmpty==false) {
// if(eCtrl.text.trim()!='') {
if (value.contains('\n')) {
debugPrint(value);
litems.add(value); // Append Text to the list
eCtrl.clear(); // Clear the Text area
setState(() {});
}
}
// }
},
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term'
),
),
new Expanded(
child: new ListView.builder
(
itemCount: litems.length,
itemBuilder: (BuildContext ctxt, int Index) {
return new Text(litems[Index]);
}
)
)
],
)
);
}
}
I am getting below error.
setState() or markNeedsBuild() called during build
if commented
setState()
it is working fine but the values are not added to list view ..
Any help will be appreciated.
You can copy paste run full code below
You can use addPostFrameCallback
code snippet
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {});
});
working demo
full code
import 'package:flutter/material.dart';
class ScanBarcode extends StatefulWidget {
#override
_ScanBarcodeState createState() => _ScanBarcodeState();
}
class _ScanBarcodeState extends State<ScanBarcode> {
List<String> litems = [];
final TextEditingController eCtrl = TextEditingController();
#override
Widget build(BuildContext ctxt) {
return Scaffold(
appBar: AppBar(
title: Text("Dynamic Demo"),
),
body: Column(
children: <Widget>[
TextFormField(
controller: eCtrl,
maxLines: null,
autovalidate: true,
textInputAction: TextInputAction.none,
// ignore: missing_return
validator: (value) {
if (value.isEmpty == false) {
// if(eCtrl.text.trim()!='') {
if (value.contains('\n')) {
debugPrint(value);
litems.add(value); // Append Text to the list
eCtrl.clear(); // Clear the Text area
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {});
});
}
}
// }
},
decoration: InputDecoration(
border: InputBorder.none, hintText: 'Enter a search term'),
),
Expanded(
child: ListView.builder(
itemCount: litems.length,
itemBuilder: (BuildContext ctxt, int Index) {
return Text(litems[Index]);
}))
],
));
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ScanBarcode(),
);
}
}
Chunhunghan's solution is a valid solution and still works. But, in my case, it's the future build & the stream builder.
What I did is, I directly assigned the Future to a FutureBuilder, which gets called every time when the build() is called, which causes the Future method to re-call & reload my UI every time I touched/typed on a TextField/TextFormField.
The Solution:
Create a Future object & assign the Future method to that future object, then assign the future object to the FutureBuilder widget.
The Code:
late Future _dataFuture;
#override
void initState() {
super.initState();
_stream = _leadDatabase.listenLeads();
_dataFuture = _fetchData();
}
//inside build()
FutureBuilder(future: _dataFuture, builder:context, dataSnapshot) {
//logics
}
The same solution is applicable for StreamBuilder.

DateFormField call save from form

the code from this question:
Receive Response from pop navigator in Flutter
with added DateTime picker form field.
If we add DateFormField like this:
maind.dart
import 'package:flutter/material.dart';
import 'package:date_field/date_field.dart';
import 'answer.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final appTitle = 'Form Validation Demo';
return MaterialApp(
title: appTitle,
home: Scaffold(
appBar: AppBar(
title: Text(appTitle),
),
body: ShowData(),
),
);
}
}
// Create a Form widget.
class MyCustomForm extends StatefulWidget {
#override
MyCustomFormState createState() {
return MyCustomFormState();
}
}
// Create a corresponding State class.
// This class holds data related to the form.
class MyCustomFormState extends State<MyCustomForm> {
// Create a global key that uniquely identifies the Form widget
// and allows validation of the form.
//
// Note: This is a GlobalKey<FormState>,
// not a GlobalKey<MyCustomFormState>.
final _formKey = GlobalKey<FormState>();
final myController = TextEditingController();
Data stateData = Data();
#override
void dispose() {
// Clean up the controller when the widget is disposed.
myController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
// Build a Form widget using the _formKey created above.
return Scaffold(
body: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
DateFormField(
initialDatePickerMode: DatePickerMode.day,
enabled: true,
key:_formKey,
onSaved: (DateTime value)
{
stateData.datefield = value;
},
validator: (DateTime value){
return stateData.datefield != null ? null : 'enter date';
},
firstDate: DateTime.now().subtract(Duration(days: 180)),
lastDate: DateTime.now().add(Duration(days: 365)),
),
TextFormField(
controller: myController,
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
onSaved: (value){
stateData.load = value;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: RaisedButton(
onPressed: () {
// Validate returns true if the form is valid, or false
// otherwise.
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
// If the form is valid, display a Snackbar.
Navigator.pop(context,stateData);
// Scaffold.of(context)
// .showSnackBar(SnackBar(content: Text(myController.text)));
// myController.text = 'look at me';
}
},
child: Text('Submit'),
),
),
],
),
),
);
}
}
class Data {
String load;
DateTime datefield;
}
when pop happens there is exception that the validator was called on null.
and "The following assertion was thrown while finalizing the widget tree:
Multiple widgets used the same GlobalKey."
if the key field is not used, then the validator is not called upon.
why?
how do you use DateFormField? from package
date_field: "^0.1.2"
adding answer.dart
import 'package:flutter/material.dart';
import 'main.dart';
class ShowData extends StatefulWidget {
#override
_ShowDataState createState() => _ShowDataState();
}
class _ShowDataState extends State<ShowData> {
String data = 'start';
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () async {
final holder = await getFormData(context);
setState(() {
data = holder.load;
});
},
elevation: 4,
),
body:Text(data,style: TextStyle(fontSize: 80),));
}
Future<Data> getFormData(BuildContext context) async {
final answer = await Navigator.push(context,MaterialPageRoute(builder: (context)=>MyCustomForm()));
return (Future.value(answer));
}
}
You can copy paste run full code below
Step 1: remove key:_formKey
DateFormField(
initialDatePickerMode: DatePickerMode.day,
enabled: true,
//key:_formKey,
Step 2: validator use value != null not stateData.datefield != null
validator: (DateTime value) {
//return stateData.datefield != null ? null : 'enter date';
return value != null ? null : 'enter date';
},
working demo
full code
import 'package:flutter/material.dart';
import 'package:date_field/date_field.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final appTitle = 'Form Validation Demo';
return MaterialApp(
title: appTitle,
home: Scaffold(
appBar: AppBar(
title: Text(appTitle),
),
body: ShowData(),
),
);
}
}
// Create a Form widget.
class MyCustomForm extends StatefulWidget {
#override
MyCustomFormState createState() {
return MyCustomFormState();
}
}
// Create a corresponding State class.
// This class holds data related to the form.
class MyCustomFormState extends State<MyCustomForm> {
// Create a global key that uniquely identifies the Form widget
// and allows validation of the form.
//
// Note: This is a GlobalKey<FormState>,
// not a GlobalKey<MyCustomFormState>.
final _formKey = GlobalKey<FormState>();
final myController = TextEditingController();
Data stateData = Data();
#override
void dispose() {
// Clean up the controller when the widget is disposed.
myController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
// Build a Form widget using the _formKey created above.
return Scaffold(
body: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
DateFormField(
initialDatePickerMode: DatePickerMode.day,
enabled: true,
//key:_formKey,
onSaved: (DateTime value) {
stateData.datefield = value;
},
validator: (DateTime value) {
//return stateData.datefield != null ? null : 'enter date';
return value != null ? null : 'enter date';
},
firstDate: DateTime.now().subtract(Duration(days: 180)),
lastDate: DateTime.now().add(Duration(days: 365)),
),
TextFormField(
controller: myController,
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
onSaved: (value) {
stateData.load = value;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: RaisedButton(
onPressed: () {
// Validate returns true if the form is valid, or false
// otherwise.
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
// If the form is valid, display a Snackbar.
Navigator.pop(context, stateData);
// Scaffold.of(context)
// .showSnackBar(SnackBar(content: Text(myController.text)));
// myController.text = 'look at me';
}
},
child: Text('Submit'),
),
),
],
),
),
);
}
}
class Data {
String load;
DateTime datefield;
}
class ShowData extends StatefulWidget {
#override
_ShowDataState createState() => _ShowDataState();
}
class _ShowDataState extends State<ShowData> {
String data = 'start';
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () async {
final holder = await getFormData(context);
print(holder.datefield);
setState(() {
data = holder.load;
});
},
elevation: 4,
),
body: Text(
data,
style: TextStyle(fontSize: 80),
));
}
Future<Data> getFormData(BuildContext context) async {
final answer = await Navigator.push(
context, MaterialPageRoute(builder: (context) => MyCustomForm()));
return (Future.value(answer));
}
}
This is now fixed with the version 0.2.0 of the package!

Flutter - Stream error not persisting (using bloc pattern)

So I am trying to make a signup form in flutter using streams and the bloc pattern.
The problem is that the error added to the sink for one my streams does not seem to persist?
The submitValid getter does not seem to consider the error added to the passwordretype stream, this causes the login button to be clickable when it shouldn't
Steps to reproduce:
Fill out a valid email
Fill out a valid password (password field)
Fill out a valid password (password retype field)
Delete everything from the retype field
Delete everything from the password field.
Type a valid password in the password field and the login button appears clickable even though the retype field is empty.
Here's my code along with a screenshot.
Bloc.dart
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'validators.dart';
import 'dart:io';
import 'package:rxdart/rxdart.dart';
class Bloc extends Object with Validators {
final _email = BehaviorSubject<String>();
final _password = BehaviorSubject<String>();
final _passwordretype = BehaviorSubject<String>();
final _isSignedIn = BehaviorSubject<bool>();
//Add data to stream
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password => _password.stream.transform(validatePassword);
Stream<String> get passwordretype=> _passwordretype.stream.transform(validatePasswordRetype)
.doOnData((String c){
if(0 != _password.value.compareTo(c)){
_passwordretype.addError("Passwords do not match");
}
});
Stream<bool> get signInStatus => _isSignedIn.stream;
Stream<bool> get submitValid =>
Rx.combineLatest3(email, password, passwordretype, (e, p, r) => true);
//Change data
Function(String) get changeEmail => _email.sink.add;
Function(String) get changePassword => _password.sink.add;
Function(String) get changePasswordRetype => _passwordretype.sink.add;
Function(bool) get showProgressBar => _isSignedIn.add;
register() async {
final FirebaseAuth _auth = FirebaseAuth.instance;
try {
showProgressBar(true);
final FirebaseUser user = (await _auth.createUserWithEmailAndPassword(
email: _email.value,
password: _password.value,
))
.user;
if (user != null) {
// setState(() {
// _success = true;
// _userEmail = user.email;
// Navigator.of(context).pushNamedAndRemoveUntil(
// '/home', (Route<dynamic> route) => false);
//
//// Navigator.of(context).pushReplacementNamed('/home');
//
// });
} else {
// Scaffold.of(context).showSnackBar(SnackBar(
// content: Text("Error occured, please try again later"),
// ));
// _success = false;
}
} catch (err) {
_isSignedIn.addError(err);
print(err);
// setState(() {
// _showLoading = false;
// _error = true;
// });
}
}
dispose() {
_email.drain();
_email.close();
_password.drain();
_password.close();
_passwordretype.drain();
_passwordretype.close();
}
}
SignupScreen.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:lendy/src/blocs/bloc.dart';
import 'package:lendy/src/blocs/provider.dart';
class SignupScreen extends StatelessWidget {
// final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// final TextEditingController _emailController = TextEditingController();
// final TextEditingController _passwordController = TextEditingController();
// final TextEditingController _passwordController2 = TextEditingController();
final FirebaseAuth _auth = FirebaseAuth.instance;
// final GoogleSignIn _googleSignIn = GoogleSignIn();
String _userID = "";
bool _success;
String _userEmail;
#override
Widget build(BuildContext context) {
final bloc = Provider.of(context);
return Scaffold(
appBar: AppBar(
title: Text("Signup"),
),
body: Container(
child: Column(
children: <Widget>[
emailField(bloc),
passwordField(bloc),
passwordFieldRe(bloc),
SizedBox(
height: 10.0,
),
button(bloc)
],
),
),
);
}
Widget emailField(Bloc bloc) {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changeEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Enter email address',
labelText: 'Email-address',
errorText: snapshot.error),
);
},
);
}
Widget passwordField(Bloc bloc) {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changePassword,
decoration: InputDecoration(
hintText: 'Enter password',
labelText: 'Password',
errorText: snapshot.error),
);
});
}
Widget passwordFieldRe(Bloc bloc) {
return StreamBuilder(
stream: bloc.passwordretype,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changePasswordRetype,
decoration: InputDecoration(
hintText: 'Retype password',
labelText: 'Password',
errorText: snapshot.error),
);
});
}
Widget button(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
child: Text('Register'),
color: Colors.blue,
//if true
onPressed: snapshot.hasData
? () {
// bloc.showProgressBar(true);
bloc.register();
}
: null);
},
);
}
Widget buttons(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot1) {
return StreamBuilder(
stream: bloc.signInStatus,
builder: (context, snapshot2) {
if (!snapshot2.hasData || snapshot2.hasError) {
return Column(
children: <Widget>[
RaisedButton(
child: Text('Register'),
color: Colors.blue,
onPressed: snapshot1.hasData
? () {
bloc.register();
}
: null,
),
snapshot2.hasError ? Text("ee") : Container()
],
);
} else {
return CircularProgressIndicator();
}
},
);
},
);
}
Widget submitButton(Bloc bloc) {
return StreamBuilder(
stream: bloc.signInStatus,
builder: (context, snapshot) {
if (snapshot.hasError || !snapshot.hasData) {
return buttons(bloc);
} else {
return CircularProgressIndicator();
}
});
}
/
Validators.dart
import 'dart:async';
class Validators {
final validateEmail = StreamTransformer<String, String>.fromHandlers(
handleData: (email, sink){
if (RegExp(
r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+#[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(email)){
sink.add(email);
} else {
sink.addError('Please enter a valid email address.');
}
}
);
final validatePassword = StreamTransformer<String, String>.fromHandlers(
handleData: (password, sink){
if (password.length > 3){
sink.add(password);
} else {
sink.addError('Password is too short.');
}
}
);
final validatePasswordRetype = StreamTransformer<String, String>.fromHandlers(
handleData: (password, sink){
print("HANDLE DATA");
if (password.length > 3){
sink.add(password);
} else {
sink.addError('Password is too short.');
}
}
);
}
screenshot
For the submitValid Stream you are using the BehaviorSubjects instead of use the stream.transforms, so for that reason, your submitValid doesn't receive the errors. Replace your submitValid with the next code:
Stream<bool> get submitValid => Rx.combineLatest3(email, password, passwordretype, (e, p, r) => true);
Found a similar issue here
I changed my submitValid function like so:
Stream<bool> get submitValid =>
Rx.combineLatest3(email, password, passwordretype, (e, p, r) {
if (p == _password.value && r == _passwordretype.value){
return true;
} else {
return false;
}
});
And changed onPressed in my button Widget like so. It only renders the clickable button if the snapshot returns true.
Widget button(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
child: Text('Register'),
color: Colors.blue,
onPressed: snapshot.hasData && snapshot.data
? () {
bloc.register();
}
: null);
},
);
}

Flutter Auth (BLoC pattern & rxDart)

I wanted to make applications with authorization on the BLoC partner, but I encountered an error:
The following NoSuchMethodError was thrown building AuhtScreen(dirty, state: _AuhtScreenState<dynamic>#00539):
The getter 'blocState' was called on null.
Receiver: null
Tried calling: blocState
It is called under the following circumstances:
In AuhtScreen
AuthBloc authBloc = BlocProvider.of(context).authBloc; (context = StatefulElement)
In BlocProvider
static BlocState of(BuildContext context) { (context = StatefulElement)
return (context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).blocState; (context = StatefulElement)(NULL)
}
I do not understand why it does not work, I do everything correctly, maybe I missed something or did not understand... Help solve the problem!
All code:
AuthBloc
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mifity/models/auht_detail.dart';
import 'package:mifity/models/user.dart';
import 'package:mifity/screens/main_screen.dart';
import 'package:mifity/services/auth_service.dart';
import 'package:rxdart/rxdart.dart';
class AuthBloc {
AuthService authService;
BuildContext _context;
final currentUserSubject = BehaviorSubject<User>.seeded(null);
final emailSubject = BehaviorSubject<String>.seeded('');
final passwordSubject = BehaviorSubject<String>.seeded('');
final loadingSubject = BehaviorSubject<bool>.seeded(false);
final loginSubject = BehaviorSubject<Null>.seeded(null);
//sink
void Function(String) get emailChanged => emailSubject.sink.add;
void Function(String) get passwordChanged => passwordSubject.sink.add;
void Function(BuildContext) get submitLogin => (context) {
this.setContext(context);
loginSubject.add(null);
};
//stream
Stream<User> get currentUser => currentUserSubject.stream;
Stream<String> get emailStream => emailSubject.stream;
Stream<String> get passwordStream => passwordSubject.stream;
Stream<bool> get loading => loadingSubject.stream;
AuthBloc({this.authService}) {
Stream<AuhtDetail> auhtDetailStream = Observable.combineLatest2(
emailStream, passwordStream, (email, password) {
return AuhtDetail(email: email, password: password);
});
Stream<User> loggedIn = Observable(loginSubject.stream)
.withLatestFrom(auhtDetailStream, (_, auhtDetail) {
return auhtDetail;
}).flatMap((auhtDetail) {
return Observable.fromFuture(authService.loginUser(auhtDetail))
.doOnListen(() {
loadingSubject.add(true);
}).doOnDone(() {
loadingSubject.add(false);
});
});
loggedIn.listen((User user) {
currentUserSubject.add(user);
Navigator.push(
_context,
new MaterialPageRoute(builder: (context) => MainScreen()),
);
}, onError: (error) {
Scaffold.of(_context).showSnackBar(new SnackBar(
content: new Text("Username or password incorrect"),
));
});
}
setContext(BuildContext context) {
_context = context;
}
close() {
emailSubject.close();
passwordSubject.close();
loadingSubject.close();
loginSubject.close();
}
}
BlocProvider
import 'package:flutter/material.dart';
import 'package:mifity/blocs/auth_bloc.dart';
import 'package:mifity/services/auth_service.dart';
class BlocProvider extends InheritedWidget {
final blocState = new BlocState(
authBloc: AuthBloc(authService: AuthService()),
);
BlocProvider({Key key, Widget child}) : super(key: key, child: child);
bool updateShouldNotify(_) => true;
static BlocState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider)
.blocState;
}
}
class BlocState {
final AuthBloc authBloc;
BlocState({this.authBloc});
}
AuthService
import 'dart:async';
import 'package:mifity/models/auht_detail.dart';
import 'package:mifity/models/error.dart';
import 'package:mifity/models/user.dart';
class AuthService {
Future<User> loginUser(AuhtDetail detail) async {
await Future.delayed(Duration(seconds: 1)); //simulate network delay
if (detail.email == 'johndoe#acme.com' && detail.password == '1234') {
return User(
id: 1,
name: 'John Doe',
email: 'johndoe#acme.com',
age: 26,
profilePic: 'john_doe.png');
} else {
throw ClientError(message: 'login details incorrect.');
}
}
}
Validator:
class Validator {
String validateEmail(String value) {
if (value.isEmpty) return 'Email Should not be empty';
final RegExp emailRegEx = new RegExp(r'^\w+#[a-zA-Z_]+?\.[a-zA-Z]{2,3}$');
if (!emailRegEx.hasMatch(value)) return 'Your Email is invalid';
return null;
}
String validatePassword(String value) {
if (value.length < 4) return 'Password should be four characters or more';
return null;
}
}
AuhtScreen
import 'package:flutter/material.dart';
import 'package:mifity/blocs/auth_bloc.dart';
import 'package:mifity/blocs/bloc_provider.dart';
import 'package:mifity/helpers/validators.dart';
class AuhtScreen extends StatefulWidget {
#override
_AuhtScreenState createState() => _AuhtScreenState();
}
class _AuhtScreenState<StateClass> extends State<AuhtScreen> {
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
Validator validator = new Validator();
final formKey = GlobalKey<FormState>();
DecorationImage backgroundImage = new DecorationImage(
image: new ExactAssetImage('assets/images/bg_image.jpg'),
fit: BoxFit.cover,
);
#override
Widget build(BuildContext context) {
AuthBloc authBloc = BlocProvider.of(context).authBloc;
final Size screenSize = MediaQuery.of(context).size;
return Scaffold(
appBar: AppBar(
title: Text('Login'),
),
body: Builder(builder: (context) {
return SingleChildScrollView(
child: Container(
height: screenSize.height - AppBar().preferredSize.height,
padding: EdgeInsets.all(10.0),
alignment: Alignment.center,
decoration: BoxDecoration(
image: (backgroundImage != null) ? backgroundImage : null),
child: Center(
child: Form(
key: formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
style: TextStyle(color: Colors.white),
controller: emailController,
decoration: InputDecoration(
labelText: 'email',
labelStyle: TextStyle(color: Colors.grey)),
validator: validator.validateEmail,
),
TextFormField(
style: TextStyle(color: Colors.white),
controller: passwordController,
decoration: InputDecoration(
labelText: 'password',
labelStyle: TextStyle(color: Colors.grey)),
obscureText: true,
validator: validator.validatePassword,
),
SizedBox(
height: 20.0,
),
StreamBuilder<bool>(
initialData: false,
stream: authBloc.loading,
builder: (context, loadingSnapshot) {
return SizedBox(
width: double.infinity,
child: RaisedButton(
color: Colors.deepOrange,
textColor: Colors.white,
child: Text((loadingSnapshot.data)
? 'Login ...'
: 'Login'),
onPressed: () {
_submit(context, authBloc);
},
),
);
},
),
],
),
),
),
),
);
}));
}
_submit(context, AuthBloc authBloc) {
authBloc.emailChanged(emailController.text);
authBloc.passwordChanged(passwordController.text);
if (formKey.currentState.validate()) {
authBloc.submitLogin(context);
}
}
}
I'm an idiot!)
MyApp:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
child: MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new AuhtScreen(),
));
}
}