Best practices in dart null safety

562

Solution 1

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

Solution 2

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).

Share:
562
Allen Hu
Author by

Allen Hu

Updated on January 03, 2023

Comments

  • Allen Hu
    Allen Hu over 1 year

    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!

    • Josteve
      Josteve about 2 years
      Check out the late keyword.
    • jamesdlin
      jamesdlin about 2 years
      @Josteve Using the late keyword usually should be a last resort. You should use it only if you can guarantee that the variable will never be accessed before being initialized.
  • Allen Hu
    Allen Hu about 2 years
    Now that you have pointed it out, null-assertion surely does the same as what I wrote... I think ultimately it is to decide where my nulls should be caught and asserted, which needs some experience for making the correct decisions.
  • Allen Hu
    Allen Hu about 2 years
    Indeed this is to some degree a "contracts" problem. I'm currently refactoring the code so the rules are mostly done and enforced by myself, so how I program and enforce those "contracts" is what I'm working on. Thanks for the heads up!