Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

0 to 100% Kotlin @ Trafi

0 to 100% Kotlin @ Trafi

Tools, patterns and benefits that we discovered migrating to a 100% Kotlin Android codebase at Trafi.

Kotlin's features that make our team more productive:
- Conciseness maintains focus
- Extension functions keep logic in scope
- Sealed class hierarchies express state
- Functional constructs encourage better code
- Explicit mutability forces design
- Explicit nullability forces design

Presented at GDG Vilnius.
https://www.meetup.com/GDG-Vilnius/events/252862364/

Avatar for Justas Medeišis

Justas Medeišis

October 11, 2018
Tweet

More Decks by Justas Medeišis

Other Decks in Programming

Transcript

  1. public class Ticket { public String id; public String name;

    @Nullable public Date endTime; public Ticket(String id, String name, @Nullable Date endTime) { this.id = id; this.name = name; this.endTime = endTime; } }
  2. public class Ticket { @SerializedName("Id") public String id; @SerializedName("Name") public

    String name; @SerializedName("EndTime") @Nullable public Date endTime; public Ticket(String id, String name, @Nullable Date endTime) { this.id = id; this.name = name; this.endTime = endTime; } }
  3. public class Ticket { @SerializedName("Id") public String id; @SerializedName("Name") public

    String name; @SerializedName("EndTime") @Nullable public Date endTime; public Ticket(String id, String name, @Nullable Date endTime) { this.id = id; this.name = name; this.endTime = endTime; } }
  4. public class Ticket { @SerializedName("Id") public String id; @SerializedName("Name") public

    String name; @SerializedName("EndTime") @Nullable public Date endTime; public Ticket(String id, String name, @Nullable Date endTime) { this.id = id; this.name = name; this.endTime = endTime; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Ticket ticket = (Ticket) o; if (!id.equals(ticket.id)) return false; if (!name.equals(ticket.name)) return false;
  5. // .. public boolean equals(Object o) { if (this ==

    o) return true; if (o == null || getClass() != o.getClass()) return false; Ticket ticket = (Ticket) o; if (!id.equals(ticket.id)) return false; if (!name.equals(ticket.name)) return false; return endTime != null ? endTime.equals(ticket.endTime) : ticket.endTime == null; } @Override public int hashCode() { int result = id.hashCode(); result = 31 * result + name.hashCode(); result = 31 * result + (endTime != null ? endTime.hashCode() : 0); return result; } }
  6. public class Ticket { @SerializedName("Id") public String id; @SerializedName("Name") public

    String name; @SerializedName("EndTime") @Nullable public Date endTime; public Ticket(String id, String name, @Nullable Date endTime) { this.id = id; this.name = name; this.endTime = endTime; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Ticket ticket = (Ticket) o; if (!id.equals(ticket.id)) return false; if (!name.equals(ticket.name)) return false;
  7. public class Ticket implements Parcelable { @SerializedName("Id") public String id;

    @SerializedName("Name") public String name; @SerializedName("EndTime") @Nullable public Date endTime; public Ticket(String id, String name, @Nullable Date endTime) { this.id = id; this.name = name; this.endTime = endTime; } protected Ticket(Parcel in) { id = in.readString(); name = in.readString(); if (in.readByte() == 0) { endTime = null; } else { endTime = new Date(in.readLong()); } }
  8. // .. protected Ticket(Parcel in) { id = in.readString(); name

    = in.readString(); if (in.readByte() == 0) { endTime = null; } else { endTime = new Date(in.readLong()); } } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); dest.writeString(name); if (endTime == null) { dest.writeByte((byte) 0); } else { dest.writeByte((byte) 1); dest.writeLong(endTime.getTime()); }
  9. // .. @Override public void writeToParcel(Parcel dest, int flags) {

    dest.writeString(id); dest.writeString(name); if (endTime == null) { dest.writeByte((byte) 0); } else { dest.writeByte((byte) 1); dest.writeLong(endTime.getTime()); } } @Override public int describeContents() { return 0; } public static final Creator<Ticket> CREATOR = new Creator<Ticket>() { @Override public Ticket createFromParcel(Parcel in) {
  10. // .. public static final Creator<Ticket> CREATOR = new Creator<Ticket>()

    { @Override public Ticket createFromParcel(Parcel in) { return new Ticket(in); } @Override public Ticket[] newArray(int size) { return new Ticket[size]; } }; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Ticket ticket = (Ticket) o; if (!id.equals(ticket.id)) return false;
  11. // .. public boolean equals(Object o) { if (this ==

    o) return true; if (o == null || getClass() != o.getClass()) return false; Ticket ticket = (Ticket) o; if (!id.equals(ticket.id)) return false; if (!name.equals(ticket.name)) return false; return endTime != null ? endTime.equals(ticket.endTime) : ticket.endTime == null; } @Override public int hashCode() { int result = id.hashCode(); result = 31 * result + name.hashCode(); result = 31 * result + (endTime != null ? endTime.hashCode() : 0); return result; } }
  12. public class Ticket implements Parcelable { @SerializedName("Id") public String id;

    @SerializedName("Name") public String name; @SerializedName("EndTime") @Nullable public Date endTime; public Ticket(String id, String name, @Nullable Date endTime) { this.id = id; this.name = name; this.endTime = endTime; } protected Ticket(Parcel in) { id = in.readString(); name = in.readString(); if (in.readByte() == 0) { endTime = null; } else { endTime = new Date(in.readLong()); } }
  13. @AutoValue public abstract class Ticket { public abstract String getId();

    public abstract String getName(); @Nullable public abstract Date getEndTime(); public static Ticket create(String id, String name, @Nullable Date endTime) { return new AutoValue_Ticket(id, name, endTime); } }
  14. @AutoValue public abstract class Ticket { @SerializedName("Id") public abstract String

    getId(); @SerializedName("Name") public abstract String getName(); @Nullable @SerializedName("EndTime") public abstract Date getEndTime(); public static Ticket create(String id, String name, @Nullable Date endTime) { return new AutoValue_Ticket(id, name, endTime); } }
  15. @AutoValue public abstract class Ticket { @SerializedName("Id") public abstract String

    getId(); @SerializedName("Name") public abstract String getName(); @Nullable @SerializedName("EndTime") public abstract Date getEndTime(); public static Ticket create(String id, String name, @Nullable Date endTime) { return new AutoValue_Ticket(id, name, endTime); } }
  16. @AutoValue public abstract class Ticket { @SerializedName("Id") public abstract String

    getId(); @SerializedName("Name") public abstract String getName(); @Nullable @SerializedName("EndTime") public abstract Date getEndTime(); public static Ticket create(String id, String name, @Nullable Date endTime) { return new AutoValue_Ticket(id, name, endTime); } public static TypeAdapter<ActiveTicket> typeAdapter(Gson gson) { return new AutoValue_Ticket.GsonTypeAdapter(gson); } }
  17. @AutoValue public abstract class Ticket { @SerializedName("Id") public abstract String

    getId(); @SerializedName("Name") public abstract String getName(); @Nullable @SerializedName("EndTime") public abstract Date getEndTime(); public static Ticket create(String id, String name, @Nullable Date endTime) { return new AutoValue_Ticket(id, name, endTime); } public static TypeAdapter<ActiveTicket> typeAdapter(Gson gson) { return new AutoValue_Ticket.GsonTypeAdapter(gson); } }
  18. @AutoValue public abstract class Ticket implements Parcelable { @SerializedName("Id") public

    abstract String getId(); @SerializedName("Name") public abstract String getName(); @Nullable @SerializedName("EndTime") public abstract Date getEndTime(); public static Ticket create(String id, String name, @Nullable Date endTime) { return new AutoValue_Ticket(id, name, endTime); } public static TypeAdapter<ActiveTicket> typeAdapter(Gson gson) { return new AutoValue_Ticket.GsonTypeAdapter(gson); } }
  19. @AutoValue public abstract class Ticket implements Parcelable { @SerializedName("Id") public

    abstract String getId(); @SerializedName("Name") public abstract String getName(); @Nullable @SerializedName("EndTime") public abstract Date getEndTime(); public static Ticket create(String id, String name, @Nullable Date endTime) { return new AutoValue_Ticket(id, name, endTime); } public static TypeAdapter<ActiveTicket> typeAdapter(Gson gson) { return new AutoValue_Ticket.GsonTypeAdapter(gson); } }
  20. @JsonClass(generateAdapter = true) data class Ticket( @Json(name = "Id") val

    id: String, @Json(name = "Name") val name: String, @Json(name = "EndTime") val endTime: Date? = null )
  21. @JsonClass(generateAdapter = true) data class Ticket( @Json(name = "Id") val

    id: String, @Json(name = "Name") val name: String, @Json(name = "EndTime") val endTime: Date? = null )
  22. @JsonClass(generateAdapter = true) data class Ticket( @Json(name = "Id") val

    id: String, @Json(name = "Name") val name: String, @Json(name = "EndTime") val endTime: Date? = null )
  23. @Parcelize @JsonClass(generateAdapter = true) data class Ticket( @Json(name = "Id")

    val id: String, @Json(name = "Name") val name: String, @Json(name = "EndTime") val endTime: Date? = null ) : Parcelable
  24. @Parcelize @JsonClass(generateAdapter = true) data class Ticket( @Json(name = "Id")

    val id: String, @Json(name = "Name") val name: String, @Json(name = "EndTime") val endTime: Date? = null ) : Parcelable
  25. @Parcelize @JsonClass(generateAdapter = true) data class Ticket( @Json(name = "Id")

    val id: String, @Json(name = "Name") val name: String, @Json(name = "EndTime") val endTime: Date? = null ) : Parcelable
  26. val ticket = Ticket("a", "30 min") println(ticket.toString()) // Ticket(id=a, name=30

    min, endTime=null) println(ticket.hashCode()) // -574755854
  27. val ticket = Ticket("a", "30 min") println(ticket.toString()) // Ticket(id=a, name=30

    min, endTime=null) println(ticket.hashCode()) // -574755854 val otherTicket = ticket.copy(endTime = Date(0))
  28. val ticket = Ticket("a", "30 min") println(ticket.toString()) // Ticket(id=a, name=30

    min, endTime=null) println(ticket.hashCode()) // -574755854 val otherTicket = ticket.copy(endTime = Date(0)) println(ticket == otherTicket) // false
  29. val ticket = Ticket("a", "30 min") println(ticket.toString()) // Ticket(id=a, name=30

    min, endTime=null) println(ticket.hashCode()) // -574755854 val otherTicket = ticket.copy(endTime = Date(0)) println(ticket == otherTicket) // false
  30. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) { val isValid: Boolean get() = endTime != null }
  31. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) fun isValid(ticket: Ticket): Boolean { return ticket.endTime != null }
  32. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) fun Ticket.isValid(): Boolean { return ticket.endTime != null }
  33. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) fun Ticket.isValid(): Boolean { return endTime != null }
  34. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) fun Ticket.isValid(): Boolean { return endTime != null }
  35. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) fun Ticket.isValid(): Boolean = endTime != null
  36. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) val Ticket.isValid: Boolean get() = endTime != null
  37. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) private val Ticket.isValid: Boolean get() = endTime != null
  38. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) private val Ticket.isValid: Boolean get() = endTime != null
  39. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) private val Ticket.isValid: Boolean get() = endTime != null
  40. data class Ticket( val id: String, val name: String, val

    endTime: Date? = null ) private val Ticket.isValid: Boolean get() = endTime != null ticket.
  41. class TicketsScreen : Fragment() { // initialization .. // state

    var tickets: List<Ticket> = emptyList() }
  42. class TicketsScreen : Fragment() { // initialization .. // state

    var tickets: List<Ticket> = emptyList() var sortedAscending: Boolean = true }
  43. class TicketsScreen : Fragment() { // initialization .. // state

    var tickets: List<Ticket> = emptyList() var sortedAscending: Boolean = true fun updateTickets() = //.. }
  44. class TicketsScreen : Fragment() { // initialization .. // state

    var tickets: List<Ticket> = emptyList() var sortedAscending: Boolean = true set(value) { field = value updateTickets() } fun updateTickets() = //.. }
  45. class TicketsScreen : Fragment() { // initialization .. // state

    .. // dependency injection @Inject lateinit var ticketService: TicketService }
  46. class TicketsScreen : Fragment() { // initialization .. // state

    .. // dependency injection .. // view inflation .. fun onCreateView(/**/): View? = // .. }
  47. class TicketsScreen : Fragment() { // initialization .. // state

    .. // dependency injection .. // view inflation .. // view binding .. fun onViewCreated(view: View) = // .. }
  48. class TicketsScreen : Fragment() { // initialization .. // state

    .. // dependency injection .. // view inflation .. // view binding .. // lifecycle .. override fun onPause() = //.. }
  49. class TicketsScreen : Fragment() { // initialization .. // state

    .. // dependency injection .. // view inflation .. // view binding .. // lifecycle .. // .. }
  50. data class ViewState( private val tickets: List<Ticket>, private val sort:

    Sort = descending ) enum class Sort { ascending, descending; }
  51. data class ViewState( private val tickets: List<Ticket>, private val sort:

    Sort = descending ) enum class Sort { ascending, descending; }
  52. sealed class Event { class Reloaded(val tickets: List<Ticket>) : Event()

    class TappedTicket(val ticket: Ticket) : Event() object TappedToggleSort : Event() }
  53. sealed class Event { class Reloaded(val tickets: List<Ticket>) : Event()

    class TappedTicket(val ticket: Ticket) : Event() object TappedToggleSort : Event() } val event: Event when (event) { is Reloaded -> // .. is TappedTicket -> // .. TappedToggleSort -> // .. }
  54. class Event { class Reloaded(val tickets: List<Ticket>) : Event() class

    TappedTicket(val ticket: Ticket) : Event() object TappedToggleSort : Event() } val event: Event when (event) { is Reloaded -> // .. is TappedTicket -> // .. TappedToggleSort -> // .. // ← error! }
  55. class Event { class Reloaded(val tickets: List<Ticket>) : Event() class

    TappedTicket(val ticket: Ticket) : Event() object TappedToggleSort : Event() } val event: Event when (event) { is Reloaded -> // .. is TappedTicket -> // .. TappedToggleSort -> // .. else -> throw IllegalStateException("Hm..?") }
  56. sealed class Event { class Reloaded(val tickets: List<Ticket>) : Event()

    class TappedTicket(val ticket: Ticket) : Event() object TappedToggleSort : Event() } val event: Event when (event) { is Reloaded -> // .. is TappedTicket -> // .. TappedToggleSort -> // .. // ← all good! }
  57. sealed class Event { class Reloaded(val tickets: List<Ticket>) : Event()

    class TappedTicket(val ticket: Ticket) : Event() object TappedToggleSort : Event() }
  58. sealed class Event { class Reloaded(val tickets: List<Ticket>) : Event()

    class TappedTicket(val ticket: Ticket) : Event() object TappedToggleSort : Event() } sealed class Effect { data class ShowTicket(val ticket: Ticket) : Effect() }
  59. sealed class Event { class Reloaded(val tickets: List<Ticket>) : Event()

    class TappedTicket(val ticket: Ticket) : Event() object TappedToggleSort : Event() } sealed class Effect { data class ShowTicket(val ticket: Ticket) : Effect() }
  60. data class ViewState( private val tickets: List<Ticket>, private val sort:

    Sort = descending ) sealed class Effect { data class ShowTicket(val ticket: Ticket) : Effect() }
  61. data class ViewState( private val tickets: List<Ticket>, private val sort:

    Sort = descending, private val effect: Effect? = null ) sealed class Effect { data class ShowTicket(val ticket: Ticket) : Effect() }
  62. data class ViewState( private val tickets: List<Ticket>, private val sort:

    Sort = descending, private val effect: Effect? = null ) sealed class Effect { data class ShowTicket(val ticket: Ticket) : Effect() }
  63. data class ViewState( private val tickets: List<Ticket>, private val sort:

    Sort = descending, private val effect: Effect? = null )
  64. data class ViewState( private val tickets: List<Ticket>, private val sort:

    Sort = descending, private val effect: Effect? = null ) { fun reduce(event: Event): State { return when (event) { is Reloaded -> copy(tickets = event.tickets) is TappedTicket -> copy(effect = ShowTicket(event.ticket)) TappedToggleSort -> copy(sort = if (sort == ascending) descending else ascending) } } // ...
  65. data class ViewState( private val tickets: List<Ticket>, private val sort:

    Sort = descending, private val effect: Effect? = null ) { // ... // UI val sortedTickets get() = if (sort == ascending) tickets.sortedBy { it.endTime } else tickets.sortedByDescending { it.endTime } // ... }
  66. data class ViewState( private val tickets: List<Ticket>, private val sort:

    Sort = descending, private val effect: Effect? = null ) { // ... // Flow val showTicket get() = (effect as? Effect.ShowTicket)?.ticket }
  67. val state = ViewState(tickets = emptyList()) val tickets = ticketService.get()

    val newState1 = state.reduce(Event.Reloaded(tickets))
  68. val state = ViewState(tickets = emptyList()) val tickets = ticketService.get()

    val newState1 = state.reduce(Event.Reloaded(tickets)) val newState2 = newState1.reduce(Event.TappedToggleSort) // ...
  69. object UserStore { var user: User? = null } navToProfileScreen(UserStore)

    class UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user title.text = user.name } }
  70. object UserStore { var user: User? = null } navToProfileScreen(UserStore)

    class UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user title.text = user.name // ← error! } }
  71. object UserStore { var user: User? = null } navToProfileScreen(UserStore)

    class UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user title.text = user?.name ?: "Something?" } }
  72. object UserStore { var user: User? = null } navToProfileScreen(UserStore)

    class UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user ?: throw IllegalStateException("How?") title.text = user.name } }
  73. object UserStore { var user: User? = null } navToProfileScreen(UserStore)

    class UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user ?: throw IllegalStateException("How?") title.text = user.name } }
  74. object UserStore { var user: User? = null } navToProfileScreen(UserStore)

    class UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user ?: throw IllegalStateException("How?") title.text = user.name } }
  75. object UserStore { var user: User? = null } UserStore.user?.let

    { navToProfileScreen(it) } ?: navToLoginScreen() class UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user ?: throw IllegalStateException("How?") title.text = user.name } }
  76. object UserStore { var user: User? = null } UserStore.user?.let

    { navToProfileScreen(it) } ?: navToLoginScreen() class UserProfileScreen(val user: User) { fun bindView() { title.text = user.name } }
  77. object UserStore { var user: User? = null } UserStore.user?.let

    { navToProfileScreen(it) } ?: navToLoginScreen() class UserProfileScreen(val user: User) { fun bindView() { title.text = user.name } }
  78. val user: User // we’re in control // vs val

    user: User? // ¯\_(ツ)_/¯
  79. object UserStore { var user: User? = null } class

    UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user ?: throw IllegalStateException("How?") title.text = user.name } }
  80. object UserStore { var user: User? = null } class

    UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user ?: throw IllegalStateException("How?") title.text = user.name } fun logOut() { store.user = null } }
  81. object UserStore { var user: User? = null } class

    UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user ?: throw IllegalStateException("How?") title.text = user.name } fun logOut() { store.user = null clearUserDataCache() } }
  82. object UserStore { var user: User? = null } class

    UserProfileScreen(val store: UserStore) { fun bindView() { val user = store.user ?: throw IllegalStateException("How?") title.text = user.name } fun logOut() { store.user = null clearUserDataCache() // then send a notification, oh and take out the trash }
  83. object UserStore { var user: User? = null } class

    UserProfileScreen(val user: User) { fun bindView() { title.text = user.name } }
  84. object UserStore { var user: User? = null } class

    UserProfileScreen(val user: User) { fun bindView() { title.text = user.name } fun logOut() { user = null } }
  85. object UserStore { var user: User? = null } class

    UserProfileScreen(val user: User) { fun bindView() { title.text = user.name } fun logOut() { user = null // ← error! } }
  86. object UserStore { var user: User? = null } class

    UserProfileScreen(val user: User) { fun bindView() { title.text = user.name } fun logOut() { user = null // ← error! } }
  87. object UserStore { var user: User? = null } class

    UserProfileScreen(val user: User, val userManager: /**/) { fun bindView() { title.text = user.name } fun logOut() { user = null // ← error! } }
  88. object UserStore { var user: User? = null } class

    UserProfileScreen(val user: User, val userManager: /**/) { fun bindView() { title.text = user.name } fun logOut() { user = null // ← error! } }
  89. object UserStore { var user: User? = null } class

    UserProfileScreen(val user: User, val userManager: /**/) { fun bindView() { title.text = user.name } fun logOut() { userManager.logOut() } }
  90. object UserStore { var user: User? = null } class

    UserProfileScreen(val user: User, val userManager: /**/) { fun bindView() { title.text = user.name } fun logOut() = userManager.logOut() }
  91. object UserStore { var user: User? = null } class

    UserProfileScreen(val user: User, val userManager: /**/) { fun bindView() { title.text = user.name } fun logOut() = userManager.logOut() }
  92. var user: User // search for assignments // vs val

    user: User // search for usages
  93. Why JetBrains Needs Kotlin by Dmitry Jemerov “ First and

    foremost, it’s about our own productivity.” Story
  94. Why JetBrains Needs Kotlin by Dmitry Jemerov “ First and

    foremost, it’s about our own productivity.”
  95. Assumptions about code are not enforceable and age badly over

    time Observation Code base changes Other people work on it
  96. Assumptions about code are not enforceable and age badly over

    time Observation Code base changes Other people work on it Product requirements change
  97. Assumptions about code are not enforceable and age badly over

    time Observation Code base changes Other people work on it Product requirements change Assumptions break
  98. Conciseness maintains focus Extension functions keep logic in scope Sealed

    class hierarchies express state Functional constructs encourage better code
  99. Conciseness maintains focus Extension functions keep logic in scope Sealed

    class hierarchies express state Functional constructs encourage better code Explicit mutability forces design
  100. Conciseness maintains focus Extension functions keep logic in scope Sealed

    class hierarchies express state Functional constructs encourage better code Explicit mutability forces design Explicit nullability forces design
  101. typealias TicketId = String data class Ticket( val id: TicketId,

    val name: String, val endTime: Date? = null )
  102. typealias TicketId = String data class Ticket( val id: TicketId,

    val name: String, val endTime: Date? = null ) val ticket = Ticket(id = "oops", name = "Ticket")
  103. typealias TicketId = String data class Ticket( val id: TicketId,

    val name: String, val endTime: Date? = null ) val ticket = Ticket(id = "oops", name = "Ticket")
  104. inline class TicketId(val id: String) data class Ticket( val id:

    TicketId, val name: String, val endTime: Date? = null ) val ticket = Ticket(id = "oops", name = "Ticket")
  105. inline class TicketId(val id: String) data class Ticket( val id:

    TicketId, val name: String, val endTime: Date? = null ) val ticket = Ticket(id = "oops", name = "Ticket") // ← error!
  106. inline class TicketId(val id: String) data class Ticket( val id:

    TicketId, val name: String, val endTime: Date? = null ) val ticket = Ticket(id = TicketId("a"), name = "Ticket") // ← ok!
  107. GlobalScope.launch { delay(1000L) println("C# backend developers") } println("Trafi is hiring")

    Thread.sleep(2000L) // Trafi is hiring // C# backend developers