Skip to main content

13 reasons why Dart is worse than Kotlin

· 10 min read

As you probably know from my previous articles and talks, I love Flutter. I really think that this is one of the best options to develop a high-quality multi-platform app. It’s so easy to create custom views with complex animations – especially, comparing with the native Android View layer. The way it promotes composition over inheritance and immutable widgets, or encourages you to make the view a pure function of a state makes me happy.

Unfortunately, I cannot say the same about Dart, the language that Flutter is written in, and that you’re supposed to use to develop Flutter apps. Why? There are at least 13 reasons for that.

#1 No null safety

Yes, I know that this will hopefully become obsolete in the (near?) future, but for now, we have to deal with it. Anyway, for the language with the first release in the year 2018 (I mean Dart 2 of course), it’s a shame not to have null safety from the very beginning.

#2 No data classes

Data classes are classes whose main purpose is to hold data [sic!]. They are highly useful as Value Object from DDD, and also, with proper language support, it’s very convenient to use immutable data classes for functional programming style.

By proper support I mean something like this in Kotlin:

data class User(val name: String, val age: Int)

val user1 = User(name = "John", age = 30)
val user2 = user1.copy(age = 20)

Even without implementing copy (and equality) functionality the Dart way is more verbose:

class User {
User({this.name, this.age});

final String name;
final int age;
}

final user1 = User(name: 'John', age: 30);
final user2 = User(name: user1.name, age: 20);

And it still lacks some features that are provided by Kotlin. E.g. in Kotlin you can pass these parameters as positional or as named (and even mix them: val user = User('John', age = 30)), while in Dart you’re either making them positional or named – but in that case they will be optional and default to null.

There is a nice package built_value that solves the problem with deep equality check and generating copy method, and we use it a lot in our codebase, but it’s not an ideal solution.

First, it still requires some boilerplate code compared to Kotlin version:

abstract class User implements Built<User, UserBuilder> {
User._();

factory User([void Function(UserBuilder) updates]) = _$User;

String get name;

int get age;
}

final user1 = User((b) => b
..name = 'John'
..age = 30);

final user2 = user1.rebuild((b) => b..age = 20);

Second, it lacks some features, e.g. you cannot easily enforce required parameters during compile time.

Will we have proper support for data classes in Dart? Probably, but not anytime soon.

#3 No sealed classes

Another feature in Kotlin language that we use a lot: sealed classes, aka co-product types, aka sum types, aka "enums on steroids"… They are extremely useful if you want to make your types sound.

Essentially, they represent a restricted class hierarchy: a value can have one of the types from a limited set. Unlike enum, this value is not a singleton, but a proper class that can have multiple instances with different states.

Why is it useful? Let’s see at probably the most common example:

sealed class Result<out VALUE> {
data class Error(val error: Throwable) : Result<Nothing>()
data class Success<VALUE>(val value: VALUE) : Result<VALUE>()
}

fun process(result: Result<String>): String = when (result) {
is Result.Error -> "Error: ${result.error}"
is Result.Success -> "Success: ${result.value}"
}

This is a nice alternative to exceptions: instead of catching exceptions (or sometimes forgetting to do that) you’re forced to deal with the result, which can be either error or valid result (the corresponding type in many functional languages and libraries is called exactly like this: Either). Not only you will have to take a possible error into account, but Kotlin will provide some nice features as well. Do you see casting inside when branches? You don’t, because no manual casting is needed. Kotlin is smart enough to do smart casting (pun intended) automatically.

What about Dart? Well, maybe someday we will get there.

#4 No custom values in enums

Sometimes (well, actually quite often) you need to associate some value with enum. In Kotlin it’s as easy as this:


enum class Level(val value: Int) {
INFO(10),
WARNING(20),
ERROR(30)
}

val value = Level.WARNING.value

Dart? Looks like no. You can use extensions to achieve similar behavior:

enum Level { info, warning, level }

extension LevelValue on Level {
int get value {
switch(this) {
case Level.info:
return 10;
case Level.warning:
return 20;
case Level.level:
return 30;
}
}
}

final value = Level.warning.value;

But first, it’s more verbose, and second, that leads us to the next point…

#5 Compiler is not smart enough

The previous example gives me this warning:

This function has a return type of ‘int’, but doesn’t end with a return statement. Try adding a return statement, or changing the return type to ‘void’.

What? Level enum has only 3 possible options, and they are all listed. There’s no way for this function to return void. I don’t want to add default branch here (otherwise, if I add another option to the enum, I can forget to update the extension, and it will silently return an incorrect result). I don’t want to suppress the warning for the same reason. I want an error here, if and only if I didn’t match all the possible options: that’s what Kotlin does.

#6 No singletons

How can you define singleton in Kotlin? Like this: object Singleton. Anything similar in Dart? I guess this is the simplest way:

class Singleton {
const Singleton._();

factory Singleton() => const Singleton._();
}

Not a big deal, of course, but when you have to write this for every case it adds up. And we use them a lot in our Kotlin project e.g. as "constant" branches of sealed classes… Oh wait, we don’t have sealed classes in Dart anyway.

#7 No switch/if/try expressions

Do you remember the example from #3 No sealed classes? There’s no return keyword, yet the function is returning a string. That’s possible thanks to two things:

  • fun getAnswer(): String = "42" is equivalent to fun getAnswer(): String { return "42" }. In Dart you can do it as well with String getAnswer() => '42';.
  • when is an expression: it means you can return the result of when and the compiler is smart enough to infer the correct type since every branch is returning String. And it’s not only about concise syntax. With returning when compiler forces us to provide all the possible options (e.g. in case of enums or sealed class you must either specify all the options or use else branch).

#8 No protected keyword

In order to make method (or variable) private, you need to prefix its name with underscore. For making it protected you can use @protected annotation from the meta package. In the first case, you’ll get a compilation error when trying to access it from outside of the allowed scope. In the second case, the best you can get is static analyzer feedback. Dart doesn’t have language support for protected members so it’s all or nothing. No compromises.

#9 No type aliases

Actually, type aliases in Dart exist, but only for function types. So you can write something like typedef FormatDate = String Function(DateTime);, but not typedef Json = Map<String, dynamic>. So, if you’re working with json in your project, add this type to snippets, you will use it often.

Yeah, the sun will shine on our side of the fence. Someday…

#10 No concise syntax

Ok, you can say here: "You just got used to Kotlin syntax, and try to turn Dart into Kotlin". Well, that’s true, I find Kotlin syntax pretty nice, but it isn’t just about getting used to it. Things like lambda syntax are so much more concise in Kotlin, e.g. listOf(1, 2, 3).map { it.toString() } looks better than [1,2,3].map((i) => i.toString()); even in such a simple example. When you have a multiline lambda (or chains of lambdas) Dart way becomes too complex for writing and reading. And this required semicolon… Come on, it’s the year 2020!

#11 No nested classes/extensions

What about having some way to nest classes? E.g. we would like to do something like this with messages:

abstract class Translations {
static abstract class Common {
static String yes = 'Yes';
static String no = 'No';
}

static abstract class Auth {
static String logIn = 'Log in';
static String logOut = 'Log out';
}
}

final message = Translations.Auth.logIn;

Dart? Sure, we have a feature request for that.

#12 No proper generic variance

In Dart type variables in generic classes are covariant. Why is it bad? Because it’s an easy way to shoot yourself in the foot. Let’s take a look at this example:

class Animal {}

class Dog extends Animal {}

class Cat extends Animal {}

void main() {
List<Animal> animals = [Dog()];
List<Cat> cats = [];
cats.add(animals.first);
}

This will compile without any problems, but in the runtime, you will receive an error:

TypeError: Instance of 'Dog': type 'Dog' is not a subtype of type 'Cat'.

You can try to do the same trick in Kotlin:

abstract class Animal
class Dog : Animal()
class Cat : Animal()

fun main() {
val animals = listOf<Animal>(Dog())
val cats = mutableListOf<Cat>()
cats.add(animals.first())
}

But it won’t even compile. It will give you an error:

Type inference failed. Expected type mismatch: inferred type is Animal but Cat was expected.

Here you can read about generics variance in Kotlin, and this is an ongoing discussion about variance in Dart.

#13 No final classes

Effective Java says: "Design and document for inheritance or else prohibit it". So in Java it’s a good practice to use final classes as often as possible. Kotlin takes one step further and makes classes final by default. What about Dart? Well, there’s no way at all to make a class final. You cannot prohibit inheritance. Freedom for everyone!


So, are there only negative things I can say about Dart? Well, there are actually some points that I really like:

  • Classes are interfaces. Each class implicitly defines an interface that consists of its public members. So you can just implement any class and provide overridden functionality: nice thing to use in tests for mocking implementation.
  • No type erasure. Unlike Kotlin, List<String> is still List<String> in runtime.
  • Last but definitely not least: language is evolving. We’ve got extensions, probably we will get null safety soon enough. Hopefully, other things from my list will become obsolete someday and Dart will eventually become Kotlin a modern and safe language.