I'm currently trying to improve null safety in my flutter app, but having relatively less real-world experiences in working with null safety, I'm not confident in some of my decisions.
For example, my app requires user login, and so I have an Auth
class that preserves the state of authentication.
class Auth {
final String? token;
final User? user;
Auth(this.token, this.user);
}
In my app, I ensure that the user
property is accessed only when the user is logged in, so it's safe do the following:
final auth = Auth(some_token, some_user);
// and when I need to access the user
final user = auth.user!
which leads to the first question:
Is it recommended to use the null assertion operator in many places in the app?
I personally find it kind of uncomfortable to do things like auth.user!.id
throughout the app, so I'm currently handling it like this:
class Auth {
final User? _user;
Auth(this._token, this._user);
User get user {
if (_user == null) {
throw StateError('Invalid user access');
} else {
return _user!;
}
}
}
but I'm not sure if this is a recommended practice in null-safety.
For the next question, I have a class that handles API calls:
class ApiCaller {
final String token;
ApiCaller(this.token);
Future<Data> getDataFromBackend() async {
// some code that requires the token
}
}
// and is accessed through riverpod provider
final apiCallerProvider = Provider<ApiCaller>((ref) {
final auth = ref.watch(authProvider);
return ApiCaller(auth.token);
})
My ApiCaller
is accessed through providers and thus the object is created when the App starts. Obviously, it requires token
to be available and thus depends on Auth
. However, token
could also be null
when the app starts and the user is not logged in.
Since I'm confident that apiCaller
isn't used when there is no existing user, doing this:
class ApiCaller {
// make token nullable
final String? token;
ApiCaller(this.token);
Future<Data> getDataFromBackend() async {
// some code that requires the token
// and use token! in all methods that need it
}
}
final apiCallerProvider = Provider<ApiCaller>((ref) {
final auth = ref.watch(authProvider);
if (auth.token == null) {
return ApiCaller()
} else {
return ApiCaller(auth.token);
}
})
should be fine. However, this also makes me use a lot of token!
throughout all methods, and I'm not too sure about that.
I could also simply do ApiCaller('')
in the non-null token
version, but this seems more of a workaround than a good practice.
Sorry for the lengthy questions. I tried looking for some better articles about real-world practices in null-safety but most are only language basics, so I hope some of you on here could give me some insights. Thanks in advance!
CodePudding user response:
The simplest way to avoid using !
when you know a nullable variable is not null is by making a non-null getter like you did on your first question:
User get user {
if (_user == null) {
throw StateError('Invalid user access');
} else {
return _user!;
}
}
I will let you know that there is no need to check if the value is null before throwing an error, the null check operator does exactly that:
Uset get user => _user!;
Unless of course you care a lot about the error itself and want to throw a different error.
As for your second question, that one is a bit trickier, you know you will not access the variable before it is initialized, but you have to initialize it before it has a value, thus your only option is to make it null, I personally don't like to use the late
keyword, but it was built expressly for this purpose, so you could use it. A late variable will not have a value until expressly assigned, and it will throw an error otherwise, another solution is to make a non-null getter like on the other page.
Also, you don't need a null check here because the result is the same:
if (auth.token == null) {
return ApiCaller()
} else {
return ApiCaller(auth.token);
}
instead do this:
return ApiCaller(auth.token);
This does feel to me like a simple problem, you are just not used to working with null-safety, which means that to you, the !
looks ugly or unsafe, but the more you work with it the more you'll become comfortable with it and the less it will look as bad code even if you use it a lot around your app.
Hopefylly, my answer is helpful to you
CodePudding user response:
Is it recommended to use the null assertion operator in many places in the app?
I consider the null assertion operator to be a bit of a code smell and try avoid using it if possible. In many cases, it can be avoided by using a local variable, checking for null
, and allowing type promotion to occur or by using null-aware operators for graceful failure.
In some cases, it's simpler and cleaner to use the null assertion as long as you can logically guarantee that the value will not be null
. If you're okay with your application crashing from a failed null assertion because that should be logically impossible, then using it is perfectly fine.
I personally find it kind of uncomfortable to do things like
auth.user!.id
throughout the app, so I'm currently handling it like this:class Auth { final User? _user; Auth(this._token, this._user); User get user { if (_user == null) { throw StateError('Invalid user access'); } else { return _user!; } } }
but I'm not sure if this is a recommended practice in null-safety.
Unless you want to control the error, throwing the StateError
is pointless. The null assertion operator will throw an error (a TypeError
) anyway.
I personally don't see much value in the user
getter. You'd still be using the null assertion operator everywhere, but it'd just be hidden behind a method call. It'd make the code prettier, but it'd be less clear where potential failure points are.
If you find yourself using the null assertion operator for the same variable multiple times in a function, you still can use a local variable to make that nicer:
void printUserDetails(Auth auth) {
final user = auth.user;
user!;
// `user` is now automatically promoted to a non-nullable `User`.
print(user.id);
print(user.name);
print(user.emailAddress);
}
I think ultimately you need to decide what you want your public API to be and what its contracts are. For example, if a user is not logged in, does it make sense to have an Auth
object at all? Could you instead have make Auth
use non-nullable members, and have consumers use Auth?
instead of Auth
where null
means "not logged in"? While that would be passing the buck to the callers, making them check for null
everywhere instead, they're already responsible to not do anything that accesses Auth.user
when not logged in.
Another API consideration is what you want the failure mode to be. Does your API contract stipulate in clear documentation that callers must never access Auth.user
when not logged in? If the caller is in doubt, are they able to check themselves? If so, then making accesses to Auth.user
fatal when it's null
is reasonable: the caller violated the contract due to a logical error that should be corrected.
However, in some situations maybe that's too harsh. Maybe your operation can fail at runtime for other reasons anyway. In those cases, you could consider failing gracefully, such as by returning null
or some error code to the caller.
final apiCallerProvider = Provider<ApiCaller>((ref) { final auth = ref.watch(authProvider); if (auth.token == null) { return ApiCaller() } else { return ApiCaller(auth.token); } }
Your ApiCaller
class as presented does not have a zero-argument constructor, so that doesn't make sense. If you meant for its constructor to be:
final String? token;
ApiCaller([this.token]);
then there's no difference between ApiCaller()
and ApiCaller(null)
, so you might as well just unconditionally use ApiCaller(auth.token)
.