Flutter MaterialApp home based on futureBuilder - flutter

When my app starts up, it detects whether the user is logged in or not (with firebase) and based on this check, it shows the homepage or the login page. Up to now everything is fine, but I would like to add one more layer.
The user can login as normal user or as admin.
So the check should be not only on the authentication state, but also on the "level" of the user, and show different pages, based on the user level.
I get the user level with a query on the firestore database, so it's a Future.
This is the code i'm using:
final usersCollection = FirebaseFirestore.instance.collection('users');
User loggedUser = FirebaseAuth.instance.currentUser;
Future<InfoUtente> userInfo;
String livelloLogin;
// here I get the user from the firestore database, based on the authenticated user id
Future<InfoUtente> fetchInfoUtente() async {
final response = await usersCollection
.where(
'uid',
isEqualTo: loggedUser.uid,
)
.get();
return InfoUtente.fromFireStore(response.docs.first);
}
// here I return the page based on the user authentication "level"
Future<Widget> widgetChoice() async {
if (!isLogged)
return LoginNewPage();
else {
userInfo.then(
(value) {
livelloLogin = value.loginLevel;
if (livelloLogin == 'struttura')
return StrutturaPage();
else
return MainPage();
},
);
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// the homepage of the material app is a future builder
home: FutureBuilder(
future: widgetChoice(),
builder: (BuildContext context, AsyncSnapshot<Widget> widget) {
if (!widget.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return widget.data;
},
),
);
}
something is not right because it always shows the circular progress indicator.
What am I doing wrong?
Is this the correct way of doing this or am I completely wrong?

If there is no data fetched or found, your screen will stuck on loading infinitely. Update your builder's implementation as
builder: (BuildContext context, AsyncSnapshot<Widget> widget) {
if(widget.connectionState == ConnectionState.done){
if (!widget.hasData) {
return Center(
child: Text('No Data exists')
);
}
return widget.data;
}
return Center(
child: CircularProgressIndicator(),
);
},
And update your widgetChoice as
Future<Widget> widgetChoice() async {
if (!isLogged)
return LoginNewPage();
else {
var userInfo = await fetchInfoUtente();
livelloLogin = userInfo.loginLevel;
if (livelloLogin == 'struttura')
return StrutturaPage();
else
return MainPage();
}
}

If i'm right you have to call the future function like that:
FutureBuilder(
future: widgetChoice,
Without ()

Related

Flutter setState vs snapshot

i would like to know whats the diffrence between using setState and snapshot when fetching apis for example
the way i fetch the apis is like the following
Widget text = Container;
Future<AnyClass> fetch() async{
final response = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
var result = AnyClass.fromJson(jsonDecode(response.body));
setState(()=> text = result.title)
}
#override
Widget build(BuildContext context) {
return Contianer(child:text)
}
there is another way which uses the snapshot to featch the data instead of using state like the following
Future<Album> fetchAlbum() async { final response = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
return Album.fromJson(jsonDecode(response.body));
} #override void initState() {
super.initState();
futureAlbum = fetchAlbum(); } #override Widget build(BuildContext context) {
return MaterialApp(
title: 'Fetch Data Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Fetch Data Example'),
),
body: Center(
child: FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
),
),
),
); }
so i would like to know what are the diffrence between these two methods. Thanks
None, you can check the FutureBuilder source code to see that is does exactly the same: call setState when it has a response.
The advantage of FutureBuilder are:
Easy handling of the different state (loading, loaded, failure)
You don't have to create a StatefulWidget, which means less line of code

Why does my async method run twice in Flutter?

I want to load a static list data when entering indexScreen,but the list sometimes show twice the same content,sometimes not.
This is my list setting:List<ListClass> listItems=List<ListClass>();,ListClass is a simple class with on different attributes and a constructor.
I use home:IndexScreen() in main.dart to show Index page.
return MaterialApp(
home: IndexScreen(),
debugShowCheckedModeBanner: false,
onGenerateRoute: router.generator,
builder: EasyLoading.init(),
);
And before this page build,it will update listItems using:
Future<bool> initUserAndIndex() async{
if (curUserEmail==null) sharedGetData(USER_EMAIL).then((value) => curUserEmail=value.toString());
print(curUserEmail);
await UserTable().getUserInfo(curUserEmail).then((value){print("user ok");});
await CollectionTable().getIndexList().then((value){print("Collection ok");return true;});
return null;
}
buildPage:
#override
Widget build(BuildContext context) {
return FutureBuilder<Object>(
future: initUserAndIndex(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState==ConnectionState.waiting)
{
EasyLoading.show(status: 'loading...');
// avoid no return,this cause a whiteborad transition,I don't know how to solve it,too.
return Container();
}
else
{
EasyLoading.dismiss();
return SafeArea(
child: Scaffold(
// the listItems is used in Body()
body: Body(),
),
);
}
},
);
}
}
I run this app,and it prints twice user ok and Collection ok.But when I use ROUTER.NAVIGETE,it only prints once.
User Information is OK,but the list is such a great problem--the page shows twice content
I put my code at an order of relevance of this prblom,I think.Next I put my the two awaited funtion here:
User:
Future<bool> getUserInfo(String userEmail) async{
await userCollection.where({'userEmail':userEmail}).get().then((res) async {
//assign to the static variables
return true;
});
return null;
}
Collection:
Future<bool> getIndexList() async {
listItems.clear();
await listCollection.get().then((value){
var v = value.data;
for (var data in v) {
//get data and package them,add after the listItems list.
listItems.add(ListClass(header, content, userId, favorCount, wordCount));
}
return true;
});
}
You probably want to assign your future in your widget class, but not in the build method as the documentation show, otherwise, everytime your build method is triggered, it will call again your FutureBuilder.
final Future<String> _calculation = Future<String>.delayed(
const Duration(seconds: 2),
() => 'Data Loaded',
);
#override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: Theme.of(context).textTheme.headline2!,
textAlign: TextAlign.center,
child: FutureBuilder<String>(
future: _calculation, // a previously-obtained Future<String> or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
// ...
}
),
),
}

changing initial route according to get request

I have an app that starts in the login page as initial route. I want to check if the users' token is still valid, and if it is, I want to go directly to another route instead of the login page.
I have all the server logic and am making a get request that returns a boolean to say if the user session(token) is valid.
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
var initRoute;
if (_isTokenValid() as bool){
initRoute = '/login';
}else{
initRoute = '/main';
}
return GestureDetector(
onTap: () {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus &&
currentFocus.focusedChild != null) {
FocusManager.instance.primaryFocus.unfocus();
}
},
child: MaterialApp(
title: 'Mappin',
theme: Themes.lightAppTheme,
darkTheme: Themes.darkAppTheme,
initialRoute: initRoute,
routes: {
'/main': (context) => NavBar(),
'/profile': (context) => ProfilePage(),
'/settings': (context) => SettingsPage(),
'/messageDetail': (context) => MessageDetail(),
'/login': (context) => LoginPage(),
},
),
);
}
Future<bool> _isTokenValid() async {
// in this function we can pass the controlers directly bcs they are declared in the class. In login they are declared inside each widget (login/signup)
final isValid = await APIService().isAccessValid();
if (!isValid){
return await APIService().isRefreshValid();
}
return isValid;
}
}
The Problem with this code is that casting Future<bool> to bool is invalid. In other pages I would set the state of a variable, but since app is a stateless widget, this is not possible.
What is a good way of receiving a boolean from the GET request (async function) and changing the initial route according to its value?
What is being received from the APIService? That might be why you are getting that error.
Try adding a toggle breakpoint on
Future<bool> _isTokenValid() async {
To see where the problems from.
FutureBuilder can help you to get a callback
FutureBuilder(
// get the Provider, and call the _isTokenValid method
future: _isTokenValid(),
// wait for the future to resolve and render the appropriate
// widget for HomePage or LoginPage
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return snapshot.hasData ? NavBar() : LoginPage();
} else {
return CircularProgressIndicator();
}
},
),

Strange FirebaseAuth.instance.onAuthStateChanged is not updating streambuilder

I am using firebase authetication in flutter application.When user signup or login i can see that FirebaseAuth.instance.onAuthStateChanged is called
but Streambuilder is not updating the widget. I also noticed that sometime FirebaseAuth.instance.onAuthStateChanged is not even called after user login. But when i reload the screen or rerun the app i can see that user is logged in. Below is my streambuilder code.
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (ctx, userSnapshot) {
if (userSnapshot.hasData) {
print(userSnapshot.data);
print('data changed');
return FutureBuilder(
future: Future.delayed(Duration(seconds: 1)),
builder: (ctx, asyncdata) {
if (asyncdata.connectionState == ConnectionState.done) {
print('user has data');
return UserList();
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
},
);
} else {
print('load auth screen');
return AuthScreen();
}
},
);
Probably you do signIn with a FirebaseAuth that you don't assign in onAuthStateChanged, so you should do like this:
_auth = FirebaseAuth.instance;
///
StreamBuilder<FirebaseUser>(
stream: _auth.onAuthStateChanged,
///
signIn() async {
var result = await _auth.signInWithCredential(credential);
}

FutureBuilder in flutter building app again and again?

what I am trying to do is check if user is logged in already or not. there is a futurebuilder inside consumer which is notifying listeners. check code:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: Auth(),
)
],
child: Consumer<Auth>(
builder: (ctx, auth, _) {
print('making it again and again');
return MaterialApp(
title: 'MY app',
theme: ThemeData(
primarySwatch: Colors.blue,
accentColor: Colors.deepOrange,
),
home: auth.isAuth
? FirstScreen()
: FutureBuilder(
future: auth.tryAutoLogin(), //inside this function there is notifylisteners()
builder: (ctx, authResultSnapShot) =>
authResultSnapShot.connectionState ==
ConnectionState.waiting
? SplashScreen()
: AuthScreen(),
));
},
),
);
}
What I get:
this is rebuilding the app again and again. tryautologin is called repeatedly.
What I want:
check for is a user logged in only once the app is started.
Future<bool> tryAutoLogin() async {
final prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey('userData')) {
return false;
}
final extractedUserData = json.decode(prefs.getString('userData'));
final expiryDate = DateTime.parse(extractedUserData['expiryDate']);
_accessToken = extractedUserData['accessToken'];
_refreshToken = extractedUserData['refreshToken'];
print(extractedUserData['userId']);
_userId = extractedUserData['userId'];
if (expiryDate.isBefore(DateTime.now())) { //for now this if statement is not running
try {
await _getAccessToken();
} catch (error) {
return false;
}
}
print('tryautologin');
notifyListeners();
_autoGetAccessToken(); //this function doesn't have any notifylisteners
return true;
}
edited:
String get accessToken {
if (_expiryDate != null &&
_expiryDate.isAfter(DateTime.now()) &&
_accessToken != null) {
return _accessToken;
}
return null;
}
bool get isAuth {
print(accessToken != null);
return accessToken != null;
}
Even the question is old quite a bit of time, this answer will help another. I have faced the problem also. this happens when you use a future builder inside a consumer. it is rebuilding again and again infinitely. So, instead of the future builder try using an if-else statement.
child: Consumer<Auth>(
builder: (ctx, auth, _) {
//Sizedbox() used for nullsafety
Widget child = SizedBox();
if (auth.isAuth) {
child = FirstScreen();
} else if (!auth.triedAutoLogin) {
auth.tryAutoLogin();
child = SplashScreen();
} else {
child = AuthScreen();
}
return MaterialApp(
title: 'MY app',
theme: ThemeData(
primarySwatch: Colors.blue,
accentColor: Colors.deepOrange,
),
home: child,
);
},
),
inside Auth class keep a boolean variable like triedAutoLogin to keep track.