DEV Community

Cover image for A Kotlin developer's view on Flutter/Dart tooling (part 1)
Marc Reichelt
Marc Reichelt

Posted on

A Kotlin developer's view on Flutter/Dart tooling (part 1)

Small thing in advance: Comparing Flutter to native development was quite a hot topic (pun intended), and still is. I'm not looking for a 'winner' here. I think there are pros and cons on both sides. Often it is good to look over the fence and see what other languages, tools or ecosystems have to offer. And only by pointing out the flaws and by learning how others do it better we can improve things in our own beloved world. So let's improve!

As you might have guessed from my other blogposts, I'm a huge fan of Kotlin. For me, this began when I first used Kotlin around 3 years ago. Back then, my coworker and friend Niklas and I built a backend in 100% Kotlin. It was one of the most pleasant development experiences I've ever had. Since then, I've built a bunch of apps and other software with it, and was very pleased with the language and the tooling.

Fast-forward to today, me and an awesome team are developing a Flutter app for a customer since end of last year. And in Flutter, as you might already know, you'll write almost no Kotlin code (only if you need to have some native functionality). Almost the entire codebase is written in Dart. And having spent some time with Flutter & Dart, I gathered quite a few examples were the tools and the language were lacking and left me in the rain, in a way I didn't expect.

Think you really like something? Or: what a deprivation test is

To explain to you how I felt I better explain what a deprivation test is. FastCompany has an article on how GitHub uses deprivation testing for product design. The basic idea is: Someone has a thing. That could be a product, feature or more. Now you give that person a new thing, and let that person use that for some time. Now comes the deprivation part: You take that new thing away from that person. So the person now is back using the old thing. And then you observe their emotions and you collect feedback. Was it ok for them to go back? Or are they missing the new thing - and why?

I'm sure everyone of you had multiple moments of that in the past. Maybe you had a smartphone that broke, and you had to use an older feature phone for some time, only to be infuriated by it. Or you already used and loved Git, and were forced to use Subversion again because a project didn't migrate yet. Or you had to go back to your old coffee beans because your go-to coffee beans were out of stock, only to find out you hadn't notice how much you liked your go-to coffe beans already.

To me, I had a very strong deprivation moment when going back to a Java project after developing in Kotlin. I couldn't really tell it before, but it made me love Kotlin even more - because I discovered that the vast amount of small improvements were making my coding experience so much better than I expected. I couldn't even tell it before what was making me so much happier - but I could tell very easily when those things disappeared.

And to get back to the topic, I also had a strong deprivation moment when switching from Kotlin to Flutter/Dart. Which I didn't expect, because I actually used Dart (without Flutter) before starting with Kotlin to learn about and implement an interpreter. Coming from Java I thought Dart was quite nice. But coming from Kotlin, my perspective changed completely. What was I missing? Without further ado: Let's dive in!

Dart, coming from Kotlin

Missing null safety

The first thing hits hard. For a long time (and still), Dart had no null safety like Kotlin built-in. So you'll find having to write a lot of these (Dart):

  Person({@required this.name, @required this.age})
      : assert(name != null),
        assert(age != null);

Instead of this (Kotlin):

class Person(val name: String, val age: Int)

And your IDE won't show an error if you write something like Person(name: null, age: null) in Dart. Brace yourselves for NullPointerExceptions.

I can't wait for the Dart team to finalize the support for null safety (they are working on it since 1.5 years), and hope the Dart/Flutter ecosystem - including all the packages and plugins - catches up quickly. If someone of the Dart teams reads this: I think you are doing an amazing job - take your time to finish this feature! šŸ‘ I really hope we'll be able to use it soon.

No data classes / sealed classes

Yep, no data classes in Dart built-in. The class above was actually written like this (Dart):

class Person {
  final String name;
  final int age;

  Person({@required this.name, @required this.age})
      : assert(name != null),
        assert(age != null);

  // toString, equals, hashCode not shown for brevity

}

In Kotlin, the example above was only missing the data keyword:

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

There are tricks to get something like this in Dart by using generators, but come on - it's 2020, and even Java 14 has records now.

I want to highlight a talk by Pascal Welsch named Making Dart more like Kotlin, in which he highlights a pretty new package freezed by Remi Rousselet. This generates code so you can get something like data classes - and even sealed classes - in Dart. It looks promising, but I didn't get my hands on it yet. Still, I believe these features must come to the Dart language as a standard.

Enums

Enums in Dart can't have any fields. So this is basically what you can do with enums:

enum Environment { prod, test }

Want to add a backend URL to all those? Write a switch/case. Want to add a lambda that initializes something for them? Write a switch/case. In Kotlin (as in Java) you can just add them as constructor parameters:

enum class Environment(val url : String) {
    prod("https://prod"),
    test("https://test")
}

Semicolons;

Semicolons in Dart are everywhere! This is something that I really wouldn't have assumed I would notice, but after spending a lot of time in Kotlin, it actually is noticable, and having no semicolons would make the code more readable.

_privateFields

Dart has no private keyword. Private fields are written with underscore prefix. I really can't stand this, this tiny thing spreads over the whole codebaseā€¦

final _message = 'please release me';

void doSomething() {
    why(_message);
    println('is ${this._message}');
    final bar = _message + _message + _message + ' so hard';
}

The new tabs vs. spaces: 'single quotes' vs. "double quotes"

Yep, you can write strings either way:

String a = '$foo $bar';
String b = "$foo $bar";

It's basically the same. And I can't even imagine how many commits there must be out there that change single quotes to double quotes, or the other one around. I mean, developers like to argue about small things anyway. Would be boring otherwise, wouldn't it? šŸ˜›

if / switch can not be used as expressions

This is a really useful feature in Kotlin, and I didn't think I would miss it. Boy, was I wrong. Let's see a cool example from Kotlin, where we use the combined power of sealed classes, smart casts, and the return value of when:

val result = createUser(credentials)
val message = when (result) {
    is Success -> "User ${result.user.username} created"
    is UserDoesAlreadyExist -> "Error: user ${result.username} does already exist"
}

Yes, when can be treated as an expression - and the Kotlin compiler will show you an error if you forget to handle a possible branch (less bugs!).

Only constant values for optional parameters

I noticed this when I tried to make a lambda parameter non-nullable in Dart, like this:

void doSomething({Function something = (){}})

This is the error you'll see:

Default value of an optional parameter must be constant

I tripped a few times over this (not only for lambdas), and it's kind of sad this doesn't work.

Constructors

One thing that already hit me a few times in Dart is that final fields can not be set in the constructor body. Let's see this simple example, where I have two classes ServiceA and ServiceB, where the second depends on the first, and I want to initialize them in my class:

class MyClass {
  final ServiceA serviceA;
  final ServiceB serviceB;

  MyClass(){
    this.serviceA = ServiceA();
    this.serviceB = ServiceB(serviceA);
  };
}

serviceA can't be used as a setter because it's final

To do that we would have to implement a series of constructors instead:

class MyClass {
  final ServiceA serviceA;
  final ServiceB serviceB;

  MyClass(this.serviceA, this.serviceB);

  MyClass.foo(this.serviceA) : serviceB = ServiceB(serviceA);

  MyClass.bar() : this.foo(ServiceA());
}

As you can see, this grows quite a bit and makes the code unreadable. And we only had one field that depended on the other. Imagine having 3 or more dependent fields. Yikes.

await: Easy to forget

Let's see this code:

Future<void> doSomethingImportant() async {
  // something important here
}

Future<void> main() async {
  doSomethingImportant();
}

In the default Dart Lint configuration, it'll just let you do this - and depending on how your code executes, the important stuff will never run. Because you forgot to add an await keyword in front of the doSomethingImportant() call. And the IDE didn't warn you. There is a Lint option called unawaited_futures from the pedantic package that will prevent you from this. But that option really should be the default.

Kotlin, with Coroutines, will prevent you from accidentally forgetting this:

suspend fun doSomethingImportant() {
    // something important here
}

fun main() {
    doSomethingImportant();
}

The IDE shows an error directly, and you must either make that method suspend as well or wrap it in something like runBlocking:

Kotlin shows error because suspending call is called without Coroutine

Oh, and by the way: You can wrap suspending functions in runBlocking in Kotlin. You can't in Dart. The only possible option is to slap await and async up to the top-most caller, which might be the main method - forcing you to add those keywords in every function of that stack, sometimes forcing you to entirely re-do your architecture if you missed this. Sometimes, programming languages have to be pragmatic to not stand in the way.

Collection API is bad

Lists in Dart are mutable by default. If you're used to Kotlin, you'll know how many nice collection functions there are. Most of them are missing in Dart. And in order to have immutable collections, you'll have to write your own. And did I mention that lists don't have equality defined?

final a = [1, 2, 3];
final b = [1, 2, 3];

void main() {
  // prints false
  print(a == b);

  badMethod(a);
  // a has now changed - good luck in finding those bugs introduced by mutable collections
  print(a);
}

void badMethod(List<int> a) {
  print(a);
  a.add(4); // please don't do this
}

Of course Dart devs will say that you can use const instead of final to get immutable values. And indeed, if we would write const a we would get an exception when we call a.add(4):

Uncaught Error: Unsupported operation: add

But the point of Kotlin's immutable collections is that you didn't even have an add method to call in the first place.

Again, there are nice libraries out there that deal with this. But this is something that belongs to the language itself - otherwise projects will never settle on a common language.

Here is an example how a nice method like associateBy (which does exist in the dartx package, but does not exist in Dart) could help every Dart developer to easily create a map from a list:

Map<int, Person> associateById(List<Person> people) {
  return people.associateBy((person) => person.id);
}

If you use Kotlin, you'll get used to use these functions all the time.

So many missing stdlib functions

If you come from Kotlin, there are so many extension functions that make your life easier. Just look at all those extension functions defined in String.kt. There are removePrefix, removeSurrounding, substringBefore, just to name a few. All those together are a massive improvement in developer productivity.

Again, Dart developers noticed this - and finally that we have extension functions in Dart, we can use those to make code much better. Some Dart developers built the great dartx package, which adds all sort of functions to the standard types. And again, this should be something that every Dart developer should be able to rely on in every project - without setting anything up.

Questionable default implementations

If you ever handled files and directories in Dart, you'll see that Directory and File have no equality defined. If you ever tried to create a set of these: nope, you'll have to convert them to absolute String paths instead in order for the set to work.

Another example is the use of dynamic in APIs. Have a look at the stdout of the ProcessResult class. It's dynamic. According to the documentation:

Standard output from the process. The value used for the stdoutEncoding argument to Process.run determines the type. If null was used this value is of type List<int> otherwise it is of type String.

So we have to just cast to String and hope it works. Type-safety is important: please don't use dynamic if you can avoid it.

More

Wow, that was already a bunch. And I didn't even run out of things I wanted to write about.

If you are a Flutter or Dart developer, and you stumbled over at least a few of these things: you're not alone! I feel your pain. Maybe you found even more things I didn't mention: Please write me on Twitter. I want to collect more of these, so we have a good overview what we as a community can improve in the future.

As I started this blog post I had in mind to write about the shortcomings of Dart as well as Flutter tooling. I realized there is so much I found with Dart - I didn't even get to the Flutter bit yet. That's why there will be a part 2 of this post soon! Follow me on Twitter and click the +FOLLOW button below to get the new post while it's hot!

Cover photo by Christopher Burns on Unsplash.

Latest comments (2)

Collapse
 
2zerosix profile image
Bogdan Lukin • Edited

Great article!

In fact, thanks to the Flutter, Dart is growing faster than ever, and a lot of work is going on to solve most of the issues that you mention here. Most of development communications are public and one can find current progress of upcoming features in project and much more minor improvements are discussed in issues.

Some useful analysis options are disabled by default, but common recommendation is to use additional rules for example: effective_dart or pedantic. There is a full list of available options: dart-lang.github.io/linter/lints/

Some useful packages you didn't mention:

  • freezed - sealed classes, pattern-matching and interesting approach to handle [non-]nullable ( runtime :( )
  • built_collection - immutable collections
Collapse
 
terkwood profile image
Felix Terkhorn

Nicely written. I've been experimenting with Kotlin here and there for about a year, and really enjoy the null safety. In my case, it's hard to dive back into Scala dev after benefitting from the additional help given by Kotlin's compiler.

The lack of private vars in Flutter would drive me nuts.

Thanks - - looking forward to your next article.