I am new to Flutter/Dart, Go and mobile development in general. I am currently coding a login authentication page in flutter that connects to a backend written in go. To check if everything worked I wanted to print out the cookie that my backend sends back in the console. Unfortunately I am getting errors not sure what to do.
EDIT: I have been able to read the cookie, should I save the cookie in this format "session=UUID" or just the "UUID"? I want to send this cookie back in the header for future get requests. My Go code will check for the cookie name "session",but I'm not sure if sending it back in that format is correct.
Flutter code (I read on another stackoverflow post to use 10.0.2.2 as localhost when using an android emulator):
EDIT: After playing around with the code I was able to read the cookie in the header but it has the name as well not just the UUID.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class Login extends StatefulWidget {
#override
State<StatefulWidget> createState(){
return new LoginState();
}
}
class LoginState extends State <Login> {
final formKey = GlobalKey<FormState>();
String _email, _password;
String cookie;
void loginApi(){
var form = formKey.currentState;
var url = "http://10.0.2.2:8080";
if (form.validate()) {
form.save();
var body = { "Email":_email, "Pass":_password};
http.post(url,body:json.encode(body)).then((response){
print(response.headers['set-cookie']);
});
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Login"),
backgroundColor: Colors.white,
),
body: new Container(
alignment: Alignment.center,
child: Form(
key: formKey,
child: Column(
children: <Widget>[
TextFormField(
autocorrect: false,
decoration: InputDecoration(
labelText: "Email:",
),
validator: (str) =>
!str.contains('#') ? "Not a Valid Email!" : null,
onSaved: (str) => _email = str,
),
TextFormField(
autocorrect: false,
decoration: InputDecoration(
labelText: "Password:",
),
validator: (str) =>
str.length <= 7 ? "Not a Valid Password!" : null,
onSaved: (str) => _password = str,
obscureText: false,
),
RaisedButton(
child: Text("Submit"),
onPressed:loginApi,
),
],
)
),
),
);
}
}
Golang code (EDIT: Inserted a missing curly bracket after the panic error and deleted the curly bracket at the end of the code):
package main
import (
"encoding/json"
"html/template"
"net/http"
uuid "github.com/satori/go.uuid"
"golang.org/x/crypto/bcrypt"
)
type user struct {
UserName string //Same as email
Password []byte
}
type logindata struct {
Email string
Pass string
}
var tpl *template.Template
var dbUsers = map[string]user{}
var dbSess = map[string]string{}
func init() {
bs, _ := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.MinCost)
dbUsers["test#test.com"] = user{"test#test.com", bs}
}
func main() {
http.HandleFunc("/", login)
http.Handle("/favicon.ico", http.NotFoundHandler()) //Don't have favicon
http.ListenAndServe(":8080", nil)
}
func login(w http.ResponseWriter, req *http.Request) {
var data logindata
decoder := json.NewDecoder(req.Body)
err_decode := decoder.Decode(&data)
if err_decode != nil {
panic(err_decode)
}
u, ok := dbUsers[data.Email]
if !ok {
http.Error(w, "Username and/or password do not match", http.StatusForbidden)
return
}
err_compare := bcrypt.CompareHashAndPassword(u.Password, []byte(data.Pass))
if err_compare != nil {
http.Error(w, "Username and/or password do not match", http.StatusForbidden)
return
}
// create session
sID, _ := uuid.NewV4()
c := &http.Cookie{
Name: "session",
Value: sID.String(),
}
http.SetCookie(w, c)
dbSess[c.Value] = data.Email
return
}
Output from console for flutter code (after I put in username and password and click submit, username is "test#test.com" password is "password"):
Performing hot reload...
Reloaded 0 of 489 libraries in 544ms.
I/flutter ( 3778): session=db3690d6-db6e-4658-8b5b-5f2d3f908a65
I also went to localhost:8080 on my browser expecting to show a blank page but it displayed "page not working" and my terminal outputted the following error I guess because I was accessing it with a browser and I wasn't sending in JSON data, not sure:
2018/05/24 23:29:38 http: panic serving [::1]:40010: EOF
goroutine 6 [running]:
net/http.(*conn).serve.func1(0xc82001a280)
/usr/lib/go-1.6/src/net/http/server.go:1389 +0xc1
panic(0x76c6c0, 0xc82000a160)
/usr/lib/go-1.6/src/runtime/panic.go:443 +0x4e9
main.login(0x7f96c9a848b8, 0xc82012c000, 0xc8201121c0)
/home/daniel/Desktop/Workspace/Genesis/main.go:43 +0x176
net/http.HandlerFunc.ServeHTTP(0x8a8688, 0x7f96c9a848b8, 0xc82012c000, 0xc8201121c0)
/usr/lib/go-1.6/src/net/http/server.go:1618 +0x3a
net/http.(*ServeMux).ServeHTTP(0xc820013020, 0x7f96c9a848b8, 0xc82012c000, 0xc8201121c0)
/usr/lib/go-1.6/src/net/http/server.go:1910 +0x17d
net/http.serverHandler.ServeHTTP(0xc82001a100, 0x7f96c9a848b8, 0xc82012c000, 0xc8201121c0)
/usr/lib/go-1.6/src/net/http/server.go:2081 +0x19e
net/http.(*conn).serve(0xc82001a280)
/usr/lib/go-1.6/src/net/http/server.go:1472 +0xf2e
created by net/http.(*Server).Serve
/usr/lib/go-1.6/src/net/http/server.go:2137 +0x44e
2018/05/24 23:29:38 http: panic serving [::1]:40012: EOF
goroutine 18 [running]:
net/http.(*conn).serve.func1(0xc82011c080)
/usr/lib/go-1.6/src/net/http/server.go:1389 +0xc1
panic(0x76c6c0, 0xc82000a160)
/usr/lib/go-1.6/src/runtime/panic.go:443 +0x4e9
main.login(0x7f96c9a848b8, 0xc8201161a0, 0xc820154000)
/home/daniel/Desktop/Workspace/Genesis/main.go:43 +0x176
net/http.HandlerFunc.ServeHTTP(0x8a8688, 0x7f96c9a848b8, 0xc8201161a0, 0xc820154000)
/usr/lib/go-1.6/src/net/http/server.go:1618 +0x3a
net/http.(*ServeMux).ServeHTTP(0xc820013020, 0x7f96c9a848b8, 0xc8201161a0, 0xc820154000)
/usr/lib/go-1.6/src/net/http/server.go:1910 +0x17d
net/http.serverHandler.ServeHTTP(0xc82001a100, 0x7f96c9a848b8, 0xc8201161a0, 0xc820154000)
/usr/lib/go-1.6/src/net/http/server.go:2081 +0x19e
net/http.(*conn).serve(0xc82011c080)
/usr/lib/go-1.6/src/net/http/server.go:1472 +0xf2e
created by net/http.(*Server).Serve
/usr/lib/go-1.6/src/net/http/server.go:2137 +0x44e
2018/05/24 23:29:43 http: panic serving [::1]:40016: EOF
goroutine 7 [running]:
net/http.(*conn).serve.func1(0xc82001a380)
/usr/lib/go-1.6/src/net/http/server.go:1389 +0xc1
panic(0x76c6c0, 0xc82000a160)
/usr/lib/go-1.6/src/runtime/panic.go:443 +0x4e9
main.login(0x7f96c9a848b8, 0xc82012c1a0, 0xc8201122a0)
/home/daniel/Desktop/Workspace/Genesis/main.go:43 +0x176
net/http.HandlerFunc.ServeHTTP(0x8a8688, 0x7f96c9a848b8, 0xc82012c1a0, 0xc8201122a0)
/usr/lib/go-1.6/src/net/http/server.go:1618 +0x3a
net/http.(*ServeMux).ServeHTTP(0xc820013020, 0x7f96c9a848b8, 0xc82012c1a0, 0xc8201122a0)
/usr/lib/go-1.6/src/net/http/server.go:1910 +0x17d
net/http.serverHandler.ServeHTTP(0xc82001a100, 0x7f96c9a848b8, 0xc82012c1a0, 0xc8201122a0)
/usr/lib/go-1.6/src/net/http/server.go:2081 +0x19e
net/http.(*conn).serve(0xc82001a380)
/usr/lib/go-1.6/src/net/http/server.go:1472 +0xf2e
created by net/http.(*Server).Serve
/usr/lib/go-1.6/src/net/http/server.go:2137 +0x44e
In your code
http.post(url,body:json.encode(body)).then((http.Response response){
cookie = response.headers['session'];
});
debugPrint(cookie);
the line
debugPrint(cookie);
is executed before
cookie = response.headers['session'];
so it is expected that cookie is null when you print it. For some weird reason debugPrint(null) throws an exception about "slit on null"
Try instead
http.post(url,body:json.encode(body)).then((http.Response response){
cookie = response.headers['session'];
debugPrint(cookie);
});
Related
I'm building a Flutter app with Android Studio (a Time Tracker, following a course on Udemy) and I am at the stage where I have created a sign-in page, that allows me to sign in using either Google, Facebook, email or 'going anonymous'. I'm using version 2.0.1 of the flutter_facebook_login plugin, since the latest version, version 3.0.0, generates lots of errors related to Cocoapods. Version 2.0.1 resolves all of those errors.
I'm doing all authentication using Flutter's firebase_auth package, so that a unique user ID can be generated, to control what is seen by each user. The sign-in process is split into two different pages. There's an 'auth.dart' page that handles all of the authorisation work, with Firebase, Google and Facebook etc. It looks like this:
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_facebook_login/flutter_facebook_login.dart';
import 'package:google_sign_in/google_sign_in.dart';
class User {
User({#required this.uid});
final String uid;
}
abstract class AuthBase {
Stream<User> get onAuthStateChanged;
Future<User> currentUser();
Future<User> signInAnonymously();
Future<User> signInWithGoogle();
Future<User> signInWithFacebook();
Future<void> signOut();
}
class Auth implements AuthBase {
final _firebaseAuth = FirebaseAuth.instance;
User _userFromFirebase(FirebaseUser user) {
if (user == null) {
return null;
}
return User(uid: user.uid);
}
#override
Stream<User> get onAuthStateChanged {
return _firebaseAuth.onAuthStateChanged.map(_userFromFirebase);
}
#override
Future<User> currentUser() async {
final user = await _firebaseAuth.currentUser();
return _userFromFirebase(user);
}
#override
Future<User> signInAnonymously() async {
final authResult = await _firebaseAuth.signInAnonymously();
return _userFromFirebase(authResult.user);
}
#override
Future<User> signInWithGoogle() async {
final googleSignIn = GoogleSignIn();
final googleAccount = await googleSignIn.signIn();
if (googleAccount != null) {
final googleAuth = await googleAccount.authentication;
if (googleAuth.accessToken != null && googleAuth.idToken != null) {
final authResult = await _firebaseAuth.signInWithCredential(
GoogleAuthProvider.getCredential(
idToken: googleAuth.idToken,
accessToken: googleAuth.accessToken,
),
);
return _userFromFirebase(authResult.user);
} else {
throw PlatformException(
code: 'ERROR_MISSING_GOOGLE_AUTH_TOKEN',
message: 'Missing Google Auth Token',
);
}
} else {
throw PlatformException(
code: 'ERROR_ABORTED_BY_USER',
message: 'Sign in aborted by user',
);
}
}
#override
Future<User> signInWithFacebook() async {
final facebookLogin = FacebookLogin();
final result = await facebookLogin.logInWithReadPermissions(
['public_profile'],
);
if (result.accessToken != null) {
final authResult = await _firebaseAuth
.signInWithCredential(FacebookAuthProvider.getCredential(
accessToken: result.accessToken.token,
));
return _userFromFirebase(authResult.user);
} else {
throw PlatformException(
code: 'ERROR_ABORTED_BY_USER',
message: 'Sign in aborted by user',
);
}
}
#override
Future<void> signOut() async {
final googleSignIn = GoogleSignIn();
await googleSignIn.signOut();
final facebookLogin = FacebookLogin();
await facebookLogin.logOut();
await _firebaseAuth.signOut();
}
}
Then, the sign-in page, with all of the buttons and interactions with Google and Facebook etc. looks like this:
import 'package:flutter/material.dart';
import 'package:time_tracker_flutter_course/app/sign_in/sign_in_button.dart';
import 'package:time_tracker_flutter_course/app/sign_in/social_sign_in_button.dart';
import 'package:time_tracker_flutter_course/services/auth.dart';
class SignInPage extends StatelessWidget {
SignInPage({#required this.auth});
final AuthBase auth;
Future<void> _signInAnonymously() async {
try {
await auth.signInAnonymously();
} catch (e) {
print(e.toString());
}
}
Future<void> _signInWithGoogle() async {
try {
await auth.signInWithGoogle();
} catch (e) {
print(e.toString());
}
}
Future<void> _signInWithFacebook() async {
try {
await auth.signInWithFacebook();
} catch (e) {
print(e.toString());
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Time Tracker'),
elevation: 2.0,
),
body: _buildContent(),
backgroundColor: Colors.grey[200],
);
}
Widget _buildContent() {
return Padding(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'Sign In',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 32.0,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 48.0),
SocialSignInButton(
assetName: 'images/google-logo.png',
text: 'Sign in with Google',
textColor: Colors.black87,
color: Colors.white,
onPressed: _signInWithGoogle,
),
SizedBox(height: 8.0),
SocialSignInButton(
assetName: 'images/facebook-logo.png',
text: 'Sign in with Facebook',
textColor: Colors.white,
color: Color(0xFF334D92),
onPressed: _signInWithFacebook,
),
SizedBox(height: 8.0),
SignInButton(
text: 'Sign in with email',
textColor: Colors.white,
color: Colors.teal[700],
onPressed: () {},
),
SizedBox(height: 8.0),
Text(
'or',
style: TextStyle(fontSize: 14.0, color: Colors.black87),
textAlign: TextAlign.center,
),
SizedBox(height: 8.0),
SignInButton(
text: 'Go anonymous',
textColor: Colors.black,
color: Colors.lime[300],
onPressed: _signInAnonymously,
),
],
),
);
}
}
All this code and methodology works perfectly in most cases, which includes:
Android simulator with anonymous login, Google AND Facebook
iOS simulator with anonymous login and Google ONLY
When I try and log in with the Facebook method on the iOS simulator in Android Studio, that's where I run into problems. In the Android Studio console, an error is 'spat out':
flutter: PlatformException(ERROR_ABORTED_BY_USER, Sign in aborted by user, null)
You'll see from the first block of code (the 'auth.dart' code) that this error is just a generic one that I have built in - I haven't been specific with it at all.
I don't believe the issue is with the flutter_facebook_login plugin, since it still works for Android, unless the plug-in has problems that are unique to iOS. I think there's an issue with the iOS set-up for Facebook, even though I have followed the instructions to the letter, including with Xcode.
Can someone help me to understand what might be causing this error, and how I can sort it? It is the only thing in the set-up that you can see that isn't working at the moment, across both simulator platforms.
I had the same issue, I think it is an issue of facebook api with ios beta version.
I found a work around. This is only a work around not the actual solution. It works for me and I hope this helps you:-
The work around checking when the status goes to FacebookLoginStatus.cancelledByUser, then using the below
facebookLogin.loginBehavior = FacebookLoginBehavior.webViewOnly;
It will force flutter to open facebook auth in webview and then you can get it working.
Have a look at the full method
Future signInWithFaceBook() async{
var facebookLogin = new FacebookLogin();
var result = await facebookLogin.logInWithReadPermissions(['email', 'public_profile']);
switch (result.status) {
case FacebookLoginStatus.loggedIn:
print(result.accessToken.token);
// Add your route to home page here after sign In
break;
case FacebookLoginStatus.cancelledByUser:
// In your case the program flow will go here as it as a bug with the api I suppose
facebookLogin.loginBehavior = FacebookLoginBehavior.webViewOnly;
// Once the code comes here the above line will force flutter to open facebook auth in a webview
result = await facebookLogin.logInWithReadPermissions(['email', 'public_profile']);
if(result.status==FacebookLoginStatus.loggedIn){
FirebaseUser user = (await _auth.signInWithCredential(FacebookAuthProvider.getCredential(accessToken: result.accessToken.token)
)
).user;
final FirebaseUser currentUser = await _auth.currentUser();
assert(user.uid == currentUser.uid);
// Add your home page here
}
print('CANCELED BY USER');
break;
case FacebookLoginStatus.error:
print(result.errorMessage);
break;
}
}
Update your auth.dart Code with one line of code from the below .It works.
#override
Future <User> signInWithFacebook() async {
final facebookLogin = FacebookLogin();
facebookLogin.LoginBehavior = FacebookLoginBehavior.webViewOnly;
final result = await facebookLogin.logInWithReadPermissions(['public_profile'],);
if (result.accessToken != null) {
final authResult = await _firebaseAuth
.signInWithCredential(FacebookAuthProvider.getCredential(
accessToken: result.accessToken.token,
)
);
return _userFromFirebase(authResult.user);
} else {
throw PlatformException(
code: 'ERROR_ABORTED_BY_USER',
message: 'Sign in aborted by user',
);
}}
I created a login page and I need to add these things to my password. How do I do it with validation alert message?
Minimum 1 Upper case
Minimum 1 lowercase
Minimum 1 Numeric Number
Minimum 1 Special Character
Common Allow Character ( ! # # $ & * ~ )
Your regular expression should look like:
r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!##\$&*~]).{8,}$
Here is an explanation:
r'^
(?=.*[A-Z]) // should contain at least one upper case
(?=.*[a-z]) // should contain at least one lower case
(?=.*?[0-9]) // should contain at least one digit
(?=.*?[!##\$&*~]) // should contain at least one Special character
.{8,} // Must be at least 8 characters in length
$
Match above expression with your password string. Using this method-
String? validatePassword(String value) {
RegExp regex =
RegExp(r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!##\$&*~]).{8,}$');
if (value.isEmpty) {
return 'Please enter password';
} else {
if (!regex.hasMatch(value)) {
return 'Enter valid password';
} else {
return null;
}
}
}
You need to use Regular Expression to validate the structure.
bool validateStructure(String value){
String pattern = r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!##\$&*~]).{8,}$';
RegExp regExp = new RegExp(pattern);
return regExp.hasMatch(value);
}
output:
Vignesh123! : true
vignesh123 : false
VIGNESH123! : false
vignesh# : false
12345678? : false
This function will validate the passed value is having the structure or not.
var _usernameController = TextEditingController();
String _usernameError;
...
#override
Widget build(BuildContext context) {
return
...
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
hintText: "Username", errorText: _usernameError),
style: TextStyle(fontSize: 18.0),
),
Container(
width: double.infinity,
height: 50.0,
child: RaisedButton(
onPressed: validate,
child: Text(
"Login",
style: TextStyle(color: Colors.white),
),
color: Theme.of(context).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0),
),
),
),
...
}
...
validate(){
if(!validateStructure(_usernameController.text)){
setState(() {
_usernameError = emailError;
_passwordError = passwordError;
});
// show dialog/snackbar to get user attention.
return;
}
// Continue
}
You have to use TextFormField widget with validator property.
TextFormField(
validator: (value) {
// add your custom validation here.
if (value.isEmpty) {
return 'Please enter some text';
}
if (value.length < 3) {
return 'Must be more than 2 charater';
}
},
),
Take a look on official docs: https://flutter.dev/docs/cookbook/forms/validation
You can achieve this using below flutter plugin.
wc_form_validators
You can use it something like this:
TextFormField(
decoration: InputDecoration(
labelText: 'Password',
),
validator: Validators.compose([
Validators.required('Password is required'),
Validators.patternString(r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!##\$&*~]).{8,}$', 'Invalid Password')
]),
),
Its documentation is really good. You can read it for more util functions like this.
By using extension in dart
extension PasswordValidator on String {
bool isValidPassword() {
return RegExp(
r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!##\$&*~]).{8,}$')
.hasMatch(this);
}
}
You can apply this in your textfield like
TextFormField(
autovalidate: true,
validator: (input) => input. isValidPassword() ? null : "Check your password...",
)
here is the complete answer
Write a Dart program to check whether a string is a valid password. a. A password must have at least ten characters. b. A password
consists of only letters and digits. c. A password must contain at
least two digits.
import 'dart:io';
main() {
var password;
stdout.write("Enter You'r Password: ");
password=stdin.readLineSync();
if(password.length>=10 && !password.contains(RegExp(r'\W')) && RegExp(r'\d+\w*\d+').hasMatch(password))
{
print(" \n\t$password is Valid Password");
}
else
{
print("\n\t$password is Invalid Password");
}
Flutter Login Validation
///creating Username and Password Controller.
TextEditingController username=TextEditingController();
TextEditingController password=TextEditingController();
Form(
child: Builder(
builder: (context) {
return Column(
children: [
TextFormField(
controller: username,
validator: (CurrentValue){
var nonNullValue=CurrentValue??'';
if(nonNullValue.isEmpty){
return ("username is required");
}
if(!nonNullValue.contains("#")){
return ("username should contains #");
}
return null;
},
),
TextFormField(
controller: password,
validator: (PassCurrentValue){
RegExp regex=RegExp(r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!##\$&*~]).{8,}$');
var passNonNullValue=PassCurrentValue??"";
if(passNonNullValue.isEmpty){
return ("Password is required");
}
else if(passNonNullValue.length<6){
return ("Password Must be more than 5 characters");
}
else if(!regex.hasMatch(passNonNullValue)){
return ("Password should contain upper,lower,digit and Special character ");
}
return null;
},
),
ElevatedButton(onPressed: (){
if(Form.of(context)?.validate()?? false){
Navigator.of(context).push(MaterialPageRoute(builder: (_)=>loginpage()));
}
}, child: Text("Login"))
],
);
}
),
)
in this picture you can see when you Enter inValid username and password it will not Navigate to another page.
when you Enter Valid Username and Password it will Navigate to another Page.
this is the best regx
bool passValid = RegExp("^(?=.{8,32}\$)(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!##\$%^&*(),.?:{}|<>]).*").hasMatch(value);
if (value.isEmpty ||!passValid)
{
return 'error';
}
I have a reactive login form following the BLOC pattern. I'm trying to programmatically clear all the values in it. In my Bloc, my submit function passes empty strings to my stream sinks:
class Bloc with Validators {
final _email = BehaviorSubject<String>();
final _password = BehaviorSubject<String>();
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password => _password.stream.transform(validatePassword);
Stream<bool> get submitValid => Observable.combineLatest2(email, password, (String e, String p) {
var valid = (e != null && e.isNotEmpty)
&& (p != null && p.isNotEmpty);
print('$e && $p = $valid');
return valid;
});
Function(String) get changeEmail => _email.sink.add;
Function(String) get changePassword => _password.sink.add;
submit() {
final validEmail = _email.value;
final validPassword = _email.value;
print('final values: $validEmail && $validPassword');
changeEmail('');
changePassword('');
}
dispose() {
_email.close();
_password.close();
}
}
When I press the submit button that calls this submit() function, I get the error messages for both of the text fields, because the values of email and password have changed behind the scenes, but they are not visually updated in the TextFields. Here are my StreamBuilders for my TextFields and Submit button:
Widget emailField(Bloc bloc) {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) { // re-runs build function every time the stream emits a new value
return TextField(
onChanged: bloc.changeEmail,
autocorrect: false,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
icon: Icon(Icons.email),
hintText: 'email address (you#example.com)',
labelText: 'Email',
errorText: snapshot.error
)
);
}
);
}
Widget passwordField(Bloc bloc) {
return StreamBuilder(
stream: bloc.password,
builder: (context, AsyncSnapshot<String> snapshot) {
return TextField(
onChanged: bloc.changePassword,
autocorrect: false,
obscureText: true,
decoration: InputDecoration(
icon: Icon(Icons.security),
hintText: 'must be greater than 6 characters',
labelText: 'Password',
errorText: snapshot.error
)
);
}
);
}
Widget submitButton(Bloc bloc) {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
child: Text('Logins'),
color: Colors.blue,
onPressed: !snapshot.hasData || snapshot.hasError || snapshot.data == false
? null
: bloc.submit
);
}
);
}'
And here is the code I'm using for my validators in my Bloc:
class Validators {
final validateEmail = StreamTransformer<String, String>.fromHandlers(
handleData: (email, sink) {
RegExp exp = new RegExp(r"^[a-zA-Z0-9.]+#[a-zA-Z0-9]+\.[a-zA-Z]+");
var valid = exp.hasMatch(email);
if (valid) {
sink.add(email);
} else {
sink.add('');
sink.addError('Invalid email address!');
}
}
);
final validatePassword = StreamTransformer<String, String>.fromHandlers(
handleData: (password, sink) {
var valid = password.length >= 6;
if (valid) {
sink.add(password);
} else {
sink.add('');
sink.addError('Password must be at least 6 characters long!');
}
}
);
}
In my validators, I emit an empty string whenever there is an error. This makes it so the submitValid getter works when the user invalidates something that used to be valid.
I know it's been a long time, but that's my way for solving it.
First, I've created a TextEditingController for my TextField. Then I've created two methods on my BLoC: updateTextOnChanged and updateTextElsewhere. On the fisrt one I just retrieved the value (because I need it to use later). On the second one I added a sink to update the controller on TextField.
Widget:
return StreamBuilder<String>(
stream: bloc.streamText,
builder: (context, snapshot) {
_controller.text = snapshot.data;
return Expanded(
child: TextField(
controller: _controller,
onChanged: (value) => {bloc.updateTextOnChanged(value)},
),
);
}
);
Bloc:
Stream<String> get streamText => _controllerTxt.stream;
String _myText;
void updateTextElsewhere(String value) {
_controllerTxt.sink.add(value);
}
void updateTextOnChanged(String value) {
_myText = value;
}
Then you just need to call updateTextElsewhere() whenever you need to update it outside onChanged.
In you're case just add an empty string like: updateTextElsewhere("");
In submit(), you seem like reseting username and password
changeEmail('');
changePassword('');
And as you commented , 're-runs build function every time the stream emits a new value'. It re-builds UI since the value updated to empty. Maybe does it cause the problem?
If you make a new Flutter project and include the dependencies and then replace your main.dart file you should be where I am on this question.
I left the original load: with Future.delayed but it doesn't seem to matter. I know partially what my problem is but am unable to come up with a better solution.
1) I don't seem to be using my snapshot.data and instead I am just making a empty List with str and then i just addAll into it and use that. So i'd love to not do that, i originally was using snapshot.data but ran into problems when I tried to "pull to load more data" which happens after you scroll to the bottom of the list.
The problem with my current method of doing this is that if you pull to load more users and then try to pull again before the users have loaded, The app breaks and doesn't wait for the data to properly load. I believe that I need to be doing that all in the load: of this library easy_refresh... but I am not sure how to rewrite my code to accomplish that.
How can I get my data to load with snapshot.data and then when I pull to refresh, I append 100 more users to that list but the UI waits for the list to update before it finishes the load. Would I be better off just putting a Blocking UI element and after the str list updates? and when new users are loaded I unblock the UI? which sorta feels hackish and not the correct way to solve this. The plugin itself should be able to do the loading and when its ready it stops the spinner under the list and says "finished".
pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_easyrefresh: ^1.2.7
http: ^0.12.0+2
main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:http/http.dart' as http;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
backgroundColor: Colors.white
),
home: DuelLeaderBoards(),
);
}
}
class DuelLeaderBoards extends StatefulWidget {
#override
_DuelLeaderBoardsState createState() => _DuelLeaderBoardsState();
}
class _DuelLeaderBoardsState extends State<DuelLeaderBoards> {
List<Entry> str = [];
GlobalKey<EasyRefreshState> _easyRefreshKey = new GlobalKey<EasyRefreshState>();
GlobalKey<RefreshHeaderState> _headerKey = new GlobalKey<RefreshHeaderState>();
GlobalKey<RefreshHeaderState> _connectorHeaderKey = new GlobalKey<RefreshHeaderState>();
GlobalKey<RefreshFooterState> _footerKey = new GlobalKey<RefreshFooterState>();
GlobalKey<RefreshFooterState> _connectorFooterKey = new GlobalKey<RefreshFooterState>();
Future<LeaderBoards> getLeaderBoards(start) async {
String apiURL = 'https://stats.quake.com/api/v2/Leaderboard?from=$start&board=duel&season=current';
final response = await http.get(apiURL);
if (response.statusCode == 200) {
final responseBody = leaderBoardsFromJson(response.body);
return responseBody;
} else {
throw Exception('Failed to load Data');
}
}
void updateLeaderBoardList(e) async {
setState(() {
str.addAll(e.entries);
});
}
#override
void initState() {
getLeaderBoards(0).then((onValue) => str = onValue.entries );
super.initState();
}
#override
Widget build(BuildContext context) {
Widget header = ClassicsHeader(
key: _headerKey,
refreshText: "pullToRefresh",
refreshReadyText: "releaseToRefresh",
refreshingText: "refreshing...",
refreshedText: "refreshed",
moreInfo: "updateAt",
bgColor: Colors.transparent,
textColor: Colors.white,
);
Widget footer = ClassicsFooter(
key: _footerKey,
loadHeight: 50.0,
loadText: "pushToLoad",
loadReadyText: "releaseToLoad",
loadingText: "loading",
loadedText: "loaded",
noMoreText: "Finished",
moreInfo: "updateAt",
bgColor: Colors.transparent,
textColor: Colors.white,
);
return FutureBuilder(
future: getLeaderBoards(0),
builder:
(BuildContext context, AsyncSnapshot<LeaderBoards> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return Builder(builder: (BuildContext context) {
return Center(
child: new EasyRefresh(
key: _easyRefreshKey,
behavior: ScrollOverBehavior(),
refreshHeader: ConnectorHeader(
key: _connectorHeaderKey,
header: header,
),
refreshFooter: ConnectorFooter(
key: _connectorFooterKey,
footer: footer,
),
child: CustomScrollView(
semanticChildCount: str.length,
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(<Widget>[header]),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return new Container(
height: 70.0,
child: Card(
child: new Text(
'${index+1}: ${str[index].userName}',
style: new TextStyle(fontSize: 18.0),
),
));
},
childCount: str.length,
)),
SliverList(
delegate: SliverChildListDelegate(<Widget>[footer]),
)
],
),
onRefresh: () async {
await new Future.delayed(const Duration(seconds: 0), () {
setState(() {});
});
},
loadMore: () async {
getLeaderBoards(str.length).then((onValue) => {
updateLeaderBoardList(onValue)
});
},
// loadMore: () async {
// await new Future.delayed(const Duration(seconds: 0), () {
// getLeaderBoards(str.length).then((onValue) => {
// updateLeaderBoardList(onValue)
// });
// });
// },
)
);
});
}
});
}
}
LeaderBoards leaderBoardsFromJson(String str) {
final jsonData = json.decode(str);
return LeaderBoards.fromJson(jsonData);
}
String leaderBoardsToJson(LeaderBoards data) {
final dyn = data.toJson();
return json.encode(dyn);
}
class LeaderBoards {
String boardType;
List<Entry> entries;
int totalEntries;
LeaderBoards({
this.boardType,
this.entries,
this.totalEntries,
});
factory LeaderBoards.fromJson(Map<String, dynamic> json) => new LeaderBoards(
boardType: json["boardType"] == null ? null : json["boardType"],
entries: json["entries"] == null ? null : new List<Entry>.from(json["entries"].map((x) => Entry.fromJson(x))),
totalEntries: json["totalEntries"] == null ? null : json["totalEntries"],
);
Map<String, dynamic> toJson() => {
"boardType": boardType == null ? null : boardType,
"entries": entries == null ? null : new List<dynamic>.from(entries.map((x) => x.toJson())),
"totalEntries": totalEntries == null ? null : totalEntries,
};
}
class Entry {
String userName;
int eloRating;
String profileIconId;
String namePlateId;
Entry({
this.userName,
this.eloRating,
this.profileIconId,
this.namePlateId,
});
factory Entry.fromJson(Map<String, dynamic> json) => new Entry(
userName: json["userName"] == null ? null : json["userName"],
eloRating: json["eloRating"] == null ? null : json["eloRating"],
profileIconId: json["profileIconId"] == null ? null : json["profileIconId"],
namePlateId: json["namePlateId"] == null ? null : json["namePlateId"],
);
Map<String, dynamic> toJson() => {
"userName": userName == null ? null : userName,
"eloRating": eloRating == null ? null : eloRating,
"profileIconId": profileIconId == null ? null : profileIconId,
"namePlateId": namePlateId == null ? null : namePlateId,
};
}
I looked at the documentation of loadMore. Since it says that the body of the function assigned to loadMore should be async, you do not need to use then:
loadMore: () async {
final result = await getLeaderBoards(str.length);
updateLeaderboardList(result);
},
loadMore: () async {
await getLeaderBoards(str.length).then((onValue) => {
updateLeaderboardList(onValue)
});
},
but putting "await" my loader waits for the function to complete before it finishes the animation.
I have a JSON app calling users but I am having problems dealing with null Maps.
This is the Json file.
MemberInfo.json
{
"Dependents": [
{
"Name": "Kim",
"Relationship": "Parent",
"Entitlements": {
"GP": {
"Entitlement": "10000",
"Utilisation": "500",
"Balance": "9500"
},
"OPS": {
"Entitlement": "10000",
"Utilisation": "500",
"Balance": "9500"
},
"IP": {
"Entitlement": "50000",
"Utilisation": "17000",
"Balance": "33000"
}
}
},
{
"Name": "Tim",
"Relationship": "Spouse",
"Entitlements": {
"GP": {
"Entitlement": "10000",
"Utilisation": "500",
"Balance": "9500"
},
"OPS": {
"Entitlement": "10000",
"Utilisation": "500",
"Balance": "9500"
},
"IP": {
}
}
}
]
}
And the dart file is as follows:
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
final String url = "http://crm.emastpa.com.my/MemberInfo.json";
final String url2 = "http://crm.emastpa.com.my/MemberInfo2.json";
class Dependents extends StatelessWidget {
Dependents({Key key, this.index, this.name, this.page}) : super(key:key);
final int index;
final String name;
final int page;
Future<String> jsonContent() async {
var res = await http.get(
Uri.encodeFull(url2),
headers: {"Accept": "application/json"});
return res.body;
}
#override
Widget build(BuildContext context) {
Widget fifthBody = new Container(
child: new Center(
child: new FutureBuilder<String>(
future: jsonContent(),
builder: (context, snapshot){
if(snapshot.hasData){
List<Widget> widgets = [];
//get snapshot data from JSON tree
var jsondecode = json.decode(snapshot.data);
//[]->Entitlements
var jsonEntData = jsondecode["Dependents"][index]["Entitlements"];
//Everything under Entitlements
var jsonEntDataGP = jsonEntData["GP"];
var jsonEntDataOPS = jsonEntData["OPS"];
var jsonEntDataIP = jsonEntData["IP"];
//GP Branch
var gp_ent = jsonEntDataGP["Entitlement"];
var gp_util = jsonEntDataGP["Utilisation"];
var gp_bal = jsonEntDataGP["Balance"];
//OPS branch
var ops_ent = jsonEntDataOPS["Entitlement"];
var ops_util = jsonEntDataOPS["Utilisation"];
var ops_bal = jsonEntDataOPS["Balance"];
//IP branch
var ip_ent = jsonEntDataIP["Entitlement"];
var ip_util = jsonEntDataIP["Utilisation"];
var ip_bal = jsonEntDataIP["Balance"];
jsonEntDataGP != null?
widgets.add(new ExpansionTile(
title: new Text("GP"),
children: <Widget>[
new ListTile(
title: new Text("Entitlement"),
trailing: new Text(gp_ent),
)
],
))
: new Center();
jsonEntDataOPS != null?
widgets.add(new ExpansionTile(
title: new Text("OPS"),
children: <Widget>[
new ListTile(
title: new Text("Entitlement"),
trailing: new Text(ops_ent),
)
]))
: new Center();
jsonEntDataIP != null?
widgets.add(new ExpansionTile(
title: new Text("IP"),
children: <Widget>[
new ListTile(
title: new Text("Entitlement"),
trailing: new Text(ip_ent),
)
]))
: new Center();
return new Column(
children: widgets,
);
}else if(snapshot.hasError){
return new Text(snapshot.error);
}
//loading the page
return new Center(
child: new CircularProgressIndicator(),
);
}),
),
);
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("$name + index: $index"),
),
body: fifthBody
),
);
}
}
This is the error I am getting:
I/flutter ( 8842): The following assertion was thrown building FutureBuilder<String>(dirty, state:
I/flutter ( 8842): _FutureBuilderState<String>#f9262):
I/flutter ( 8842): 'package:flutter/src/widgets/text.dart': Failed assertion: line 230 pos 15: 'data != null': is not
I/flutter ( 8842): true.
I am trying to build a list of the user's entitlements but I am not so sure as to how to deal with a null key such as "IP". The app works as long as the key has values inside but will return the exception if the key is null.
How should I deal with this problem?
IP is empty JsonObject
"IP": {},
So when you try to access data of IP object it returns null & passing that to textview later gives data is null error.
You are trying to get data from json object without keys as
var ip_ent = jsonEntDataIP["Entitlement"];
And using later as
new Text(ip_ent)
To overcome this you need to assign default value in case of variable is null or can hide that view as your requirement.
To assign default value in case of null, please do this.
var ip_ent = jsonEntDataIP["Entitlement"] ?? '';
var jsonEntDataIP = jsonEntData["IP"] ?? '';