Displaying multiple screens based on ternary condition in dart - flutter

I have a scenario where I am showing a spinner when a page loads and while it's loading, it fetches some data in DB and sets a bool value to either true or false based on data availability.
I then want to share either screen A or B based on the boolean result.
I have done the following in my code but the app keeps showing the spinner. Any ideas what I might be doing incorrectly?
return _isLoading?
Center(child:Loading(),):
_isPersonalInfoSubmitted?ScreenA():ScreenB();
Second Attempt (Using Future Builder)
I want to show categories if the loggedin user is an admin. Else, for the rest of the users, I want to fetch address of the user from the DB. If the address is null, show Personal Details Screen else show Categories.
return FutureBuilder (
future: userId=='ADMIN_ID'?
Provider.of<Categories>(context,listen:false).fetchAndReturnCategories():
Provider.of<Addresses>(context,listen: false).fetchAndReturnAddress(userId)!=null?
Provider.of<Categories>(context,listen:false).fetchAndReturnCategories():null,
builder: (context, snap) {
inspect(snap);
if (snap.hasData) {
var categoriesData = Provider.of<Categories>(context);
return snap.hasData?
Scaffold(...) : PersonalDetails();
What happens here is that the method fetchAndReturnCategories gets executed even if the userID is not admin id. Do I have the correct setup?

prefer to use FutureBuilder
FutureBuilder<SomeClass>(
future: fetchdatFuture,
builder: (ctx, snap) {
if (snap.hasData) {
return snap.data?ScreenA():ScreenB();
} else if (snap.connectionState == ConnectionState.waiting)
{
return Center(child:Loading());
}
return Text("Error");
},
)

I think you have missed the setState to rebuild your widget
var bool _isLoading = true;
return _isLoading?
Center(child:Loading(),):
_isPersonalInfoSubmitted?ScreenA():ScreenB();
void _apiCall() {
// After Success of API Call
setState((){
_isLoading = false;
})
}

Related

Flutter, How handle state when awaiting for firebase data

checkBorrowerAlreadyExists() Function checks if a user input name already exists in firebase data and returns a bool. I am using this function in an if condition after onPressed of a button, I am providing user input as an argument to the function in if condition and display the snackbar based on the bool value. This works perfectly fine when firebase already has data, But initially when the user register and opens the app, firebase won't have any data to check if name already exists and nothing happens on the screen.
I used else condition to return false if no data, move on and add input in firebase, but didn't work. I also tried using the bool in different way but didn't work. How do I solve this.
checkBorrowerAlreadyExists(String textfieldname) async {
await for (var snapshots in _firestorem
.collection('lender')
.doc(auth.currentUser?.uid)
.collection('borrowers')
.snapshots()) {
for (var message in snapshots.docs) {
bool isThere;
if (message.data().containsValue(textfieldname)) {
return isThere = true;
} else {
return isThere = false;
}
}
}
}
onPressed: () async {
if (borrowerNameController.text.isEmpty) {
Get.snackbar(
'Error:', "Borrower Name can't be empty",
backgroundColor: Colors.red);
} else if (await checkBorrowerAlreadyExists(
borrowerNameController.text)) {
Get.snackbar('Error',
'Borrower Name alread exists, Enter new name',
backgroundColor: Colors.red);
} else{ upload data in firebase}
You can't use await in the build method, as there's no way to hold up rendering.
The solution is actually in the return type of your function, which you left implicit not. If you make the return type explicit, it is:
Future<bool> checkBorrowerAlreadyExists(String textfieldname) async {
...
Since this returns a Future, you need to use a FutureBuilder in your build method to render the boolean value (and to determine what to render until that value is available).
So something like:
onPressed: () async {
if (borrowerNameController.text.isEmpty) {
Get.snackbar('Error:', "Borrower Name can't be empty", backgroundColor: Colors.red);
}
else {
return FutureBuilder<String>(
future: checkBorrowerAlreadyExists(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
List<Widget> children;
if (snapshot.hasData) {
return Text(borrowerNameController.text)
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}'),
} else {
return Text('Awaiting result...'),
}
},
),
I recommend checking out the documentation on FutureBuilder and Flutter's codelab on asynchronous programming.

Flutter null check operator used on a null value flutter

I am trying to check if user make a payment or not.If payment is done, user will see homepage.If payment is not done User will see the payment page. The problem is that I am getting null check operator used on a null value. What I am doing wrong?
class TwoPage extends StatelessWidget {
Package? offer;
PurchaserInfo? _purchaserInfo;
bool? payment;
Future<bool> ispaymentdone() async {
await Purchases.setDebugLogsEnabled(true);
await Purchases.setup("public_key");
PurchaserInfo purchaserInfo = await Purchases.getPurchaserInfo();
print(purchaserInfo);
print("buraya kadar iyi");
Offerings offerings = await Purchases.getOfferings();
print(offerings);
// optional error handling
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
//if (!mounted) return;
_purchaserInfo = purchaserInfo;
if(purchaserInfo.entitlements.all["content-usage"]!=null){
if ( purchaserInfo.entitlements.all["content-usage"]!.isActive) {
print("trueee");
return true;
}
}
return false;
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: ispaymentdone(),
builder: (context, snapshot) {
if (snapshot.data == null)
return SizedBox(
child: Center(child: CircularProgressIndicator(color:Colors.purple)),
height: 10.0,
width: 10.0,
);
else if (snapshot.data == true)
return NewHomeScreen();
else
return MainPayment(purchaserInfo: _purchaserInfo, offer: offer);
},
);
}
}
This is because u r using null check operator on your variables but the value of that particular variable is NULL at a point when u want to use it ,either u have to give some initial value to that variable or make sure value is not null.
It is my mistake:
return MainPayment(purchaserInfo: _purchaserInfo, offer: offer);
I didn't assign anything to offer variable.

How to perform async calls inside streambuilder?

I have an flutter app, which uses FirebaseAuth for authentication and Firestore for storing data. To store user profile (name, photo etc), I've created a separate collection in my firestore database.
So, once a user registers he is redirected to a screen where he can add his profile data. This data is again stored as a document.
I want to implement the same checks when the app is starting:
So, I display a splash screen and in the backend it checks,
Is the user logged in? If yes, proceed, else redirect him to the terms and conditions page.
Does the collection have user profile? If yes, proceed, else redirect him to a page where he can add his photo etc.
I was able to accomplish point 1, but I am not able to do the 2nd check.
Here's the code:
class SplashScreen extends StatelessWidget {
static final String id = 'splash_screen';
final Firestore _firestore = Firestore.instance;
#override
Widget build(BuildContext context) {
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
FirebaseUser user = snapshot.data;
if (user == null) {
return TermsAndConditions();
}
// Check if user profile has been created
return ChatsScreen();
} else {
return SplashScreenContent();
}
},
);
}
}
The commented line is where the code for checking user profile should go.
I tried the following:
_firestore.collection('users').where('id', isEqualTo: user.uid).snapshots().first.then( (value) {
if(value.documents.isEmpty) {
return ProfileScreen();
}
});
As I understand I cannot return the value to outer function from the callback. How can I achieve this
What you want to do is to show splash screen every time.
Then, inside splash screen, you can create the check.
call this function inside initState
void bootstrap() async {
var user = await FirebaseAuth.instance.currentUser();
if (user == null) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => TermsAndConditions()));
return;
}
DocumentSnapshot userDoc =
await Firestore.instance.collection("users").document(user.uid).get();
if (!userDoc.exists) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => ProfilePage()));
return;
}
Navigator.push(
context, MaterialPageRoute(builder: (context) => ChatsScreen()));
}
You can also add initial 2-3 seconds delay, else the splash screen can be too abruptly changed.
You can find many splash screen libraries in pub.dev that allow you to do this bootstraping.

How to inform FutureBuilder that database was updated?

I have a group profile page, where a user can change the description of a group. He clicks on the description, gets on a new screen and saves it to Firestore. He then get's back via Navigator.pop(context) to the group profile page which lists all elements via FutureBuilder.
First, I had the database request for my FutureBuilder inside the main build method (directly inside future builder 'future: request') which was working but I learnt it is wrong. But now I have to wait for a rebuild to see changes. How do I tell FutureBuilder that there is a data update?
I am loading Firestore data as follows within the group profile page:
Future<DocumentSnapshot> _future;
#override
void initState() {
super.initState();
_getFiretoreData();
}
Future<void> _getFiretoreData() async{
setState(() {
this._future = Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();});
}
The FutureBuilder is inside the main build method and gets the 'already loaded' future like this:
FutureBuilder(future: _future, ...)
Now I would like to tell him: a change happened to _future, please rebuild ;-).
Ok, I managed it like this (which took me only a few lines of code). Leave the code as it is and get a true callback from the navigator to know that there was a change on the second page:
// check if second page callback is true
bool _changed = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProfileUpdate(userId: globals.userId.toString())),
);
// if it's true, reload future data
_changed ? _getFiretoreData() : Container();
On the second page give the save button a Navigator.pop(context, true).
i would advice you not to use future builder in this situation and use future.then() in an async function and after you get your data update the build without using future builder..!
Future getData() async {
//here you can call the function and handle the output(return value) as result
getFiretoreData().then((result) {
// print(result);
setState(() {
//handle your result here.
//update build here.
});
});
}
How about this?
#override
Widget build(BuildContext context) {
if (_future == null) {
// show loading indicator while waiting for data
return Center(child: CircularProgressIndicator());
} else {
return YourWidget();
}
}
You do not need to set any state. You just need to return your collection of users in your GetFirestoreData method.
Future<TypeYouReturning> _getFirestoreData() async{
return Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();
}
Inside your FutureBuilder widget you can set it up something like Theo recommended, I would do something like this
return FutureBuilder(
future: _getFirestoreData(),
builder: (context, AsyncSnapshot<TypeYouReturning> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
if (snapshot.data.length == 0)
return Text("No available data just yet");
return Container();//This should be the desire widget you want the user to see
}
},
);
Why don't you use Stream builder instead of Future builder?
StreamBuilder(stream: _future, ...)
You can change the variable name to _stream for clarity.

Whyn StreamBuilder's hasData property does no set to True if Observable emmits an event?

I need to implement search and display results. I use Observables from RxDart.
abstract class SearchState {}
class SearchCompleted extends SearchState {}
class SearchEmpty extends SearchState {}
final _searchSubject = PublishSubject<String>();
Observable<SearchState> get result {
return _searchSubject.switchMap((term) {
return _search(term); // returns Observable<SearchCompleted>
}).switchIfEmpty(Observable.just(SearchEmpty()));
}
Somewhere in StreamBuilder is use this result as source for stream. And at opening screen (no search) I expect that
snapshot.hasData = true
because my observable emmits SearchEmpty but I get false. What I did wrong? All that I need is just display some message if search result is empty.
UPD: After additional investigation of StreamBuilder, reading RxDart docs and systemationzation of information from pskink I came to conclution that I was mistaken. switchIfEmpty() means that stream is switched to fallback only if original stream returns nothing (after placing value to sink). I need use startWith() which forces the observable to emmit required start value. So the correct code is
Observable<SearchState> get result {
return _searchSubject.switchMap((term) {
return _search(term); // returns Observable<SearchCompleted>
}).startWith(SearchEmpty());
}
UPD2: At first build of widget StreamBuilder's snapshop.hasData = false, even using startWith() because connectionStatus = ConnectionStatus.waiting (i.e. when stream is preparing to receive data). To avoid this you must set value for initialData property. For example:
StreamBuilder(
initialData: SearchEmpty(),
stream: result,
builder: ...
)
Or you can return some widget while connection is in waiting status. For example:
StreamBuilder(
stream: result,
builder: (context, snapshot) {
// this allow to skip using `initialData`
if (snapshot.connectionStatus == ConnectionStatus.waiting) {
return Center(child: CircularProgressIndicator());
}
// Process data
if (snapshot.hasData) {
if (snapshot.data is SearchEmpty()) { return Text('no items');}
if (snapshot.data is SearchCompleted()) { return ListView(...);}
}
}
),
I've just had something like that problem at the moment, I worked around it getting the DataSnapshot, which returns null if there's no key/value.
if (snapshot.hasData && snapshot.data.snapshot.value != null) {
return //your code
}
else if(snapshot.hasData && snapshot.data.snapshot.value == null){
return //no items found
}
else{
return //your code
}
This way I could manage the StreamBuilder when it has no data.
This would work aswell:
if(snapshot.hasData){
if(snapshot.data.snapshot.value == null){
return //no data found
}
else{
return //data
}
}