flutter: how to show loading animation before log in? - flutter

I'm working with flutter. After I input my id and password, I want to show a log in animation before entering the home page. I use a dialog but I feel like my code is very blunt and has potential bugs. Is there a better solution?
// this part is for the input content is legal
else {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return LoadingStyle.buildWidget(); // this is a simple loading animation
});
service.createSession(context, code, id).then((response) { // log in part
if (response != null) {
this.saveUser(response); // something about saving user info
} else {
print('null respose');
}
}).catchError((e) {
if (e is GrpcError && e.code == StatusCode.unauthenticated) {
setState(() {
this.errorMessage = "grpc network error";
});
}
}).whenComplete(() {
Navigator.of(context).pop(); // pop dialog here, is this right?
MyRoutersl.goNewPage(context); // enter the new page
});
}

I suggest to use FutureBuilder. There is also some default loading Widget like CircularProgressIndicator() can be used when in progress.
Because login is some sort of Asynchronous progress, you can use FutureBuilder like below:
FutureBuilder(
future: service.createSession(... // Use Async function to return the result
builder: (context, snapshot) {
if(snapshot.hasData && snapshot.connectionState == done ){
// return widget after login successfully
// result should equal to snapshot.data
} else {
// return CircularProgressIndicator();
}
}
)
If you need more fancy loading indicator, you can check this package flutter_spinkit

You can use Libraries from pub.dev like loading_overlay
or you can build your own loading widget, example :
class OverlayWidget extends StatelessWidget {
final Widget child;
final bool isLoading;
OverlayWidget({#required this.child, this.isLoading = false})
: assert(child != null);
#override
Widget build(BuildContext context) {
return Stack(
children: [
child,
Visibility(
visible: isLoading,
child: Container(
color: Colors.grey.withOpacity(0.4),
child: Center(
child: Platform.isIOS
? CupertinoActivityIndicator(
radius: 20,
)
: CircularProgressIndicator(),
),
),
)
],
);
}
}

Please follow this (modal_progress_hud)
import 'package:modal_progress_hud/modal_progress_hud.dart';
......
bool _saving = false
....
#override
Widget build(BuildContext context) {
return Scaffold(
body: ModalProgressHUD(child: Container(
Form(...)
), inAsyncCall: _saving),
);
}

Related

FutureBuilder gives an unexpected result and slowing UI down

It is a first statefull widget
bool _isPressed = false;
...
ElevatedButton(
child: const Text('Run long calculations'),
onPressed: () {
setState(() {
_isPressed = !_isPressed;
});
},
),
_isPressed ? const Result() : Container(),
...
and Result widget with its builds function returns
FutureBuilder<String>(
future: _process(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: LinearProgressIndicator(),
);
} else {
if (snapshot.error != null) {
return const Center(
child: Text('An error occurred'),
);
} else {
return Text('${snapshot.data}');
}
}
},
);
Future<String> _process() async {
await argon2.hashPasswordString('dummy text', salt: Salt.newSalt()); // long calculations
return 'dummy result';
}
Why the FutureBuilder does not render LinearProgressIndicator before it render final text? Actualy, the LinearProgressIndicator is rendered for a very small amount of time before final text rendered, but there is something wrong with it, because the circular indicator should spin much longer.
_process() seems to slow down the application and that's why the progress indicator does not spin. But how can it be if the result of the computation is Future and the code awaits for it...
I think its better to change your conditions like below .
based on flutter Doc
if (snapshot.hasData) {
// data
return Text('${snapshot.data}');
} else if (snapshot.hasError) {
// error
} else {
// CircularProgressIndicator
return SizedBox(
child: CircularProgressIndicator(),
width: 60,
height: 60,
);
}
If this answer does not help you and you think have a UI freeze because of heavy task in _process() method you should do the process task in separate Isolate.
Your code is fine, if you replace the _getHash body with just a Future.delayed() the progress indicator shows fine. Hence the problem is in hashPasswordString. If you look at the implementation of this function you'll notice that in fact it is synchronous.
So the quick fix would be - create a static function like that:
static String _calculateHash(String input) {
final result = argon2.hashPasswordStringSync(input,
salt: Salt.newSalt(), iterations: 256, parallelism: 8);
return result.hexString;
}
and use it with the compute function:
Future<String> _hash() {
return compute(_calculateHash, 'input text');
// this is not needed anymore
// DArgon2Result result = await argon2.hashPasswordString('input text',
// salt: Salt.newSalt(), iterations: 256, parallelism: 8);
// return result.hexString;
}
static String _calculateHash(String input) {
final result = argon2.hashPasswordStringSync(input,
salt: Salt.newSalt(), iterations: 256, parallelism: 8);
return result.hexString;
}
The long and proper fix - create a PR for the dargon2_flutter package.
The problem is with this line:
if (snapshot.connectionState == ConnectionState.waiting)
You see, ConnectionState.waiting is used when there is no connection yet, for example when a stream has no value.
Here is what each connection state is:
Active
after an asyncronous computation started, but before it ends.
None
When there is no asyncronous computation at all (for example, the future is None on a future builder)
Done
After the asyncronous computation has ended
Waiting
Before the asynchronous computation begins
So when you check if the connection state is waiting, the value is true for a split second and then the connection state switches to active, here is what your if statement should look like:
if (snapshot.connectionState == ConnectionState.active)
MohandeRr has suggested the impmentation flutter docs has used, but i usually do it like this
if (snapshot.connectionState != ConnectionState.done) {
return const Center(
child: LinearProgressIndicator(),
);
}
if (snapshot.hasError) {
return const Center(
child: Text('An error occurred'),
);
}
return Text('${snapshot.data}');
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool _isPressed = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Demo"),
actions: [
TextButton(
child: const Text(
'Press',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
setState(() {
_isPressed = !_isPressed;
});
},
)
],
),
body: _isPressed
? FutureBuilder<String>(
future: process(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Center(
child: Text(snapshot.data ?? ""),
);
} else if (snapshot.hasError) {
return const Center(
child: Text('An error occurred'),
);
} else {
return const Center(
child: LinearProgressIndicator(),
);
}
},
)
: const Center(
child: Text("Hidden"),
),
);
}
Future<String> process() async {
await Future.delayed(const Duration(seconds: 3));
return "Hello World";
}
}
There are 2 problems at play here:
You are creating a new Future (_process()) for every build loop. You need to put that in your state and reuse/clear it appropriately.
You are using the wrong ConnectionState check - snapshot.connectionState != ConnectionState.done is probably what you need

Flutter: async await

In the profile page of my app, I want to save a future list of objects from a firebase collection to a variable (myRecipes), using an async/await function. Depending on the outcome list, I want to display different widgets (using ifHasRecipes()) - if the list turns out to be null or empty, I want to display a text widget, otherwise I want to display the objects in the list using a listview builder (FavoritesHomePage class).
class Profile extends StatefulWidget {
final FirebaseAuth _auth = FirebaseAuth.instance;
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
List<Recipe> myRecipes;
Future<List<Recipe>> getUserRecipes(UserData userData) async {
return myRecipes = await DatabaseService().findUserRecipes(userData.uid);
}
Widget ifHasRecipes() {
if (myRecipes != null && myRecipes != []) {
return FavoritesHomePage(
recipes: myRecipes, scrollDirection: Axis.vertical, title: 'Your recipes',);
} else {
return Text('You have no favorites yet');
}
}
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UserData>(
stream: DatabaseService(uid: user.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
UserData userData = snapshot.data;
getUserRecipes(userData);
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
//widgets using userData
ifHasRecipes(),
],
),
),
);
} else {
return Scaffold(
body: Center(
child: SpinKitRipple(),),
);
}
});
}
}
How can I make this code synchronized? I want to run the getUserRecipes() and when it's done, return different widgets depending on the outcome.
If I do a hot reload, the code "works" as I want it to, but sometimes when I navigate to this profile page via my pageview widget, the async/await function returning the variable myRecipes isn't done before the ifHasRecipes() is built, and then myRecipes is null (even though it shouldn't be)... Hope this isn't too confusing, sorry.
In this case you can use a FutureBuilder, with this one you will have different states, just like the StreamBuilder, and you can show different widgets depending on the state, until the Future is resolved and you have the data.
I've done a little bit of refactoring to your code to make it work with the FutureBuilder, also I've changed it to Stateless, in this case it will display a CircularProgressIndicator until the Future is resolved, it will also handle errors and the lack of data.
class Profile extends StatelessWidget {
const Profile({Key key}) : super(key: key);
Future<List<Recipe>> getUserRecipes(UserData userData) async {
return await DatabaseService().findUserRecipes(userData.uid);
}
Widget ifHasRecipes(List<Recipe> myRecipes) {
if (myRecipes != null && myRecipes != []) {
return FavoritesHomePage(
recipes: myRecipes,
scrollDirection: Axis.vertical,
title: 'Your recipes',
);
} else {
return Text('You have no favorites yet');
}
}
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UserData>(
stream: DatabaseService(uid: user.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Scaffold(
body: SafeArea(
child: FutureBuilder(
future: getUserRecipes(snapshot.data),
builder: (context, futureSnapshot) {
if (futureSnapshot.hasError)
return Text('Error: ${futureSnapshot.error}');
switch (futureSnapshot.connectionState) {
case ConnectionState.none:
return Center(child: CircularProgressIndicator());
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.active:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:{
if (futureSnapshot.hasData) {
List<Recipe> myRecipes = futureSnapshot.data;
return Column(
children: <Widget>[
//widgets using userData
ifHasRecipes(myRecipes),
],
);
}
return Text('There\'s no available data.');
}
}
return null;
},
),
),
);
} else {
return Scaffold(
body: Center(
child: SpinKitRipple(),
),
);
}
},
);
}
}
If I understand the code correctly, the solution is to rebuild the widget when the future is resolved by adding setState((){}); to the getUserRecipes() method :
Future<void> getUserRecipes(UserData userData) async {
myRecipes = await DatabaseService().findUserRecipes(userData.uid);
setState((){});
}
(You don't have to return the value if you're assigning it to the state, but rather access it directly.)
By the way, you can use the ternary operator (or just regular conditions) to do conditional UI. Put this instead of ifHasRecipes(), :
(myRecipes != null && myRecipes != []) ?
FavoritesHomePage(
recipes: myRecipes, scrollDirection: Axis.vertical, title: 'Your recipes',)
: Text('You have no favorites yet')
If you get an error with this, increase your minimum SDK version to 2.6.0 in the pubspec.yaml

Auto navigation on login page in flutter app

My app needs to automatically initiate biometric login and navigate to a page based on outcome.
This is a common need and I followed advice similar to this solution
Code below
class _LoginPageState extends State<LoginPage> {
#override
Widget build(BuildContext context) {
final UserModel userModel = Provider.of<UserModel>(context);
return (userModel.biometricLoginEnabled && !userModel.isAuthenticated)
? _attemptBiometricAuthentication(context, userModel)
: _buildLoginForm(context, userModel);
}
Widget _attemptBiometricAuthentication(
BuildContext context, UserModel userModel) {
return FutureBuilder(
future: _initiateBiometricAuthentication(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) {
// make sure user is marked as authenticated
userModel.setAuthenticationWithoutNotification(true);
return HomePage(); // <-- WHOA!!
} else if (snapshot.hasData && snapshot.data == false) {
// we should have an updated error from _initiateBiometricAuthentication
return _buildLoginForm(context, userModel);
} else if (snapshot.hasError) {
return _buildLoginForm(context, userModel);
} else {
// we're waiting
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height,
minHeight: MediaQuery.of(context).size.height,
),
alignment: Alignment.center,
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Image(
image: AssetImage('images/Logo.png'),
fit: BoxFit.fitWidth,
),
CircularProgressIndicator(),
],
),
),
);
}
},
);
}
}
The problem is with the line to return HomePage() if authentication succeeds.
If there is a call to setState() and a rebuild occurs I have the HomePage being rebuilt inside LoginPage. Routing is also a little messed up because the app thinks it's on route /login but its actually on /home.
I feel like I'm missing something entirely in triggering routing automatically.
You need to listen result from the Future method and navigate to other Page. (never do it inside build Widget).
Demo:
Code example:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class FutureNavigation extends StatefulWidget {
#override
_FutureNavigationState createState() => _FutureNavigationState();
}
class _FutureNavigationState extends State<FutureNavigation> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo Future Navigator"),
),
body: buildBody(context),
);
}
Widget buildBody(BuildContext context) {
return FutureBuilder(
future: _login(),
builder: (context, snapshot) {
return Center(child: CircularProgressIndicator());
},
);
}
Future<String> _login() async {
await Future.delayed(Duration(seconds: 3)).then((value) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (BuildContext context) {
return HomePage();
},
),
);
});
return "Logined";
}
}

Button pressed return a future builder

I have a button and if pressed should return a future builder here is my code.
I already search some examples on the web but no luck, Im new in flutter development and trying to create a simple login with api call.
Future<AccessToken>fetchAccessToken() async{final token = await _repository.fetchToKen();
>>return token;
}
onPressed: () {FutureBuilder<AccessToken>(future:bloc.fetchAccessToken(),builder: (context, snapshot) {if (snapshot.hasError) {return Text('Error');} else if (snapshot.hasData) {return Text('data');} else {return `Center`(child: CircularProgressIndicator(),);}},);}
I want to show a progress indicator while waiting for the api response, but after I receive the response, my builder inside the future builder is not called.
You can't simply return a widget and place it in the widget tree like that. Maybe you can use conditional list for hiding and showing the FutureBuilder widget.
import 'package:flutter/material.dart';
class ApiWidget extends StatefulWidget {
#override
_ApiWidgetState createState() => _ApiWidgetState();
}
class _ApiWidgetState extends State<ApiWidget> {
Repository _repository = Repository();
Future<AccessToken> accessTokenFuture;
bool isButtonPressed = false;
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
FloatingActionButton(onPressed: () {
setState(() {
try {
isButtonPressed = true;
accessTokenFuture = fetchAccessToken();
} catch (_) {
print('Fetch error');
}
});
}, child: Icon(Icons.add),),
if(isButtonPressed)
FutureBuilder<AccessToken>(
future: bloc.fetchAccessToken(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Text('Error');
}
Column(
children: <Widget>[Text(snapshot.data)],
);
},
),
],);
}
}
You can do something like that:
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
FloatingActionButton(onPressed: () {
setState(() {
try {
isLoading = true;
accessTokenFuture = await fetchAccessToken();
isLoading = false;
} catch (_) {
isLoading = false;
print('Fetch error');
}
});
}, child: Icon(Icons.add),),
_buildAsyncInfo(),
],);
}
Widget _buildAsyncInfo() {
return isLoading ?
CircularProgressIndicator() :
Column(
children: <Widget>[Text(snapshot.data)],
);
}

Displaying Snackbar inside a SearchDelegate

I am using a SearchDelegate and want to display a Snackbar when the user tries to perform a search with an empty query. I've tried returning Scaffold widgets from both the buildSuggestions and buildResults methods and then using a Builder / GlobalKey inside the buildResults method to display a message to the user if the search query has a length of zero. However this leads to the Scaffold's state being updated during the render method which throws an exception. Has anyone dealt with a similar challenge? Seems like a common use case that you would want to display a Snackbar inside your search delegate, yet I can't seem to fathom an easy way to do it.
Figured it out
class DataSearch extends SearchDelegate<String> {
List<Drug> drugList = new List<Drug>();
DataSearch(Future<List<Drug>> listDrugName) {
this.drugListFuture = listDrugName;
}
#override
List<Widget> buildActions(BuildContext context) {
// actions for app bar
return [
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
query = "";
})
];
}
#override
Widget buildLeading(BuildContext context) {
// leading icon on the left of app bar
return IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow, progress: transitionAnimation),
onPressed: () {
close(context, null);
});
}
#override
Widget buildResults(BuildContext context) {
// show result from selection
return null;
}
#override
Widget buildSuggestions(BuildContext context) {
return new FutureBuilder(
future: db.getDrugEntries(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData || snapshot.data.length < 1) {
return new Center(
child: new LoadingIndicator(Constants.msgLoading));
} else {
drugList = snapshot.data;
// show when user searches for something
final suggestionList = query.isEmpty
? drugList
: drugList
.where((r) =>
(r.drugId.toLowerCase())
.contains(query.toLowerCase()) ||
(r.fullDrugName.toLowerCase())
.contains(query.toLowerCase()) ||
(r.otherName.toLowerCase())
.contains(query.toLowerCase()) ||
(r.tradeName.toLowerCase())
.contains(query.toLowerCase()))
.toList();
return ListView.builder(
itemBuilder: (context, index) {
String drugName = suggestionList[index].genericName;
String drugId = suggestionList[index].drugId;
int queryIndex = drugName.indexOf(query);
if (queryIndex == -1) {
queryIndex = 0;
}
int queryIndexEnd = queryIndex + query.length;
return Container(button//...onTap:_launchExtraContent(context,drugId);
},
itemCount: suggestionList.length,
);
}
});
}
_
_launchExtraContent(BuildContext context, StringtheFileName) async {
try {
//......
} catch (e) {
_showSnackBar(context,'ERROR: Unable to retrieve file please submit a bug report');
}
}
void _showSnackBar(BuildContext context, String text) {
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text(
text,
textAlign: TextAlign.center,
),
backgroundColor: Colors.red,
));
}
}