How to leverage Dart context-aware safety for clean and readable code?

How to leverage Dart context-aware safety for clean and readable code?

Sometimes, even the simple code can be hard to read and understand. Experienced developers know that writing clean, readable code is essential for maintainability and scalability. You all will agree that the code you write today will be read by someone else tomorrow. So, it’s important to write code that is easy to read and understand. In this article, we’ll take a look at some of the ways you can leverage Dart syntax to make your code more readable and maintainable.

  • Use context-aware null safety smartly
  • Use context-aware type safety smartly


Use null safety smartly

We often assert a variable as null-null, whenever the receiver is a non-nullable type. If a variable is already checked for null safety, the dart compiler does not complain about it in the checked block. This context-aware null safety is a great feature that can help you write cleaner and more readable code.

void main() {
  String? name = 'John Doe';

  printNameOrDoNothing(name);
}

void printNameOrDoNothing(String? name) {
  if (name != null) {
    print(name.length); // notice that we needn't assert name as non-null
  }
}        

You might be using this pattern already. The confusion occurs when you have to access a value within a nested object, and which is nullable. In such cases, you might have to assert the nested values as non-null. But there’s a trick for that. The Dart compiler is only aware of the null safety of the immediate object. So, if you assert the immediate object as non-null, you can access the nested values without asserting them as non-null. As we’ve seen above, you can use the context-aware null safety smartly to make your code more readable. So to sum up, we only have to store the nested object in a variable and assert Dart compiler that it’s not null.

// void printCityOrDoNothing(User user) {
//   if (user.address?.city != null) {
//     print(user.address!.city);
//   }
// }

void printCityOrDoNothing(User user) {
  final city = user.address?.city;

  if (city != null) {
    print(city);
  }
}

class User {
  final String name;
  final Address? address;

  User({required this.name, this.address});
}

class Address {
  final String city;

  Address({required this.city});
}        

Notice that, in the commented code, we had to assert the nested value as non-null. But in the subsequent code, we just stored the nested value in a variable and checked its null safety, so, we did not have to assert it. This makes the code more readable and maintainable.


Use context-aware type safety smartly

Dart is a statically typed language, which means that, the type of the variable is known at compile time. This can help you catch errors early in the development process. But sometimes, you may have to explicitly specify the type of the variable, which can make your code less readable.

Similar to how we implement smart context-aware null safety, we can implement context-aware type safety. If the type of variable can be inferred from the context, we can omit casting the type explicitly. This can make the code more concise and easier to read.

abstract class User {
  String get id;

  String? get name;
}

class Developer extends User {
  final String id;
  final String? name;

  final String vcsId;

  Developer(this.vcsId, {required this.id, this.name});
}

class OpsUser extends User {
  final String id;
  final String? name;

  final String opsId;

  OpsUser({required this.id, required this.name, required this.opsId});
}

class OpsRepository {
  void delete(String id) {
    // delete user
  }
}

class VCSRepository {
  void delete(String id) {
    // delete user
  }
}

class UserRepository {
  void delete(String id) {
    // delete user
  }
}

final userRepo = UserRepository();
final opsRepo = OpsRepository();
final vcsRepo = VCSRepository();

extension Delete on User {
  void delete() {
    // switch (this) {
    //   case Developer():
    //     vcsRepo.delete((this as Developer).vcsId);
    //   case OpsUser():
    //     opsRepo.delete((this as OpsUser).opsId);
    // }

    final user = this;
    
    switch (user) {
      case Developer():
        vcsRepo.delete(user.vcsId);
      case OpsUser():
        opsRepo.delete(user.opsId);
    }

    userRepo.delete(this.id);
  }
        

In the commented code, we had to cast the type of the user explicitly. But in the subsequent code, we just stored the user in a variable and the compiler is smart enough to take the hint from the variable based on the context. Notice, how easy it is to read and understand the subsequent code!!

Here to make the community stronger by sharing our knowledge. For more tech blogs read here and visit Nonstopio to stay updated on the latest and greatest in the web & mobile tech world.

要查看或添加评论,请登录

社区洞察

其他会员也浏览了