My process is as follows. The screen has just two elements - TextFormField and an ElevatedButton.
Get email address from user
User clicks button
Button validates input, then
Calls FutureBuilder, which
Tries to fetch client record from REST API
Redirects to appropriate route
This is my first Flutter/Dart program FYI, so I might be making a beginner mistake.
Question: The very first line of the FutureBuilder isn't executed. No error, no messages, nothing. Why does this happen?
The user enters the email address, clicks the button, the fetchClientInfo function is executed, which returns a Future<ClientInfo> and that's that.
Could you help please?
#override
Widget build(BuildContext context) {
final _formKey = GlobalKey<FormState>();
return Scaffold(
appBar: AppBar(
title: Text("Register Profile"),
),
body: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.all(20),
child: TextFormField(
controller: emailController,
validator: (email) {
if (email.isEmpty) {
return 'Please enter your email address.';
} else if (!EmailValidator.validate(email)) {
return 'Please enter a valid email address.';
}
return null;
},
decoration: InputDecoration(
border: new UnderlineInputBorder(borderSide: new BorderSide(color: Colors.red)),
labelText: 'Email',
hintText: 'Enter your email address',
contentPadding: EdgeInsets.all(20.0),
),
)),
ElevatedButton(
onPressed: () => {
if (_formKey.currentState.validate())
{
FutureBuilder<ClientInfo>(
future: fetchClientInfo(emailController.text),
builder: (BuildContext context, snapshot) {
print("here");
if (snapshot.data.outcome) {
return Text("main screen");
} else if (!snapshot.data.outcome) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
RegisterNewUser(emailAddress: emailController.text)));
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// Show a spinner
return CircularProgressIndicator();
},
)
}
},
child: Text(
"Check Email",
))
])));
}
Future<ClientInfo> fetchClientInfo(String emailAddress) async {
var url = Uri.https(APIAccess.baseAPIURL, APIAccess.pathToClientAPI, {
'client_id': '$emailAddress',
'action': 'info',
'key': '${APIAccess.key}'
});
final response = await http.get(url);
if (response.statusCode == 200) {
return ClientInfo.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load album');
}
}
You're missing a couple of things:
return statement (in your lambda you're creating a FutureBuilder but it's not being used anywhere)
if(true) {
return SizedBox.shrink();
}
correct lambda syntax (well, technically it's correct but it's not doing what you want): https://dart.dev/guides/language/language-tour#anonymous-functions
// that's how compiler sees it
Map<dynamic, dynamic> Function() foo = () => {
};
What you did reminds me of javascript, but in dart lambdas look a bit different
return Button(
onTap: () => doStuff(),
);
return Button(
onTap: () {
doStuff();
}
);
// and if you want to return a value from block lambda
return Builder(
builder: (context) {
return SizedBox.shrink();
}
);
rendering widget on tap
When handling tap events, it's best to redirect calls to a component that's handling business logic, and only listen for current state in the widget.
What you want to read about is state management. The topic is highly opinionated, so you have to choose yourself the solution that's right for you. https://flutter.dev/docs/development/data-and-backend/state-mgmt
I myself like using a slightly modified version of bloc. You can find the 'original' one here: https://pub.dev/packages/flutter_bloc
A new de-facto standard if it comes to state management is Riverpod
If you just want to make your code work, do something like this:
class Demo extends StatefulWidget {
#override
_DemoState createState() => _DemoState();
}
class _DemoState extends State<Demo> {
Future<ClientInfo?> clientInfo = Future.value(null);
#override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () {
setState(() {
clientInfo = fetchClientInfo(emailController.text);
});
},
child: _buildButtonContent(),
),
FutureBuilder<ClientInfo>(
initialData: null,
future: clientInfo,
builder: (BuildContext context, snapshot) {
if (snapshot.data == null) {
return SizedBox.shrink();
} else {
return Text(snapshot.data.toString());
}
},
)
],
);
}
}
Related
I have a page that shows a loading while making my API call, and once the call is done it shows the received data.
On debugger everything works correctly, but when I create the apk with 'flutter build apk', and download it, the loading remains indefinitely.
I also put a showDialog at the end of my Provider function that makes the API call (I put this showDialog just below notifyListeners().
I can't understand why in debug it works and in release it doesn't.
(This notifyListeners thing not working just does it for every API call I make)
This is the code of the provider function that makes the api call:
Future<void> getUserSites(context) async {
_userSites.clear();
isLoading = true;
notifyListeners();
try {
final response = await NetworkService.call(
url: '/api/structure/Sites',
method: Method.Get,
context: context) as List<dynamic>;
for (var i = 0; i < response.length; i++) {
_userSites.add(Sites.fromJson(response.elementAt(i)));
}
if (defaultSite == null) {
if (SimplePreferences.getDefaultSite() == null) {
defaultSite = _userSites.isNotEmpty ? _userSites.first : null;
if (defaultSite != null) {
SimplePreferences.setDefaultSite(defaultSite!.id);
}
} else {
defaultSite = _userSites.firstWhere(
(element) => element.id == SimplePreferences.getDefaultSite()!);
}
}
} catch (e) {
inspect(e);
if (SimplePreferences.getToken() != null) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('General Error'),
content: Text(e.toString()),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
'Ok',
),
)
],
),
);
}
// throw e;
}
isLoading = false;
notifyListeners();
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('getUserSites done!'),
content: Text(_userSites.toString()),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
'Ok',
),
)
],
),
);
}
this is the Home page code:
class HomePageScreen extends StatelessWidget { const HomePageScreen({super.key}); static const String routeName = '/';
#override Widget build(BuildContext context) { log('New Page: Home Page'); final provider = Provider.of<MyManager>(context);
return provider.isLoading ? const Center(
child: CircularProgressIndicator(),
)
: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MainButton(
onTap: () async {
Navigator.of(context)
.pushNamed(ShowPatrolScreen.routeName);
await provider.getPatrol(context);
},
icon: Icons.home,
title: 'ShowPatrol',
),
printSito(provider.defaultSite?.description ?? 'Nessun Sito', context),
PrintRequestZ(
showCompleted: false,
),
],
),
),
);
}
Widget printSito(String name, context) { .... //pass context for Navigator and Theme } } `
this is the main page:
...
final myScreens = [
const HomePageScreen(),
...
];
#override
void initState() {
// TODO: implement initState
super.initState();
print('token: ${SimplePreferences.getToken()}');
if (SimplePreferences.getToken() == null){
Navigator.of(context).pushReplacementNamed('/Auth');
}
var provider = Provider.of<MyManager>(context, listen: false);
provider.setAll(context); //this function calls all my API calls, but for testing, I commented out all other functions and kept only the one written above
}
#override
Widget build(BuildContext context) {
var provider = Provider.of<MyManager>(context);
return Scaffold(
appBar: const MyAppBar(title: 'Ronda',canGoBack: false,),
body: myScreens[currentPage],
bottomNavigationBar: ...
),
}
Thanks in advance!
after some research i found the solution.
You have to use WidgetsBinding.instance.addPostFrameCallback
in the parent component.
So my home page now looks like this:
#override
void initState() {
// TODO: implement initState
super.initState();
print('token: ${SimplePreferences.getToken()}');
if (SimplePreferences.getToken() == null){
Navigator.of(context).pushReplacementNamed('/Auth');
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
var provider = Provider.of<MyManager>(context, listen: false);
provider.setAll(context); //this function calls all my API calls, but for testing, I commented out all other functions and kept only the one written above
});
}
I don't quite understand why though. If someone could explain it to me, I'd be very happy
Use Consumer to access the Provider's Variable
return Consumer<YourProviderName>(builder : (context, value, child){
return value.isLoading? const Center(
child: CircularProgressIndicator(),
):YourWidget(),
});
Good day, when I run flutter -> errors in the window like 'GlobalKey was used multiple times inside one widget's child list..' Sorry if it is a stupid question.. so what do I have to do?
HOW do I understand I used 'global key' more than 1times -> what should I do?
code ->
Future<void> main() async{
WidgetsFlutterBinding.ensureInitialized();
Firebase.initializeApp();
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context){
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Mama',
onGenerateRoute: (settings){
switch(settings.name){
case '/home':
return PageTransition(
child: HomePage(),
type: PageTransitionType.fade
);
}
},
);
}
}
class MyHomePage extends ConsumerWidget {
GlobalKey<ScaffoldState> scaffoldState = new GlobalKey();
processLogin(BuildContext context) async{
var user = FirebaseAuth.instance.currentUser;
if(user == null){
FirebaseAuthUi.instance()
.launchAuth([AuthProvider.phone()]).then((FirebaseUser) async {
context.read().state = FirebaseAuth.instance.currentUser;
// Navigator.pushNamedAndRemoveUntil(
// context, '/home', (route) => false);
await checkLoginState(context,true);
ScaffoldMessenger.of(scaffoldState.currentContext!).showSnackBar(SnackBar(content: Text('Login Success ${FirebaseAuth.instance.currentUser?.phoneNumber}')));
}).catchError((e){
if(e is PlatformException)
if(e.code == FirebaseAuthUi.kUserCancelledError){
ScaffoldMessenger.of(scaffoldState.currentContext!).showSnackBar(SnackBar(content: Text('${e.message}')));
}
else{
ScaffoldMessenger.of(scaffoldState.currentContext!).showSnackBar(const SnackBar(content: Text('Unk Error')));
}
});
}
else{
}
}
Widget build(BuildContext context, watch){
return Scaffold(
key: scaffoldState,
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/my_bg.png'),
fit: BoxFit.cover
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children:[
Container(
padding: const EdgeInsets.all(16),
width: MediaQuery.of(context).size.width,
child:FutureBuilder(
future: checkLoginState(context,false),
builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator());
} else{
var userState = snapshot.data as LOGIN_STATE;
if(userState == LOGIN_STATE.LOGGED){
return Container();
}
else{
return ElevatedButton.icon(
onPressed: ()=> processLogin(context),
icon :Icon(Icons.phone, color: Colors.white,),
label: Text('LOGIN WITH ME', style: TextStyle(color:Colors.white),),
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.black)),
);
}
}
}
),
)
],
),
),
);
}
Future<LOGIN_STATE> checkLoginState(BuildContext context, bool fromLogin) async {
await Future.delayed(Duration(seconds: fromLogin == true ? 0 : 3)).then((value)=>{
FirebaseAuth.instance.currentUser
?.getIdToken()
.then((token){
print('$token');
context.read().state = token;
Navigator.pushNamedAndRemoveUntil(context, '/home', (route) => false);
})
});
return FirebaseAuth.instance.currentUser != null
? LOGIN_STATE.LOGGED
: LOGIN_STATE.NOT_LOGIN;
}
}
QUESTION:
How do I properly use Globalkeys to ensure that there is only one per widget? Any help would be greatly appreciated.
Nothing, Expecting an answer
you need to store global key inside a variable then use the variable name wherever you want to use, take reference from the below code, I am not sure, whether it will help you or not, but you will get an idea from this.
final GlobalKey<FormState> _formKey = GlobalKey();
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
TextFormField(
decoration: const InputDecoration(labelText: 'E-Mail'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value!.isEmpty || !value.contains('#')) {
return 'Invalid email!';
}
return null;
// return null;
},
onSaved: (value) {
_authData['email'] = value!;
},
),
),
also , you have written three lines like this,
ScaffoldMessenger.of(scaffoldState.currentContext!).
here,replace "scaffoldState" with context.
I am creating an application for managing shopping list, products and users using firestore and flutter, and I am starting to learn the bloc pattern. I have 2 blocs in my app the AuthBloc for users, and the ShoppingBloc for shopping lists. Right now I display the list of shopping lists of an user, and when I press a button I want to go to another screen to create a new shopping list. I want that when I press the button I change the state and when the state is change a listener (or something similar) changes the view.
My ShoppingListView is:
#override
Widget build(BuildContext context) {
context.read<ShoppingBloc>().add(const ShoppingEventInitialize());
return BlocConsumer<ShoppingBloc, ShoppingState>(
listener: (context, state) {
if (state is ShoppingCartState) {
print('El estado es shoppingCartState');
} else {
print('El estado es ' + state.toString());
}
},
builder: (context, state) {
if (state is ShoppingCartState) {
return Scaffold(
appBar: AppBar(
title: const Text('Your Shopping Cart Lists'),
actions: [
IconButton(
onPressed: () {
context.read<ShoppingBloc>().add(CreateEvent());
},
icon: const Icon(Icons.add),
),
PopupMenuButton<MenuAction>(
onSelected: (value) async {
switch (value) {
case MenuAction.logout:
final shouldLogout = await showLogOutDialog(context);
if (shouldLogout) {
context.read<AuthBloc>().add(const AuthEventLogout());
}
}
},
itemBuilder: (context) {
return [
const PopupMenuItem<MenuAction>(
value: MenuAction.logout,
child: Text('Logout'),
),
];
},
)
],
),
body: StreamBuilder(
stream:
_shoppingCartService.getShoppingCartLists(ownerUserId: userId),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.active:
if (snapshot.hasData) {
final allShoppingCartLists =
snapshot.data as Iterable<CloudShoppingCartList>;
return ShoppingCartListView(
shoppingCartLists: allShoppingCartLists,
onDeleteShoppingCartList: (shoppingCartList) async {
await _shoppingCartService.deleteShoppingCart(
shoppingCartListId:
shoppingCartList.shoppingCartListId);
},
onTap: (shoppingCartList) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ShoppingCartListDetailsView(
shoppingCartId:
shoppingCartList.shoppingCartListId,
shoppingCartName: shoppingCartList
.name)),
);
},
);
} else {
return const CircularProgressIndicator();
}
default:
return const CircularProgressIndicator();
}
})
);
} else {
return (const CreateUpdateShoppingCartListView());
}
And the CreateShoppingListView is:
#override
Widget build(BuildContext context) {
return BlocBuilder<ShoppingBloc, ShoppingState>(
builder: (context, state) {
if (state is ShoppingCartState) {
return ShoppingCartView();
} else
return Scaffold(
appBar: AppBar(
title: const Text('New shopping cart list'),
actions: [
IconButton(
onPressed: () {
final text = _textController.text;
final ownerUserId = currentUser.id;
context.read<ShoppingBloc>()
.add(ShoppingCreateNewShoppingCartEvent(
ownerUserId, text));
},
icon: const Icon(Icons.add),
),
IconButton(
onPressed: () async {
final text = _textController.text;
if (_shoppingCartList == null || text.isEmpty) {
await showCannotShareEmptyNoteDialog(context);
} else {
Share.share(text);
}
},
icon: const Icon(Icons.share),
)
],
),
body: Column(children: [
TextField(
controller: _textController,
keyboardType: TextInputType.multiline,
maxLines: null,
textInputAction: TextInputAction.go,
decoration: const InputDecoration(
hintText: 'Start typing you shopping cart name...',
),
),
],
}
}
I needed to create a bloc builder in the CreateShoppingListView to listen to the shoppingState and change view if the state is ShoppingCartState. My question is, it is necessary to create a bloc builder in each view to react to the states changes or is there a way to create a bloc builder that works for all views. I don't know if I explained myself corretly.
Thank you in advance
I can't seem to get my future builder to update. The api response is working fine I can see it in my logs. (model.getSuburbs). but it doesn't seem like my my future in the FutureBuilder suburbs is doing anything.. Am I missing something obvious (The onSubmitis trigger when I enter the last number and triggers the api)
class PostcodePage extends StatefulWidget {
static Route<dynamic> route() {
return MaterialPageRoute(
builder: (BuildContext context) => PostcodePage(),
);
}
#override
_PostcodeScreenState createState() => _PostcodeScreenState();
}
class _PostcodeScreenState extends State<PostcodePage> {
PostcodeViewmodel model = serviceLocator<PostcodeViewmodel>();
Future<List<Suburb>> suburbs;
String postCode;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.all(32),
child: Column(children: [
SizedBox(height: 200),
PinEntryField(
onSubmit: (input) => getSub(pc: input),
),
FutureBuilder<List<Suburb>>(
future: suburbs,
builder: (context, snapshot) {
if (snapshot.connectionState==ConnectionState.active) {
return Text('Would Like something here...');
} else
return Text('But always end up here...');
},
),
// (postCode != null) Text(postCode),
SizedBox(
height: 300,
),
SizedBox(
width: double.maxFinite,
child: OnBoardingButton(
text: 'Begin',
onPressed: () {},
color: Color(0xff00E6B9),
),
),
]),
),
);
}
getSub({String pc}) {
setState(() {
suburbs = model.getSuburbs(country: 'au', postcode: pc);
});
}
}
Try to change your condition inside the builder.
This code snapshot.connectionState==ConnectionState.active could be really really short depending on the suburbs future.
Please try this inside the builder.
if (snapshot.hasData) {
return Text('Would Like something here...');
} else {
return Text('But always end up here...');
}
I am calling login api on button click, I am able to get response from server but on clicking on button it doesn't show progress bar. I am using BLoC pattern for this. Here is the code,
import 'package:flutter/material.dart';
import '../blocs/bloc.dart';
import '../blocs/provider.dart';
import '../models/login_response.dart';
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Provider(
child: new Scaffold(
body: Container(
child: LoginForm(),
),
),
);
}
}
class LoginForm extends StatefulWidget {
// since its a stateful widget we need to create state for it.
const LoginForm({Key key}) : super(key: key);
#override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
#override
Widget build(BuildContext context) {
return Form(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 50),
),
// Start creating widget here.
emailField(),
passwordField(),
Container(margin: EdgeInsets.only(top: 25.0)),
submitButton()
],
),
);
}
Widget emailField() {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changeEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you#example.com',
labelText: 'Email Address',
errorText: snapshot.error
),
);
}
);
}
Widget passwordField() {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changePassword,
obscureText: true,
decoration: InputDecoration(
labelText: 'Please enter your password',
hintText: 'Password',
errorText: snapshot.error
),
);
},
);
}
Widget submitButton() {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
onPressed:() => showWidgetForNetworkCall(context),
// onPressed: () {
// // Do submit button action.
// showWidgetForNetworkCall(context);
// // callLoginApi();
// },
child: const Text('Login'),
textColor: Colors.white,
color: Colors.blueAccent,
);
},
);
}
// Loading Widget
Widget _buildLoadingWidget() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Loading data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
],
),
);
}
// // Error Widget
Widget _buildErrorWidget(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Loading error data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
],
),
);
}
// show server data
showServerData() {
print(" Servr >>>>>> Data : ");
}
Widget showWidgetForNetworkCall(BuildContext context) {
bloc.loginSubmit();
return StreamBuilder(
stream: bloc.loginSubject.stream,
builder: (context, AsyncSnapshot<LoginResponse>snapshot){
if (snapshot.hasData) {
return showServerData();
} else if (snapshot.hasError) {
return _buildErrorWidget(snapshot.error);
} else {
return _buildLoadingWidget();
}
},
);
}
}
This is my login_screen.dart. And my bloc class for api call is:
postData() async {
LoginResponse response = await _repository.postData(_loginResource);
_subject.sink.add(response);
}
I am able to parse json api, but not able to get the response of my model i.e, 'LoginResponse' in login_screen.dart class and also the CircularProgressBar doesn't show when api is called on button click.
Code of the BLoC class is :
import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'validators.dart';
import '../models/login_response.dart';
import '../repository/login_repository.dart';
import '../resources/login_resource.dart';
class Bloc extends Object with Validators {
final LoginRepository _repository = LoginRepository();
final BehaviorSubject<LoginResponse> _subject =
BehaviorSubject<LoginResponse>();
LoginResource _loginResource = LoginResource();
final _email = BehaviorSubject<String>(); // Declaring variable as private
final _password = BehaviorSubject<String>(); // Declaring variable as private
// Add data to stream (Its like setter)
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password =>
_password.stream.transform(validatePassword);
Stream<bool> get submitValid => Observable.combineLatest2(email, password, (e, p) => true);
// Change data. For retrieveing email value.
Function(String) get changeEmail => _email.sink.add;
Function(String) get changePassword => _password.sink.add;
loginSubmit() {
_loginResource.email = "bar1";
_loginResource.password = "bar2";
postData();
}
postData() async {
LoginResponse response = await _repository.postData(_loginResource);
_subject.sink.add(response);
}
dispose() {
_email.close();
_password.close();
_subject.close();
}
BehaviorSubject<LoginResponse> get loginSubject => _subject;
}
final bloc = Bloc();
Kindly let me know what I am missing. Thanks in advance :)
Well here we go. I make some changes in your UI layer and in BLoC class with order to accomplish what you're asking for. I will firstly show the pieces of code that I insert and explain what I was think when I wrote it and after all I will paste the entire source code will all changes. Maybe you can use the concept that I had used to adapt the source code to your needs. All code has comments so please read it will help you a lot.
First of all I create an enum to represent the status of the login process and a class that holds the login process status and a message about it. Both are part of your UI layer.
/// NON_LOGIN: means that login is not happening
/// LOGGIN: means that login is happening
/// LOGIN_ERROR: means that something is wrong with login
/// LOGIN_SUCCESS: the login process was a success.
enum LoginStatus { NON_LOGIN, LOGGING, LOGIN_SUCCESS, LOGIN_ERROR }
class LoginState {
final LoginStatus status;
final String message;
LoginState({this.status, this.message});
}
In _LoginFormState class inside build method I inserted a StreamBuilder that will show and hide the progressbar when the login is happening or show an error widget.
#override
Widget build(BuildContext context) {
return Form(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 50),
),
// Start creating widget here.
emailField(),
passwordField(),
Container(margin: EdgeInsets.only(top: 25.0)),
submitButton(),
StreamBuilder<LoginState>(
stream: bloc.loginStateStream,
builder: (context, AsyncSnapshot<LoginState> snapshot){
if ( !snapshot.hasData )
return Container();
switch(snapshot.data.status){
case LoginStatus.LOGGING:
return _buildLoadingWidget();
case LoginStatus.LOGIN_ERROR:
return _buildErrorWidget(snapshot.data.message);
case LoginStatus.LOGIN_SUCCESS:
// Here you can go to another screen after login success.
return Center(child: Text("${snapshot.data.message}"),);
case LoginStatus.NON_LOGIN:
default:
return Container();
}
},
),
],
),
);
}
And the last change in your UI layer was in submitButton method the only change was in onPress event of your button now it calls bloc.loginSubmit method.
return RaisedButton(
onPressed:() => bloc.loginSubmit(), // the only change
child: const Text('Login'),
textColor: Colors.white,
color: Colors.blueAccent,
);
Now all the changes are in BLoC class. Basically I created a new subject for handling the state changes of login process using LoginStatus enum and LoginState class and tell to view what widget must be showed to user.
//The subject and a get method to expose his stream
final PublishSubject<LoginState> _loginStateSubject = new PublishSubject();
Observable<LoginState> get loginStateStream => _loginStateSubject.stream;
All the login state changes handling I wrote inside postData method.
postData() async {
// this call will change the UI and a CircularProgressBar will be showed.
changeLoginState(state: LoginState( status: LoginStatus.LOGGING, message: "logging") );
// waiting for login response!
LoginResponse response = await _repository.postData(_loginResource);
print(response); // just to text debug your response.
//Here you can verify if the login process was successfully or if there is
// some kind of error based in your LoginResponse model class.
// avoiding write this logic in UI layer.
if(response.hasError){
changeLoginState(state: LoginState(status: LoginStatus.LOGIN_ERROR,
message: response.errorMessage)
);
// and after 1.5 seconds we make the error message disappear from UI.
// you can do this in UI layer too
Future.delayed(Duration(milliseconds: 1500), (){
// you can pass null to state property, will make the same effect
changeLoginState(state: LoginState(status: LoginStatus.NON_LOGIN)); });
}
else {
changeLoginState(state: LoginState(status:
LoginStatus.LOGIN_SUCCESS, message: "Login Success"));
}
//_subject.sink.add(response);
}
With this approach you avoid send to your UI layer objects from you model layer like LoginResponse class objects and this kind of concept makes your code more clean and do not broken MVC pattern and your UI layer holds only layout code.
Make some tests, I didn't, adapt to your needs and comment if you need something I will answer when I can.
The entire source code:
/// NON_LOGIN: means that login is not happening
/// LOGGIN: means that login is happening
/// LOGIN_ERROR: means that something is wrong with login
/// LOGIN_SUCCESS: the login process was a success.
///
enum LoginStatus { NON_LOGIN, LOGGING, LOGIN_SUCCESS, LOGIN_ERROR }
class LoginState {
final LoginStatus status;
final String message;
LoginState({this.status, this.message});
}
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Provider(
child: new Scaffold(
body: Container(
child: LoginForm(),
),
),
);
}
}
class LoginForm extends StatefulWidget {
// since its a stateful widget we need to create state for it.
const LoginForm({Key key}) : super(key: key);
#override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
#override
Widget build(BuildContext context) {
return Form(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 50),
),
// Start creating widget here.
emailField(),
passwordField(),
Container(margin: EdgeInsets.only(top: 25.0)),
submitButton(),
StreamBuilder<LoginState>(
stream: bloc.loginStateStream,
builder: (context, AsyncSnapshot<LoginState> snapshot){
if ( !snapshot.hasData )
return Container();
switch(snapshot.data.status){
case LoginStatus.LOGGING:
return _buildLoadingWidget();
case LoginStatus.LOGIN_ERROR:
return _buildErrorWidget(snapshot.data.message);
case LoginStatus.LOGIN_SUCCESS:
// Here you can go to another screen after login success.
return Center(child: Text("${snapshot.data.message}"),);
case LoginStatus.NON_LOGIN:
default:
return Container();
}
},
),
],
),
);
}
Widget emailField() {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changeEmail,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you#example.com',
labelText: 'Email Address',
errorText: snapshot.error
),
);
}
);
}
Widget passwordField() {
return StreamBuilder(
stream: bloc.password,
builder: (context, snapshot) {
return TextField(
onChanged: bloc.changePassword,
obscureText: true,
decoration: InputDecoration(
labelText: 'Please enter your password',
hintText: 'Password',
errorText: snapshot.error
),
);
},
);
}
Widget submitButton() {
return StreamBuilder(
stream: bloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
onPressed:() => bloc.loginSubmit(),
child: const Text('Login'),
textColor: Colors.white,
color: Colors.blueAccent,
);
},
);
}
// Loading Widget
Widget _buildLoadingWidget() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Loading data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
],
),
);
}
// // Error Widget
Widget _buildErrorWidget(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Loading error data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
],
),
);
}
/*
// show server data
showServerData() {
print(" Servr >>>>>> Data : ");
}
Widget showWidgetForNetworkCall() {
return StreamBuilder(
stream: bloc.loginSubject.stream,
builder: (context, AsyncSnapshot<LoginResponse>snapshot){
if (snapshot.hasData) {
return showServerData();
} else if (snapshot.hasError) {
return _buildErrorWidget(snapshot.error);
} else {
return _buildLoadingWidget();
}
},
);
}*/
}
class Bloc extends Object with Validators {
//final BehaviorSubject<LoginResponse> _subject = BehaviorSubject<LoginResponse>();
//BehaviorSubject<LoginResponse> get loginSubject => _subject;
final LoginRepository _repository = LoginRepository();
final PublishSubject<LoginState> _loginStateSubject = new PublishSubject();
Observable<LoginState> get loginStateStream => _loginStateSubject.stream;
LoginResource _loginResource = LoginResource();
final _email = BehaviorSubject<String>(); // Declaring variable as private
final _password = BehaviorSubject<String>(); // Declaring variable as private
// Add data to stream (Its like setter)
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password => _password.stream.transform(validatePassword);
Stream<bool> get submitValid => Observable.combineLatest2(email, password, (e, p) => true);
// Change data. For retrieveing email value.
Function(String) get changeEmail => _email.sink.add;
Function(String) get changePassword => _password.sink.add;
void changeLoginState({LoginState state } ) => _loginStateSubject.sink.add(state);
loginSubmit() {
_loginResource.email = "bar1";
_loginResource.password = "bar2";
postData();
}
postData() async {
// this call will change the UI and a CircularProgressBar will be showed.
changeLoginState(state: LoginState( status: LoginStatus.LOGGING, message: "logging") );
// waiting for login response!
LoginResponse response = await _repository.postData(_loginResource);
print(response); // just to text debug your response.
//Here you can verify if the login process was successfully or if there is
// some kind of error based in your LoginResponse model class.
if(response.hasError){
changeLoginState(state: LoginState(status: LoginStatus.LOGIN_ERROR,
message: response.errorMessage)
);
// and after 1.5 seconds we make the error message disappear from UI.
// you can do this in UI layer too
Future.delayed(Duration(milliseconds: 1500), (){
// you can pass null to state property, will make the same effect
changeLoginState(state: LoginState(status: LoginStatus.NON_LOGIN)); });
}
else {
changeLoginState(state: LoginState(status:
LoginStatus.LOGIN_SUCCESS, message: "Login Success"));
}
//_subject.sink.add(response);
}
dispose() {
_loginStateSubject.close();
_email.close();
_password.close();
//_subject.close();
}
}
final bloc = Bloc();