Flutter Dart "unconditionally accessed because the receiver can be 'null'." problem - flutter

Here is my flutter code where I try to use FutureBuilder but encounter problems due to the null protection in Dart.
class AbcClass extends StatefulWidget {
#override
_AbcClassState createState() =>
_AbcClassState();
}
class _AbcClassState
extends State<AbcClass>
with AutomaticKeepAliveClientMixin {
_AbcClassState();
Future? _purchaserInfoSnapshot;
#override
void initState() {
_purchaserInfoSnapshot = setPurchaserInfo();
super.initState();
}
setPurchaserInfo() async {
PurchaserInfo purchaserInfo = await getPurchaserInfo();
Purchases.addPurchaserInfoUpdateListener((purchaserInfo) async {
if (this.mounted) {
setState(() {
_purchaserInfoSnapshot = Future.value(purchaserInfo);
});
}
});
return purchaserInfo;
}
#override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
future: _purchaserInfoSnapshot,
builder: (context, snapshot) {
if (!snapshot.hasData ||
snapshot.data == null ||
snapshot.connectionState != ConnectionState.done) {
return Center(
child: Text(
'Connecting...',
style: Theme.of(context).textTheme.headline3,
));
} else {
if (snapshot.data.entitlements.active.isNotEmpty) {
return Scaffold(...);
} else {
return MakePurchase();
}
}
});
}
}
The part that creates the problem is the following:
if (snapshot.data.entitlements.active.isNotEmpty)
And the error message:
The property 'entitlements' can't be unconditionally accessed because the receiver can be 'null'.
Try making the access conditional (using '?.') or adding a null check to the target ('!').
I tried updating it as the following:
else if (snapshot.data!.entitlements.active.isNotEmpty)
... but it did not help.
Any ideas on how I am supposed to deal with it?
Note: I did not paste the entire code as it involves a lot of opther logic that is not relevant to this question. I hope the pseudo code above will still help.

You've probably enabled null-safety in your project. Now the compiler won't let you compile code that it is not sure that won't throw an Null Exception error (like trying to access a property of a nullable variable without checking if its not null first)
You can either mark it with the bang ! operator and tell the compiler this variable will never be null, which may not be true, so be aware when using it.
Or you could check if the variable is not null before accessing its properties or trying to call methods on it.
Try this:
final active = snapshot.data?.entitlements?.active;
if (active != null && active.isNotEmpty)
I've also seen that you tried to check if snapshot.data was not null first. But remember that for the flow-analysis to work, you have to assign the variable you're trying to test to a local final variable.
Like:
final data = snapshot.data;
if (!snapshot.hasData || data == null || snapshot.connectionState != ConnectionState.done) {
return ...

Related

The argument type 'User?' can't be assigned to the parameter type 'Future<Object?>?'., when i can the code got this error

return FutureBuilder(
future: FirebaseAuth.instance.currentUser(), The function can't be unconditionally invoked because it can be 'null. Try adding a null check ('!').
...
This is the code where I got the error, I tried to add null check operator but could not work
You are getting this error because FirebaseAuth.instance.currentUser() is not a future function. It returns User?. Hence, it can't be used as a future inside FutureBuilder.
Here's a snippet from its source code:
In your code snippet, I can't figure out why you need this future builder. If you can, remove it completely since you are not using any of its properties. You can return the ListView.builder that you are using at line number 32 from line number 23.
I hope that helps!
First make variable like this
late<User?> Future user;
Then in side initState function do
#override
initState(){
super.initState();
user = FirebaseAuth.instance.currentUser();
}
then passed inside futureBuilder
like this
FutureBuilder<User?>(
future: user,
builder: (context,child , snapShot){
if(snapShot.data != null)
{
return YOUR_WIDGET ();
}
else
{
return ANY LOADING WIDGET():
}
});
note don't forget to but Data Type for futureBuilder like
futureBuilder<YOUR DATA TYPE>(...)
this for Future in general but you did'nt have to use future builder
instate use normal Widget but first get user inside initState
late user;
#override
initState(){
super.initState();
user = FirebaseAuth.instance.currentUser();
}
#override
Widget build(BuildContext context) {
if(user != null){
return YOUR_WIDGET
} else {
return ANY LOADING WIDGET():
}
}
FirebaseAuth.instance.currentUser() is a getter not a future.
How to go about it ?
Use variable of Type User? and then once it is not null show the ListView
User? user;
#override
void initState() {
super.initState();
setState((){
user = FirebaseAuth.instance.currentUser();
}
}
#override
Widget build(BuildContext context) {
return Container(
child: user ? <ListView here> : <CircularProgressIndicator here>
);
}

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.

Is there any easy way to use a Future (which performs an http connection) inside a stateful widget without having it reconnect on every screen build?

Every time the screen is rebuilt the getJSONfromTheSite seems to get invoked. Is seems because the future is placed inside the Widget build that every time I rebuild the screen it's just calling the apiResponse.getJSONfromTheSite('sitelist') future. But When I try to simply move the apiResponse.getJSONfromTheSite('sitelist') call outside the Widget and into the initState it doesn't work at all.
I'm not fully grasping the interplay of Futures in relation to a stateful widget, but in this case I need to keep the widget stateful because Im using a pull to refresh function to rebuild my state
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder(
future: _apiResponse.getJSONfromTheSite('sitelist'),
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
// (Do some UI stuff)
class RemoteDataSource {
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
return Result<AppData>.success(AppData.fromRawJson(response.body));
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
A FutureBuilder, as the name suggests, wants to build you something using a FUTURE value that you provide. For that to happen, you should perform an operation outside the build method (for example, in the State class or in the initState function) and store its Future value (like a promise in javascript), to be used later on the FutureBuilder.
You have access to this value inside the FutureBuilder on the snapshot.data variable, as I can see you already know by looking at your code. The way I coded the following solution, you should no longer have issues about multiple requests to the website each time it builds the widget UI (getJSONfromTheSite will only be called once and the result from this call will be available to you inside the FutureBuilder!)
The solution:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource(); // I left this here because I'm not sure if you use this value anywhere else (if you don't, simply delete this line)
// when creating the widget's state, perform the call to the site once and store the Future in a variable
Future<Result> _apiResponseState = RemoteDataSource().getJSONfromTheSite('sitelist');
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder<SuccessState>(
future: _apiResponseState,
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
EDIT: Edited answer to use Result as the inner type of the Future (instead of SuccessState).
The FutureBuilder's behavior can be expected as following according to the documentation
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies.
It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder.
If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
As stated above, if the future is created at the same time as the FutureBuilder, the FutureBuilder will rebuilt every time there's change from the parent. To avoid this change, as well as making the call from initState, one easy way is to use another Widget call StreamBuilder.
An example from your code:
class RemoteDataSource {
final controller = StreamController<AppData>();
void _apiResponse.getJSONfromTheSite('sitelist') {
// ... other lines
if (response.statusCode == 200) {
// Add the parsed data to the Stream
controller.add(AppData.fromRawJson(response.body));
}
// ... other lines
}
In your SiteListScreen:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
_apiResponse.getJSONfromTheSite('sitelist');
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: StreamBuilder<AppData>(
stream: _apiResponse.controller.stream, // Listen to the Stream using StreamBuilder
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
AppData sitelistCollection = snapshot.data;
}
},
),
);
}
This StreamBuilder is a popular concept through out most of Flutter's apps nowadays (and is the basis of many Flutter's architecture), so it's a good idea to take a good look and use the best of it.
There is a simple way you do not need to change too much coding. Like
class RemoteDataSource {
Result _result;
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
if (_result != null) {
return _result;
}
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
_result = Result<AppData>.success(AppData.fromRawJson(response.body));
return _result;
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
I only store the success result to _result, I do not sure that you want store the error result. When you rebuild the widget, it will check if it already get the success result. If true, return the stored result, it not, call api.

Problem using Flutter Provider when updating a value during build

I'm trying to update my uid in a provider just after checking whether a user is logged. When I do that, throws an error when building widgets even though the app does not crash. Here is the code:
class HandleAuth extends StatelessWidget {
#override
Widget build(BuildContext context) {
var user = Provider.of<FirebaseUser>(context);
if (user != null) {
print('user.uid is ${user.uid}');
final loggedUserInfo = Provider.of<LoggedUserInfo>(context, listen: false);
loggedUserInfo.updateUserInfo(user.uid);
print('first scan screen user: ${loggedUserInfo.userUid}');
}
return (user == null)
? WelcomeNewUserScreen()
: ServicesAroundMe();
}
}
And here is the provider:
class LoggedUserInfo with ChangeNotifier {
String _uid;
String get userUid {
return _uid;
}
void updateUserInfo(String updatedUid) {
_uid = updatedUid;
print('updated uid is $_uid');
notifyListeners();
}
}
It throws this error:
This ListenableProvider widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: ListenableProvider
You have to bear in mind that each time you call the method updateUserInfo, the notifyListeners() is triggered which tries to rebuild dirty widgets. Provider.of<T>(context) without the argument listen: false also does the same thing. Hence 2 rebuild trigger are called which cause the error.
When working with a provider, it is advisable to use a stream.
To make your code more scalable, create a custom class for your user and Either use ChangeNotifier or provider and streams.
For example;
class User {
final String uid;
final String displayName;
User({ #required this.uid, this.displayName });
}
class Auth {
User _firebaseUserMapper( FirebaseUser user) {
if(user == null){
return null;
}
return User(uid: user.uid, displayName: user.displayName);
}
Stream<User> get onAuthStateChange {
return Firebase.instance.onAuthStateChanged.map(_firebaseUserMapper);
}
}
In your page screen, you use like bellow
class HandleAuth extends StatelessWidget {
#override
Widget build(BuildContext context) {
final auth = Provider.of<Auth>(context, listen: false);
return StreamBuilder<User>(
stream: auth.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if( snapshot.connectionState == ConnectionState.active) {
User user = snapshot.data;
if (user == null ){
return WelcomeNewUserScreen();
}
return Provider<User>.value(
value: user,
child: ServicesAroundMe(),
);
}
return Scaffold(
body: Center(
child: CircularProgressIndicator();
),
);
}
);
}
The stream will forever listen for a currentUser/newUser and navigate to the appropriate page.

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
}
}