The beauty of the Flutter's programming language: 4 superb features of Dart

The beauty of the Flutter's programming language: 4 superb features of Dart

A little deep dive into some of the most useful features of the Dart language.

One of the cons that I read most when reading about Flutter is the use of the Dart programming language. It is not as mature as Kotlin is one of the most mentioned arguments I read. In my opinion (and I admit that it can be controversial), Dart is a great language, I wouldn't change it for any other when creating apps with Flutter, and I say it after working professionally creating android apps in Kotlin, which is also an elegant and beautiful language, by the way.

In this article I intend to showcase 4 of my favorites features of the Dart programming language, in no particular order; let's see how can we take advantage of this modern tool:

Null safety

Recently added in version 2.12 (included in Flutter 2.0). Null safety is a must in any modern language that pretends to be solid and productive. Having runtime errors because a variable is null when it isn't intended to be null should be something of the past, that's why the Dart team has been working on implementing sound null safety, which means that we can have types that can be nullable and types that can't be nullable, if we try to perform an unsafe operation on the later we will get a compilation error before the app is built:

// This is a String that can be null
String? nullVar;

// This String cannot be null, the compiler forces me
// to set it a value because of its non-nullable nature.
String nonNullVar = 'I am not null';

// Alternatively, I can specify that the value will be
// set later, but the property continues to be non-nullable.
late String lateNonNullVar;

// If I want to call a method on an instance of a type
// that can be null, I need first to do a runtime check that
// its value is not null.
if (nullVar != null) {
  nonNullVar.toLowerCase();
}

// Or call it using the '?' operator, which means that the 
// method will only be called if the instance is not null:
nullVar?.toLowerCase();

// If the type is not nullable I can safely call any
// method on it directly.
nonNullVar.toLowerCase();

// Always remember to initialize late vars, or you
// will get an exception when trying to access its members.
lateNonNullVar = 'some value';
lateNonNullVar.toLowerCase();

Async / await

Just like in Javascript we have Promises , in Dart we have Futures ; where the async/await are the main keywords, giving us the developers an easy and powerful way to handle asynchronous operations:

With Futures we can easily stop the flow of the current operation, wait for some asynchronous operation to complete, and then resume the work.

// To specify that a function will perform an asynchronous
// operation (like doing a network request or reading from
// a database) we mark it with the 'async' keyword:
asyncFunction() async {
  // ...
}

// Use the 'await' keyword to stop the flow until the function
// has completed its task:
await asyncFunction();

// You must declare a function as 'async' if it contains
// calls to other async functions:
main() async {
  await asyncFunction();
}

// If the async function returns a value, wrap it within
// a Future. For instance, the following function
// would do a network call and return its result:
Future<NetworkResult> doNetworkCall() async {
  // ...
}

final result = await doNetworkCall();

// But what if the network request fails and an exception
// is thrown? Just wrap the call in a try/catch block:
late NetworkResult result;
try {
  result = await doNetworkCall();
} on NetworkException catch (e) {
  // Handle error
}

It is important to mention that even using the async/await keywords everything is executed in the same thread, if we need specific requirements for performance we can spawn alternative threads with dart isolates.

Multiple ways to define function parameters

In Dart we have multiple options when defining function parameters:

// You can define mandatory parameters as you do in
// many other languages, specifying their type and setting a label:
functionWithMandatoryParameters(String someString, int someNumber) {
  // ...
}

// You are forced to send the defined parameters 
// when using the function:
functionWithMandatoryParameters('some_string', 46);

// You can however specify that the parameters are optional:
// (note that the type must be defined as nullable, precisely because
// there's no guarantee that the caller will send a value)
functionWithOptionalParams(
  {String? optionalString, int? optionalNumber}) {
  // ...
}

// You can call this function without sending any values,
// or specifying a value for an optional parameter with its label:
functionWithOptionalParams();
functionWithOptionalParams(optionalString: 'some_string');
functionWithOptionalParams(
  optionalString: 'some_string', optionalNumber: 46);

// When defining optional parameters, you can set a default value
// that will be used in case that there is no value sent by the caller:
functionWithDefaultValue({String someString = 'default'}) {
  // ...
}

// The value of someString is 'default'
functionWithDefaultValue();
// The value of someString is 'some_string'
functionWithDefaultValue(someString: 'some_string');

// Lastly, you can even define mandatory named parameters with the
// 'required' keyword, this is useful to enhance code readability.
createUser(
  {required String username,
  required String name,
  required String surname,
  required String address,
  required String city,
  required String country}) {
// ...
}

createUser(
  username: 'Ghost',
  name: 'John',
  surname: 'Doe',
  address: '3590  Mill Street',
  city: 'Beaver',
  country: 'US');

Composition with mixins

One of the least trendings in software development is composition over inheritance, which means adding functionality to a class using component-like elements instead of inheriting from a parent. This method allows us to easily add encapsulated functionality without dealing with a complex hierarchy of inheritance.

For instance, imagine that you have a login logic that you might want to use in different places within your application. You could create a component (a mixin) with this logic and then reuse it whenever you need it:

abstract class AuthUtils {
  Future<User> login() async {
    // Here we can add the login logic that will later be reused
    // in any class that ads this mixin.
  }
}

class LoginPage extends StatefulWidget {
  LoginPage({Key? key}) : super(key: key);

  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> with AuthUtils {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: login(), // Calling the mixin function
      builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
        // ...
      },
    );
  }
}

The advantage here is that we can add as many mixins as we want, opposing to inheriting from just one parent by using inheritance.

Here is a great post explaining with a lot more details what mixins are about.

Conclusion

These are only 4 of the multiple useful features that Dart offers to developers. If you want to learn more I recommend you to visit the Dart language tour, which explains every detail of the language in a very friendly way.

Happy coding!

Did you find this article valuable?

Support David Serrano by becoming a sponsor. Any amount is appreciated!