A bunch of Kotlin Examples for learning
Well well, fancy seeing you here. You ready to learn what Kotlin
is all about?
Here's a basic study guide subject to change:
- Learn about Kotlin
data class
es - What makes these unique compared to Java?
- What can we use when calling Kotlin from Java?
- Run the existing unit tests for
PersonTest.kt
- Implement your own
data class
- Call
toString
on yourdata class
and print results
- Call
- Implement your own Kotlin Unit test
- Implement your own Java Unit test that calls Kotlin
- Construct your class using Named constructor arguments
- Create a function with Nullable type as a parameter in a test
- Call a method on this object with a nullable type
- What did you have to do?
- Try calling the function with null
- Try calling with an actual object
- What happens? (Set break points or
print()
to see)
- Understand what Kotlin data classes do automatically for us
- Understand how to call Kotlin data classes from Java (don’t worry, just works)
- Understand named constructor parameters and what that means for Good Ol’ Builder patterns
- Understand what Non-Nullable and Nullable types are
- Learn additional basic Kotlin control structures and how they differ from Java
- Examples of converting Optionals to type-safe accessors and null coalescing operators
- Examples of converting streams to Kotlin types
- Convert IfElseConversion class to Kotlin. Run tests.
- Convert WhenConversion class to Kotlin. Run tests and implement missing methods.
- Convert OptionalConversion class to Kotlin. Run tests and implement missing methods.
- Convert StreamConversion class to Kotlin. Run tests and implement missing methods.
- Beginning understanding of "when"
- Understand how if/else differs from Java
- Understand how to convert Optionals to nullable types in Kotlin
- Understand how to convert Streams to Kotlin collections and when to use
asSequence()
- Learn about extension functions
- Learn about basic destructuring
- Create a basic extension function for counting odd numbers in a list,
countOdd()
- Create a more challenging extension function for creating a map from a
List
,toChunkedMap()
- Print out
petMap
using destructuring - Use destructing on
Animal
class
- Get all existing unit tests to pass
- See how easy it is to extend existing Java library classes, even without needing the source code
- Utilize the
stream
like conversion skills from Lesson 2. - Exposure to the different
for
loop and range syntax in Kotlin (seen in Unit test) - Exposure to using
to
infix operator for generatingPair
s while constructing aMap
- Learn about Scoping functions
Scoping functions are the bread and butter of Kotlin
. You'll see these everywhere peppered in code bases and for
GOOD reason! They can really simplify and add flexibility to your code base.
- They provide an easy way to chain together calls and avoid having to repeat variable names.
- Many times they avoid having to declare extra variables
- Reduce lines of code to maintain
- Clarify intent
- Easier to understand with less code clutter
- Provide all the conveniences of chaining calls together
- Formally called
Monads
- Formally called
These functions are
run
with
T.run
T.let
T.also
T.apply
Note T.
just means you call the extension function on an object, e.g., T.also
used as: object.also{ process(it)}
Scoping functions are handy ways to reduce code when processing objects. They are very similar and there is quite a bit of overlap on uses. As such there are many solutions to the same problem.
For example you could construct an object and pass to a processing function as:
class Level {
///... in some method. Level contains Level.checkCollisions(Mario)
// Mario class has a Int member called "coins" and public accessors for "var coins: Int"
Mario(powerUp = PowerUp.MUSHROOM)
.also {
it.coins = 10
checkCollisions(it)
}
Here are some other ways:
Mario(powerUp = PowerUp.MUSHROOM)
.apply{
coins = 10
checkCollisions(this)}
Mario(powerUp = PowerUp.MUSHROOM)
.let {
it.coins = 10
checkCollisions(it)
}
Mario(powerUp = PowerUp.MUSHROOM)
.run {
coins = 10
checkCollisions(this)
}
with(Mario(powerUp = PowerUp.MUSHROOM)) {
coins = 10
checkCollisions(this)
}
As a reference this is what each is doing:
Function | Type | Passed as | Returns |
---|---|---|---|
also | Extension | it | Same |
apply | Extension | this | Same |
let | Extension | it | Last Line in Block |
run | Extension | this | Last Line in Block |
with | Stand alone | this | Last Line in Block |
Well that depends on your intent of course but let's just say:
- We aren't returning
Mario
and - just running that operation
checkCollisions
Given these conditions I'd avoid T.let
, run
, T.run
and with
,
- They all return the last line of the block
- We should probably only use these functions for
mapping
similar toJava
'sOptional.map
,Stream.map
etc. - This can avoid any confusion/mistakes while chaining calls together
- Immutable lambda chaining, so to speak
let
is great for mapping types- Especially in nullable map chaining (like
Java
'sOptional.map
)
- Especially in nullable map chaining (like
// Param is a nullable type Type?
someObject.param
?.let{
mutate(it)
}
// Now output from `mutate(it)` is used
?.let {
mutateAgain(it)
}
// Maybe you change some mutable state from `mutateAgain(it)` here
?.apply {
state = "OK"
}
Note the state = "OK"
only executes if all the other operations succeed without returning null
and
if param
isn't null.
-
run
seems to be a rare case where you want to scope the object asthis
and possibly map to a different value after processing.someObject.run { someListInObject.forEach {print(it)} mapToSomething(someObject) }
- I find it clearer to do these in 2 atomic steps for readability e.g.
someObject.apply{ /*...*/} .let { /* map stuff */}
- I find it clearer to do these in 2 atomic steps for readability e.g.
-
with
is the same asrun
only the syntax is differentwith(someObject) { // set some internal state // Return something else }
- I'd favor
apply
and not mutate state, or do as suggested with run and use.apply{}.let{}
otherwise
- I'd favor
See Coroutines