Page
1
(This page has no text content)
Page
2
Advanced Kotlin MarcinMoskała This book is for sale at http://leanpub.com/advanced_kotlin This versionwas published on 2023-12-07 This is a Leanpub book. Leanpub empowers authors and publisherswith the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools andmany iterations to get reader feedback, pivot until you have the right book and build traction once you do. © 2022 - 2023MarcinMoskała
Page
3
For thosewho helped and inspiredme onmy path of learning programming craftsmanship, especially Kamil Kędzia, Rafał Trzeciak, BartekWilczyński,MichałMalanowicz, andMateusz Mikulski.
Page
4
Contents Introduction 1 Who is this book for? 1 Thatwill be covered? 1 The structure of the book 3 TheKotlin for Developers series 3 Conventions 4 Code conventions 4 Acknowledgments 6 Generic variancemodifiers 8 List variance 10 Consumer variance 12 Function types 15 The Covariant NothingObject 17 The Covariant Nothing Class 23 Variancemodifier limitations 25 UnsafeVariance annotation 31 Variancemodifier positions 33 Star projection 35 Summary 35 Exercise: Generic types usage 37 Exercise: Generic Response 38 Exercise: Generic Consumer 39 Interface delegation 42 The delegation pattern 42 Delegation and inheritance 43 Kotlin interface delegation support 45 Wrapper classes 49 The decorator pattern 50 Intersection types 53
Page
5
CONTENTS Limitations 55 Conflicting elements fromparents 56 Summary 57 Exercise: ApplicationScope 58 Property delegation 59 Howproperty delegationworks 60 Other getValue and setValue parameters 64 Implementing a customproperty delegate 67 Provide a delegate 70 Property delegates in Kotlin stdlib 73 The notNull delegate 73 Exercise: Lateinit delegate 75 The lazy delegate 76 Exercise: Blog Post Properties 88 The observable delegate 89 The vetoable delegate 94 Amap as a delegate 96 Review of how variables work 98 Summary 105 Exercise:Mutable lazy delegate 105 Kotlin Contracts 107 Themeaning of a contract 108 Howmany times dowe invoke a function froman argument? 109 Implications of the fact that a function has re- turned a value 113 Using contracts in practice 115 Summary 116 Exercise: Coroutine timemeasurement 117 Java interoperability 119 Nullable types 119 Kotlin typemapping 122 JVMprimitives 123 Collection types 125 Annotation targets 129 Static elements 135 JvmField 137 Using Java accessors in Kotlin 139 JvmName 140
Page
6
CONTENTS JvmMultifileClass 144 JvmOverloads 145 Unit 148 Function types and function interfaces 149 Tricky names 152 Throws 153 JvmRecord 156 Summary 157 Exercise: Adjust Kotlin for Java usage 157 Using KotlinMultiplatform 161 Multiplatformmodule configuration 161 Expect and actual elements 164 Possibilities 167 Multiplatform libraries 171 Amultiplatformmobile application 179 Summary 185 Exercise:MultiplatformLocalDateTime 185 JavaScript interoperability 187 Setting up a project 188 Using libraries available for Kotlin/JS 190 Using Kotlin/JS 190 Building and linking a package 193 Distributing a package to npm 195 Exposing objects 195 Exposing Flow and StateFlow 200 Adding npmdependencies 205 Frameworks and libraries for Kotlin/JS 207 JavaScript andKotlin/JS limitations 207 Summary 209 Exercise:Migrating a Kotlin/JVMproject to KMP 209 Reflection 211 Hierarchy of classes 213 Function references 214 Parameter references 222 Property references 224 Class reference 227 Serialization example 236 Referencing types 242 Type reflection example: Randomvalue 246
Page
7
CONTENTS Kotlin and Java reflection 252 Breaking encapsulation 253 Summary 254 Exercise: Function caller 254 Exercise: Object serialization to JSON 256 Exercise: Object serialization to XML 258 Exercise: DSL-based dependency injection library 262 Annotation processing 265 Your first annotation processor 265 Hiding generated classes 278 Summary 281 Exercise: Annotation Processing 281 Kotlin Symbol Processing 283 Your first KSP processor 284 Testing KSP 293 Dependencies and incremental processing 296 Multiple rounds processing 302 Using KSP onmultiplatformprojects 305 Summary 306 Exercise: Kotlin Symbol Processing 306 Kotlin Compiler Plugins 308 Compiler frontend and backend 308 Compiler extensions 310 Popular compiler plugins 314 Making all classes open 315 Changing a type 317 Generate functionwrappers 319 Example plugin implementations 320 Summary 321 Static Code Analysers 322 What are Static Analysers? 323 Types of analysers 326 Kotlin Code Analysers 330 Setting up detekt 333 Writing your first detekt Rule 338 Conclusion 345 Ending 346 Exercise solutions 347
Page
8
CONTENTS 1 Introduction Youcanbeadeveloper - evenagoodone -withoutunderstand- ing the topics explained in this book, but at somepoint, you’ll need it. You’re likely already using tools made using features described in this book every day, such as libraries based on annotation processing or compiler plugins, classes that use variance modifiers, functions with contracts, or property delegates, but do you understand these features? Would you be able to implement similar tools yourself? Would you be able to analyze and debug them? This book will make all this possible for you. It focuses exclusively on the most advanced Kotlin topics, which are often not well understood even by senior Kotlin developers. It should equip youwith the knowl- edge you need and show you possibilities you never before imagined. I hope you enjoy it asmuch as I enjoyedwriting it. Who is this book for? This book is for experienced Kotlin developers. I assume that readers understand topics like function types and lambda ex- pressions, collection processing, creation and usage of DSLs, and essential Kotlin types like Any? and Nothing. If you don’t have enough experience, I recommend the previous books from this series:Kotlin Essentials and Functional Kotlin. That will be covered? The chapter titles explainwhatwill be covered quitewell, but here is amore detailed list: • Generic variancemodifiers • The Covariant NothingObject pattern • Generic variancemodifier limitations • Interface delegation • Implementing customproperty delegates • Property delegates fromKotlin stdlib
Page
9
CONTENTS 2 • Kotlin Contracts • Kotlin and Java typemapping • Annotations for Kotlin and Java interoperability • Multiplatform development structure, concepts and possibilities • Implementingmultiplatform libraries • Implementing Android and iOS applications with sharedmodules • Essentials of Kotlin/JS • Reflecting Kotlin elements • Reflecting Kotlin types • Implementing customAnnotation Processors • Implementing customKotlin Symbol Processors • KSP incremental compilation and multiple-round pro- cessing • Defining Compiler Plugins • Core Static Analysis concepts • Overview of Kotlin static analyzers • Defining customDetekt rules This book is full of example projects, including: • A type-safe task update class using the Covariant Noth- ing Object pattern (Generic variancemodifiers chapter) • Logging a property delegate (Property delegation chap- ter) • An object serializer (Reflection chapter) • A random value generator for generic types (Reflection chapter) • An annotation processor that generates an interface for a class (Annotation Processing chapter). • A Kotlin symbol processor that generates an interface for a class (Kotlin Symbol Processing chapter). • ADetekt rule that finds System.out.println usage.
Page
10
CONTENTS 3 The structure of the book I decided that the chapters should have a flat structure; how- ever, if you read the book from the beginning, you might notice that the order is not accidental. The first four chap- ters, Generic variance modifiers, Interface Delegation, Property delegation and Kotlin Contracts, explain Kotlin features that are commonly used yet are not well understood by most de- velopers. The next three chapters, Java interoperability,Using Kotlin Multiplatform and JavaScript interoperability, describe the details of using different flavors of Kotlin. The rest of the book, namely the chapters Reflection, Annotation Processing, Kotlin Symbol Processing, Kotlin Compiler Plugins and Static Code Analysers, are about meta-programming, and each of these chapters builds on the previous ones. The Kotlin for Developers series This book is a part of a series of books calledKotlin forDevelop- ers, which includes the following books: • Kotlin Essentials, which covers all the basic Kotlin fea- tures. • Functional Kotlin, which is dedicated to functional Kotlin features, including function types, lambda expressions, collection processing, DSLs, and scope functions. • Kotlin Coroutines: Deep Dive, which covers all the Kotlin Coroutines features, includinghowtouse and test them, using flow, the best practices, and the most common mistakes. • Advanced Kotlin, which is dedicated to advanced Kotlin features, including generic variance modifiers, delega- tion, multiplatform programming, annotation process- ing, KSP and compiler plugins. • Effective Kotlin: Best Practices, which is dedicated to the best practices of Kotlin programming.
Page
11
CONTENTS 4 Do not worry, you do not need to read the previous books in the series to understand this one. However, if you are interested in learning more about Kotlin, I recommend con- sidering the other books from this series. Conventions When I refer to a concrete element from code, I will use code- font. To name a concept, I will capitalize the word. To refer- ence an arbitrary element of some type, I will not capitalize theword. For example: • Flow is a type or an interface, so it’s printed in code-font (as in “Function needs to return Flow”), • Flowrepresents a concept, so it is capitalized (as in “This explains the essential difference between Channel and Flow”), • a flow is an instance, like a list or a set, which iswhy it is not capitalized (“Every flowconsists of a fewelements”). Another example: List refers concretely to a list interface or type (“The type of l is List”), while List represents a concept, and a list is one ofmany lists (the list variable holds a list). Code conventions Most of the presented snippets are executable code with no import statements. In the online version of this book on the Kt. Academy website, most snippets can be executed so readers can playwith the code. Snippet results are presented using the println function. The result will often be placed in comments after the statement that prints it.
Page
12
CONTENTS 5 import kotlin.reflect.KType import kotlin.reflect.typeOf fun main() { val t1: KType = typeOf<Int?>() println(t1) // kotlin.Int? val t2: KType = typeOf<List<Int?>>() println(t2) // kotlin.collections.List<kotlin.Int?> val t3: KType = typeOf<() -> Map<Int, Char?>>() println(t3) // () -> kotlin.collections.Map<kotlin.Int, kotlin.Char?> } In some snippets, you might notice strange formatting. This is because the line length in this book is only 62 characters, so I adjusted the formatting to fit the page width. This is, for instance,why the result of println(t3) in the above example is located on the next line. Sometimes, some parts of code or results are shortened with .... In such cases, you can read it as “there should be more here, but the author decided to omit it”. class A { val b by lazy { B() } val c by lazy { C() } val d by lazy { D() } }
Page
13
CONTENTS 6 Acknowledgments Owen Griffiths has been developing soft- ware since the mid-1990s and remembers theproductivityof languages suchasClip- per and Borland Delphi. Since 2001, he’s focused on web, server-based Java, and the open-source revolution. With many years of commercial Java experience, he picked up Kotlin in early 2015. After taking detours into Clo- jure and Scala, he - like Goldilocks - thinks Kotlin is just right and tastes the best. Owen enthusiastically helps Kotlin developers continue to succeed. Nicola Corti is a Google Developer Ex- pert for Kotlin. He’s been working with this language since before version 1.0 and is the maintainer of several open- source librariesandtools formobiledevel- opers (Detekt, Chucker, AppIntro). He’s currently working in the React Native core team at Meta, building one of the most popular cross- platform mobile frameworks, and he’s an active member of the developer community. His involvement goes from speak- ing at international conferences to being a member of CFP committees and supporting developer communities across Europe. Inhis free time,he also lovesbaking, podcasting, and running. Matthias Schenk started his career with Java over ten years ago, mainly in the Spring/Spring Boot Ecosystem. Eighteen monthsago,heswitchedtoKotlinandhas since become a big fan of working with native Kotlin frameworks like Koin, Ktor, and Exposed.
Page
14
CONTENTS 7 Jacek Kotorowicz graduated from UMCS and is now an An- droid developer based in Lublin. Hewrote hisMaster’s thesis in C++ in Vim and LaTeX. Later, he found himself in a love- hate relationship with JVM languages and the Android plat- form.He first usedKotlin (or, at least, tried to) before version 1.0. He’s still learning howNOT to be a perfectionist and how to find time for learning and hobbies. EndreDeak is a software architect buildingAI infrastructure at Disco, a market-leading legal tech company. He has 15 yearsof experiencebuildingcomplexscalable systems,andhe thinks Kotlin is one of the best programming languages ever created. I would also like to thankMichael Timberlake, our language reviewer, for his excellent corrections to thewhole book.
Page
15
CONTENTS 8 Generic variance modifiers Let’s say that Puppy is a subtype of Dog, and you have a generic Box class to enclose them both. The question is: what is the relation between the Box<Puppy> and Box<Dog> types? In other words, can we use Box<Puppy> where Box<Dog> is expected, or vice versa?To answer these questions,we need to knowwhat the variancemodifier of this class type parameter is¹. When a type parameter has no variancemodifier (no out or in modifier),we say it is invariant and thusexpects anexact type. So, if we have class Box<T>, then there is no relation between Box<Puppy> and Box<Dog>. class Box<T> open class Dog class Puppy : Dog() fun main() { val d: Dog = Puppy() // Puppy is a subtype of Dog val bd: Box<Dog> = Box<Puppy>() // Error: Type mismatch val bp: Box<Puppy> = Box<Dog>() // Error: Type mismatch val bn: Box<Number> = Box<Int>() // Error: Type mismatch val bi: Box<Int> = Box<Number>() // Error: Type mismatch } Variance modifiers determine what the relationship should be between Box<Puppy> and Box<Dog>. When we use the out ¹In this chapter, I assume that youknowwhat a type is and understand the basics of generic classes and functions. As a reminder, the type parameter is a placeholder for a type, e.g., T in class Box<T> or fun a<T>() {}. The type argument is the actual type usedwhen a class is created or a function is called, e.g., Int in Box<Int>() or a<Int>(). A type is not the same as a class. For a class User, there are at least two types: User and User?. For a generic class, there are many types, like Box<Int>, and Box<String>.
Page
16
CONTENTS 9 modifier, we make a covariant type parameter. When A is a subtypeof B, the Box typeparameter is covariant (outmodifier) and the Box<A> type is a subtype of Box<B>. So, in our example, for class Box<out T>, the Box<Puppy> type is a subtype of Box<Dog>. class Box<out T> open class Dog class Puppy : Dog() fun main() { val d: Dog = Puppy() // Puppy is a subtype of Dog val bd: Box<Dog> = Box<Puppy>() // OK val bp: Box<Puppy> = Box<Dog>() // Error: Type mismatch val bn: Box<Number> = Box<Int>() // OK val bi: Box<Int> = Box<Number>() // Error: Type mismatch } When we use the in modifier, we make a contravariant type parameter. When A is a subtype of B and the Box type parame- ter is contravariant (inmodifier), then type Box<B> is a subtype of Box<A>. So, in our example, for class Box<in T> the Box<Dog> type is a subtype of Box<Puppy>. class Box<in T> open class Dog class Puppy : Dog() fun main() { val d: Dog = Puppy() // Puppy is a subtype of Dog val bd: Box<Dog> = Box<Puppy>() // Error: Type mismatch val bp: Box<Puppy> = Box<Dog>() // OK val bn: Box<Number> = Box<Int>() // Error: Type mismatch val bi: Box<Int> = Box<Number>() // OK }
Page
17
CONTENTS 10 These variance modifiers are illustrated in the diagram be- low: At this point, you might be wondering how these variance modifiers are useful. In particular, contravariance might sound strange to you, so letme show you some examples. List variance Let’s consider that you have the type Animal and its subclass Cat. You also have the standalone function petAnimals, which you use to pet all your animals when you get back home. You also have a list of cats that is of type List<Cat>. The question is: can you use your list of cats as an argument to the function petAnimals, which expects a list of animals? interface Animal { fun pet() } class Cat(val name: String) : Animal { override fun pet() { println("$name says Meow") } } fun petAnimals(animals: List<Animal>) { for (animal in animals) { animal.pet()
Page
18
CONTENTS 11 } } fun main() { val cats: List<Cat> = listOf(Cat("Mruczek"), Cat("Puszek")) petAnimals(cats) // Can I do that? } Theanswer isYES.Why?Because inKotlin, the List interface type parameter is covariant, so it has the outmodifier, which is why List<Cat> can be usedwhere List<Animal> is expected. Covariance (out) is a proper variance modifier because List is read-only. Covariance can’t be used for a mutable data structure. The MutableList interface has an invariant type parameter, so it has no variancemodifier.
Page
19
CONTENTS 12 Thus, MutableList<Cat> cannot be used where MutableList<Animal> is expected. There are good reasons for this which we will explore when we discuss the safety of variancemodifiers. For now, I will just show you an example of what might go wrong if MutableList were covariant: we could use MutableList<Cat> where MutableList<Animal> is expected and then use this reference to add Dog to our list of cats. Someonewould be really surprised to find a dog in a list of cats. interface Animal class Cat(val name: String) : Animal class Dog(val name: String) : Animal fun addAnimal(animals: MutableList<Animal>) { animals.add(Dog("Cookie")) } fun main() { val cats: MutableList<Cat> = mutableListOf(Cat("Mruczek"), Cat("Puszek")) addAnimal(cats) // COMPILATION ERROR val cat: Cat = cats.last() // If code would compile, it would break here } This illustrates why covariance, as its name out suggests, is appropriate for types that are only exposed and only go out of an object but never go in. So, covariance should be used for immutable classes. Consumer variance Let’s say that you have a class that can be used to send mes- sages of a certain type.
Page
20
CONTENTS 13 interface Sender<T : Message> { fun send(message: T) } interface Message interface OrderManagerMessage : Message class AddOrder(val order: Order) : OrderManagerMessage class CancelOrder(val orderId: String) : OrderManagerMessage interface InvoiceManagerMessage : Message class MakeInvoice(val order: Order) : OrderManagerMessage Now, you’ve made a class called GeneralSender that is capable of sending any kind of message. The question is: can you use GeneralSender where a class for sending some specific kind of messages is expected? You should be able to! If GeneralSender can send all kinds of messages, it should be able to send specificmessage types aswell. class GeneralSender( serviceUrl: String ) : Sender<Message> { private val connection = makeConnection(serviceUrl) override fun send(message: Message) { connection.send(message.toApi()) } } val orderManagerSender: Sender<OrderManagerMessage> = GeneralSender(ORDER_MANAGER_URL) val invoiceManagerSender: Sender<InvoiceManagerMessage> = GeneralSender(INVOICE_MANAGER_URL) For a sender of any message to be a sender of some specific message type,weneed thesender type tohaveacontravariant parameter, therefore it needs the inmodifier.