Dotlin is a Kotlin to Dart compiler. The aim is to integrate Kotlin as a language into the Dart ecosystem, combining best of both worlds: The Kotlin language & standard library, and the Dart ecosystem & build system.
Dotlin makes use of Kotlin's IR (Immediate Representation) compiler, and uses that to generate Dart source code.
Dotlin is still in development. You can track overall development on the project board.
- Supports all Kotlin language features (WIP: #25)
- Supports the Kotlin standard library (WIP)
- Use any dependency written in Dart, without conversion or writing type definitions (WIP)
- Integrates with Dart's build system (use
pubspec.yaml
to define dependencies) - Generates code that is still readable and pleasant to use for Dart consumers (WIP)
Dotlin is a dialect of Kotlin. Some changes have been made to better integrate into the Dart runtime, and also to remove some JVM-centric legacy traits.
Note that because of these changes, Dotlin code is not compatible with Kotlin/JVM, or other official Kotlin variants. Dotlin aims to intergrate the Kotlin language (and stdlib) into Dart, not the full Kotlin ecosystem.
Because of the Dart runtime, there is no type erasure. This means that you will never need to use reified
in Dotlin.
For example, the following code, which would fail in Kotlin, works in Dotlin:
class MyClass<T>
fun test(arg: Any) {
if (arg is MyClass<String>) {
// Do something.
}
}
This would've been reported in Kotlin as:
⚠️ Cannot check for instance of erased type: MyClass<String>
In Dart, any class can be implemented as an interface. In Kotlin, you either have an interface or a class.
Since you can use any Dart library in Dotlin, you can also implement any Dart class as an interface or mixin, just like in Dart. The syntax for that is as follows:
class MyClass : TheirDartClass(Interface), AnotherDartClass(Interface)
This will compile to (leaving out irrelevant code for example's sake):
class MyClass implements TheirDartClass, AnotherDartClass {}
Even though TheirDartClass
is a class
in Dotlin (not an interface
) you can implement
it as an interface. When you implement a Dart class like this, it's implemented
as a pure interface (like in Dart), meaning you have to implement the whole interface yourself.
The same can be done for mixins:
class MyClass : TheirDartClass(Mixin)
class MyClass with TheirDartClass {}
This only works if TheirDartClass
can be used as a mixin, meaing it either is declared with the mixin
keyword, or
has no constructors and extends Object
(Any
). If a Dart class is not a valid mixin, the
special mixin inheritance syntax is not available.
If you want to extend a Dart class, regular Kotlin syntax can be used.
The implicit interface/mixin syntax is only necessary for Dart libraries that don't have handwritten
Dotlin declarations for them. If there are Dotlin declarations, regular Kotlin class
/interface
rules apply.
Kotlin has a very strict concept of const
. Only a few primitives can be declared const
, and only as
top-level values or properties on object
s. In Dart, on the other hand, it's possible to have const
constructors
for classes and collection literals, and have local const
variables.
To facilitate this, const
is also more lenient and Dart-like in Dotlin. This means that the following Dotlin code:
class MyClass const constructor(private val message: String)
const val myFirstClass = MyClass("First")
fun main() {
const val mySecondClass = MyClass("Second")
}
Compiles to:
class MyClass {
const MyClass(this._message);
final String _message;
}
const MyClass myFirstClass = MyClass('First');
void main() {
const MyClass mySecondClass = MyClass('Second');
}
You can use all Dart const
features in Dotlin.
If you want to explicitly invoke a const
constructor, you can use the following syntax:
@const MyClass("Something")
Note the @
before const
. This is because @const
is an annotation, not a keyword.
The Kotlin compiler does not support keywords in front of expressions at the parser level.
The difference is easy to remember: with any declaration you must use const
, and with any
invocation you must use @const
.
Note that as in Dart, @const
is not necessary when it's implied, e.g. by assigning to a const val
.
In Kotlin, lateinit
is not applicable to properties with types that are primitive or nullable/have a nullable upper bound. In Dotlin, this is possible.
For example, the following code, which would fail in Kotlin, works in Dotlin:
class Example<T> {
lateinit var myNullableVar: String?
lateinit var myPrimitiveVar: Int
lateinit var myGenericVar: T
}
Respectively, these declarations would've been reported in Kotlin with the following errors:
⚠️ 'lateinit' modifier is not allowed on properties of nullable types
⚠️ 'lateinit' modifier is not allowed on properties of primitive types
⚠️ 'lateinit' modifier is not allowed on properties of a type with nullable upper bound
But with Dotlin, this compiles to:
class Example<T> {
late String? myNullableVar;
late int myPrimitiveVar;
late T myGenericVar;
}
In Kotlin, lateinit var
s cannot be checked whether they're initialized from outside the containing class. For example, the following code:
class Example {
lateinit var lateVar: String
}
fun main() {
if (Example()::lateVar.isInitialized) {
// Do something.
}
}
The call would've been reported as:
⚠️ Backing field of 'var lateVar: String' is not accessible at this point
However, in Dotlin, this compiles with no issues.
Kotlin primitives that are not used in Dart and would only complicate code have been removed, meaning that
Byte
, Short
, Long
, Float
, and Char
are not present. This is because Dotlin has the following
mapping of built-ins:
Dart | Kotlin |
---|---|
int |
Int |
double |
Double |
String |
String |
bool |
Boolean |
Object |
Any |
Never |
Nothing |
This also means that Int
now refers to a 64-bit integer, instead of 32-bit as in Kotlin.
In Kotlin, any class that implements hasNext()
and next()
is considered an iterator. In Dotlin,
this is not the case. Instead, it's more like Dart: A class is only an considered an iterator if it
implements dart.core.Iterator
. This means that the Dart Iterator
API is used: instead of
hasNext()
and next()
, moveNext()
and current
are used.
kotlin.collections.Iterator
is not available. However, the kotlin.collections.Iterator
subtypes are, changed to fit dart.core.Iterator
: MutableIterator
, BidirectionalIterator
,
ListIterator
, and MutableListIterator
.
In Kotlin, you can only throw Throwable
or its subtypes. In Dotlin, this
restriction is removed. As in Dart, you can throw anything except null
.
throw "This works!"
To integrate better with the Dart runtime, and because Dart has better
error/exception
defintions, they are used instead of the JVM exception classes. This also means Throwable
is not available, since it doesn't
serve any use anymore.
In Kotlin, only annotation class
es can be used as annotations. In Dart, classes with at least one const
constructor
or const
top-level values can be used as annotations.
To facilitate, the same is possible in Dotlin. Classes and properties that can be used as annotation, have synthetic
annotation class
counter-parts generated in an annotations
package, which can be used as annotations.
For example, if you have a Dart class in lib/markers.dart
such as:
class Fragile {
const Fragile();
}
It can be used in Dotlin like so:
import my.package.markers.annotations.Fragile
@Fragile
class Box
Normally, the Fragile
class would be imported through my.package.markers.Fragile
, but the annotation is in a special annotations
sub-package.
Note that if you want to use Fragile
in a non-annotation context, you can stil use it as normal through my.package.markers.Fragile
.
In Kotlin, declarations are imported through their FQN (fully-qualified name), while in Dart they're imported through URIs. In Dotlin, FQNs are generated based on a few factors.
For example, if you have a package named species
published by example.com
,
which has a file lib/mammals.dart
where the class Human
is declared, you can import Human
as follows in Dotlin:
import com.example.species.mammals.Human
A lot of packages follow the convention of exporting their published API in a file with the same name of their package in lib/
. For example,
you'd import package:species/species.dart
and use Human
from there, because it's export
ed (or declared) in species.dart
.
To import a package-level import, Dotlin removes the need of specifying the package name twice, thus you'd import it like so:
import com.example.species.Human
If a package does not have a verified publisher, the package host is used as a fallback (in most cases pub.dev
):
import dev.pub.species.Human
If a package does not have a host (e.g. it's local or from git), it's prefixed with pkg
:
import pkg.species.Human
In the future, you'll also be able to provide a groupId
per dependency in pubspec.yaml
, also for your own package.
Aside from the obvious differences between the Kotlin language and stdlib, Dotlin adds some Dart-specific enhancements. Also some other additions, because of differences between the Dart and Kotlin languages.
In Dart, you cannot pass lambda literals (function expressions) as arguments to const constructors, only references to top-level or static named functions.
In Dart, the following code:
class Hobbit {
const Hobbit(this._computeName);
final String Function() _computeName;
}
void main() {
const bilbo = Hobbit(() => "Bilbo Baggins");
}
Would throw the following error, because of the lambda literal argument:
⚠️ Arguments of a constant creation must be constant expressions.
Even though if you passed a reference of a named top-level/static function with the exact same body, it would work.
Dotlin does this for you, so the following code compiles:
class Hobbit const constructor(private val computeName: () -> String)
fun main() {
const val bilbo = Hobbit { "Bilbo Baggins" }
}
And results in:
class Hobbit {
const Hobbit(this._computeName);
final String Function() _computeName;
}
void main() {
const Hobbit bilbo = Hobbit(_$11f4);
}
String _$11f4() {
return 'Bilbo Baggins';
}
As you can see, a named function is generated based on the lambda, and passed to the
const
constructor.
This is only possible if the lambda does not capture local or class closure values. You can use top-level/global values, however.
In Dotlin, you can create const inline
functions, which can be used similarly to const
constructors.
These functions must have a single return with a valid const
expression, and otherwise only contain const
variables.
An example:
class Hobbit const constructor(name: String, age: Int, isCurrentRingbearer: Boolean)
const inline fun bilboBaggings(): Hobbit {
const val fullName = "Bilbo Baggings"
return Hobbit(fullName, age = 111, isCurrentRingbearer = false)
}
fun main() {
const val bilbo = bilboBaggings()
}
The bilboBaggings()
call is inlined, meaning the called constructor
is still const
:
@pragma('vm:always-consider-inlining')
Hobbit bilboBaggings() {
const String fullName = 'Bilbo Baggings';
return Hobbit(fullName, 111, false);
}
void main() {
const Hobbit bilbo = Hobbit('Bilbo Baggings', 111, false);
}
You can also use arguments in const inline
functions:
class Hobbit const constructor(name: String, age: Int, isCurrentRingbearer: Boolean)
const inline fun baggings(firstName: String, age: Int): Hobbit {
const val fullName = "$firstName Baggings"
const val hasRing = firstName == "Frodo"
return Hobbit(fullName, age, isCurrentRingbearer = hasRing)
}
fun main() {
const val frodo = baggings("Frodo", age = 33)
}
Note that if you use arguments in const
variables, they will be made
non-const. This is because const inline
functions can still be called
as non-const. However, if called as const
, arguments are also const
inlined:
@pragma('vm:always-consider-inlining')
Hobbit baggings(String firstName, int age) {
final String fullName = '${firstName} Baggings';
final bool hasRing = firstName == 'Frodo';
return Hobbit(fullName, age, hasRing);
}
void main() {
const Hobbit frodo = Hobbit('Frodo Baggings', 33, 'Frodo' == 'Frodo');
}
Kotlin does not have type literals like Dart does. To accomodate for this, Dotlin
has a typeOf
function, which compiles to a Dart type literal. For example, the following statement:
val myType = typeOf<String>()
Compiles to:
final myType = String;
Existing Dart collections have been dissected into different interfaces based on their mutability, just like in Kotlin.
However, List
has been split into more interfaces, to represent all List
kinds that exist in Dart runtime using types.
Dotlin's Iterable
is mapped directly to Dart's Iterable
. This means that unlike in Kotlin,
Iterable
s are lazy.
The Iterable
class is significantly larger because Dart's Iterable
contains a
lot of methods. However, they've been renamed to match Kotlin conventions, some examples:
Dart | Kotlin |
---|---|
where |
filter |
whereType |
filterIsInstance |
expand |
flatMap |
every |
all |
skip |
drop |
Represents any type of collection of elements. It provides a common interface for
List
and `Set, which in Dart don't have a common interface.
Note
Runtime type checks work:List
s andSet
s are consideredCollection
s at runtime.
Represents any kind of mutable collection of elements. "Mutable" specifically means growable in Dart terms, meaning elements can be added and removed.
Note
Runtime type checks work: DartList
s andSet
s are consideredMutableCollection
s, only if they are actually mutable. Examples (Dart):[1, 2, 3] is MutableCollection<int> == trueList.unmodifiable([1, 2, 3]) is MutableCollection<int> == falseThese type checks don't work as Dart code as-is, but are compiled specially when writing a similar expression in Dotlin.
Dart: List
A read-only interface that represents any kind of Dart's List
s. Mutating methods can be
accessed through subtypes.
Dart: List.unmodifiable
, const [..]
An immutable list. Same interface as List
, but guaranteed to be immutable.
Note
Runtime type checks work: DartList
s are consideredImmutableList
s, only if they are actually immutable. Examples (Dart):const [1, 2, 3] is ImmutableList<int> == trueList.unmodifiable([1, 2, 3]) is ImmutableList<int> == true[1, 2, 3] is ImmutableList<int> == false
Dart: List
(growable: true|false
)
An interface that supports changing elements (list[0] = "abc"
), but not adding or removing elements. This
interface represents both FixedSizeList
s and MutableList
s, since they are both writeable.
Note
Runtime type checks work: DartList
s are consideredWriteableList
s, only if they are actually writeable. Examples (Dart):[1, 2, 3] is WriteableList<int> == trueList.of([1, 2, 3], growable: false) is WriteableList<int> == trueList.unmodifiable([1, 2, 3]) is WriteableList<int> == false
Dart: List
(growable: false
)
An interface that represents writeable fixed-length Dart List
s, also known as arrays. Elements can
be changed (array[0] = "abc"
), but not be added or removed. Any other operation that would change
the size of the list is also not possible.
The difference between this interface and WriteableList
is that WriteableList
represents
any list whose elements can be changed, which also includes MutableList
s.
Note
Runtime type checks work: DartList
s are consideredFixedSizeList
s, only if they are actually writeable. Examples (Dart):List.of([1, 2, 3], growable: false) is FixedSizeList<int> == true[1, 2, 3] is FixedSizeList<int> == falseList.unmodifiable([1, 2, 3]) is FixedSizeList<int> == false
Dart: List
(growable: true
)
An interface that represents growable Dart List
s. Elements can be changed, added and removed.
Note
Runtime type checks work: DartList
s are consideredMutableList
s, only if they are actually mutable (writeable & growable). Examples (Dart):[1, 2, 3] is MutableList<int> == trueList.of([1, 2, 3], growable: false) is MutableList<int> == falseList.unmodifiable([1, 2, 3]) is MutableList<int> == false
Dart: Set
A read-only interface that represents any kind of Dart's Set
s. Mutating methods can be
accessed through MutableSet
.
Dart: Set.unmodifiable
, const {..}
An immutable set. Same interface as Set
, but guaranteed to be immutable.
Note
Runtime type checks work: DartSet
s are consideredImmutableSet
s, only if they are actually immutable. Examples (Dart):const {1, 2, 3} is ImmutableSet<int> == trueSet.unmodifiable({1, 2, 3}) is ImmutableSet<int> == true{1, 2, 3} is ImmutableSet<int> == false
Dart: Set
({..}
)
An interface that represents growable Dart Set
s. Elements can be changed, added and removed.
Note
Runtime type checks work: DartSet
s are consideredMutableSet
s, only if they are actually mutable. Examples (Dart):{1, 2, 3} is MutableSet<int> == trueSet.unmodifiable({1, 2, 3}) is MutableSet<int> == false
Dotlin, at this point in time, should not be used for any production projects. If you want to try it out, clone the repo and you can then build it with
./gradlew build distZip
Then you can find Dotlin in build/distributions/dotlin-<version>.zip
.
In there, there's a bin/dotlin
executable you can try out.
Since the project is at an early stage, a lot is still changing and therefore — for now — code contributions are not encouraged. However, in the future when Dotlin is in a more stable state, this will definitely change.
When code contributions are encouraged, you are required to sign off all of your commits:
My commit message
Signed-off-by: Jan Jansen <jan@jansen.dev>
By contributing and signing off your commits, you agree to the Developer Certificate of Origin (DCO), which you can read here.
For now however, it is encouraged to try Dotlin out, and if you notice anything odd, or want to request a feature/improvement, to create an issue.
Dotlin itself is licensed under the AGPL.
Note that this does not apply to code generated by Dotlin. Code generated by Dotlin can be used in projects of any license.
All libraries used by consumers (e.g. the Kotlin standard library implementation, the Dart core Kotlin definitions) are licensed under the Apache 2.0.
The Dotlin logo (docs/assets/dotlin.png
) is licensed under CC BY-NC-ND 4.0.
Dotlin is not associated with JetBrains or the Kotlin Foundation.