This repo is a Kotlin Native playground.
I've followed the official guide to setup the iOS and the Android modules
These are the things that I wanna explore:
- Enums
- Enums with arguments
- Sealed classes
- Objects
- Nullability
- Companion objects
- Generics
- Higher-Order Functions and Lambdas
- DSL
- Inheritance
- Unit tests in the common module
- Import pure kotlin library in the common module
- Distribution as library
- maven for the (android + common) as android library
- cocoapods for the (iOS + common) as framework
Table of contents
Enum definition
enum class SimpleEnum{
FIRST, SECOND, THIRD
}
Into the android module I'm able to do all the common stuff that we are used to do with enums (access to the cases, iterate and count). So we can write a test that looks like
@Test
fun usageOfSimpleEnum() {
assertEquals(3, SimpleEnum.values().size)
SimpleEnum.values().forEach {
println(it)
}
}
Into the ios module I can access to the enum cases, but I don't know how iterate or get the enum size. So we can write a test that looks like
func testUsageOfSimpleEnum() {
print(KotlinLibrarySimpleEnum.first)
print(KotlinLibrarySimpleEnum.second)
print(KotlinLibrarySimpleEnum.third)
}
Enum definition
enum class EnumWithValue(val associatedValue: Int) {
ONE(1),
TWO(2)
}
As in the previous section, even with the enums with argument, we can do everything we expect with an enum. So the test can be:
@Test
fun usageOfEnumWithValue() {
assertEquals(2, EnumWithValue.values().size)
EnumWithValue.values().forEach {
println("$it : ${it.associatedValue}")
}
}
As above, the same "issues" with the enum are still present even with the enums with values. At least we can access the value of the variable:
func testUsageOfEnumWithValue() {
XCTAssert(KotlinLibraryEnumWithValue.one.associatedValue == 1)
XCTAssert(KotlinLibraryEnumWithValue.two.associatedValue == 2)
}
sealed class definition
sealed class SealedClassExample {
class Success(val successMessage: String) : SealedClassExample()
object Error : SealedClassExample()
}
As usual into the android module we can do everything we expect with a sealed class. So the test can be:
@Test
fun usageOfSealedClass() {
val successMessage = "We are the champions"
val success: SealedClassExample = SealedClassExample.Success(successMessage)
assertSuccess(success, successMessage)
val error: SealedClassExample = SealedClassExample.Error
assertError(error)
when (success) {
is SealedClassExample.Success -> println("SealedClassExample.Success instance")
is SealedClassExample.Error -> println("SealedClassExample.Error instance")
}
}
private fun assertSuccess(sealed: SealedClassExample, expectedMessage: String) {
assertTrue(sealed is SealedClassExample.Success)
assertTrue((sealed as SealedClassExample.Success).successMessage == expectedMessage)
}
private fun assertError(sealed: SealedClassExample) {
assertTrue(sealed === SealedClassExample.Error)
}
As far as I know swift and objective-c have no concept of sealed classes. Therefore I do not think it is possible to have something similar to the when
of kotlin.
The tests we can have are like:
func testSealedClassUsage() {
let successMessage = "I'm the winner"
assertSuccessInstance(selead : KotlinLibrarySealedClassExampleSuccess(successMessage: successMessage), expectedMessage : successMessage)
assertErrorInstance(selead : KotlinLibrarySealedClassExampleError())
}
private func assertSuccessInstance(selead : KotlinLibrarySealedClassExample, expectedMessage: String){
XCTAssert(selead is KotlinLibrarySealedClassExampleSuccess,"Failed: sealed is \(String(describing: selead.self))")
let success = selead as! KotlinLibrarySealedClassExampleSuccess
XCTAssert(success.successMessage == expectedMessage,"Failed: message is '\(String(describing: success.successMessage))' the expected is '\(String(describing: expectedMessage))' ")
}
private func assertErrorInstance(selead : KotlinLibrarySealedClassExample){
XCTAssert(selead is KotlinLibrarySealedClassExampleError)
}
Object definition
object KotlinObject {
val INITIAL_VALUE_FOR_NULLABLE_STRING_PROPERTY = null
const val INITIAL_VALUE_FOR_INT_PROPERTY = 0
const val INITIAL_VALUE_FOR_NULLABLE_ANY_PROPERTY = "any variable initial state"
var internalStateNullableString: String? = INITIAL_VALUE_FOR_NULLABLE_STRING_PROPERTY
var internalStateInt: Int = INITIAL_VALUE_FOR_INT_PROPERTY
var internalStateNullableAny: Any? = INITIAL_VALUE_FOR_NULLABLE_ANY_PROPERTY
}
As usual, no surprises
@Test
fun usageOfKotlinObject() {
assertTrue(KotlinObject === KotlinObject)
}
Well, even here in the ios module the kotlin object is a singleton π
KotlinLibraryKotlinObject()
give us always the same instance
func testKotlinObjectUsage() {
XCTAssert(KotlinLibraryKotlinObject() === KotlinLibraryKotlinObject())
}
Reusing the object defined in the section above
Once again, no surprises
@Test
fun usageOfKotlinObject_properties() {
playWithIntProperty()
playWithNullableAnyProperty()
playWithNullableStringProperty()
}
private fun playWithIntProperty() {
assertTrue(KotlinObject.internalStateInt == KotlinObject.INITIAL_VALUE_FOR_INT_PROPERTY)
KotlinObject.internalStateInt = 123
assertTrue(KotlinObject.internalStateInt == 123)
}
private fun playWithNullableAnyProperty() {
assertTrue(KotlinObject.internalStateNullableAny == KotlinObject.INITIAL_VALUE_FOR_NULLABLE_ANY_PROPERTY)
KotlinObject.internalStateNullableAny = 123
assertTrue(KotlinObject.internalStateInt == 123)
KotlinObject.internalStateNullableAny = "Now I'm a string"
assertTrue(KotlinObject.internalStateNullableAny == "Now I'm a string")
}
private fun playWithNullableStringProperty() {
assertTrue(KotlinObject.internalStateNullableString == KotlinObject.INITIAL_VALUE_FOR_NULLABLE_STRING_PROPERTY)
KotlinObject.internalStateNullableString = "Now I'm not null"
assertTrue(KotlinObject.internalStateNullableString == "Now I'm not null")
}
There's something weird happening
func testKotlinObjectUsage_properties() {
playWithIntProperty()
playWithNullableAnyProperty()
playWithNullableStringProperty()
}
private func playWithIntProperty(){
XCTAssert(KotlinLibraryKotlinObject().internalStateInt == KotlinLibraryKotlinObject().initial_VALUE_FOR_INT_PROPERTY) // #1
KotlinLibraryKotlinObject().internalStateInt = 123
XCTAssert(KotlinLibraryKotlinObject().internalStateInt == 123)
}
private func playWithNullableAnyProperty(){
XCTAssert((KotlinLibraryKotlinObject().internalStateNullableAny as! String) == KotlinLibraryKotlinObject().initial_VALUE_FOR_NULLABLE_ANY_PROPERTY)
KotlinLibraryKotlinObject().internalStateNullableAny = 123
XCTAssert((KotlinLibraryKotlinObject().internalStateNullableAny as! Int) == 123)
KotlinLibraryKotlinObject().internalStateNullableAny = "Now I'm a string"
XCTAssert((KotlinLibraryKotlinObject().internalStateNullableAny as! String) == "Now I'm a string")
}
private func playWithNullableStringProperty(){
XCTAssert(KotlinLibraryKotlinObject().internalStateNullableString == (KotlinLibraryKotlinObject().initial_VALUE_FOR_NULLABLE_STRING_PROPERTY as! String?)) #2
KotlinLibraryKotlinObject().internalStateNullableString = "Now I'm not null"
XCTAssert(KotlinLibraryKotlinObject().internalStateNullableString! == "Now I'm not null")
}
- the kotlin val defined as
const val INITIAL_VALUE_FOR_INT_PROPERTY = 0
is mapped ininitial_VALUE_FOR_INT_PROPERTY
with "initial" lowercase - the kotlin val defined as
val INITIAL_VALUE_FOR_NULLABLE_STRING_PROPERTY = null
hasNothing?
as type (doc as reference π¬). Xcode tell us that the kotlin val was mapped into avar initial_VALUE_FOR_NULLABLE_STRING_PROPERTY: KotlinLibraryStdlibNothing?
so, a cast toString?
seems to be inevitable
Companion object definition
class Container(val state: String) {
fun getDecoratedState(): String {
return "${sharedVar ?: ""} $state"
}
companion object {
const val MY_SUPER_NICE_CONST = 12
var sharedVar: String? = null
}
}
As usual, no surprises
@Test
fun usageOfCompanionObject() {
assertEquals(12, Container.MY_SUPER_NICE_CONST)
val firstContainer = Container("first")
val secondContainer = Container("second")
assertFalse(firstContainer === secondContainer)
Container.sharedVar = "Hello"
assertEquals("Hello first", firstContainer.getDecoratedState())
assertEquals("Hello second", secondContainer.getDecoratedState())
}
As we could see into the objects section, even the swift usage of a companion object is straightforward
func testCompanionObjectUsage() {
XCTAssertEqual(12, KotlinLibraryContainerCompanion().my_SUPER_NICE_CONST)
let firstContainer = KotlinLibraryContainer(state: "first")
let secondContainer = KotlinLibraryContainer(state: "second")
XCTAssertFalse(firstContainer === secondContainer)
KotlinLibraryContainerCompanion().sharedVar = "Hello"
XCTAssert(firstContainer.getDecoratedState() == "Hello first")
XCTAssert(secondContainer.getDecoratedState() == "Hello second")
}
fun topLevelFunction(): String {
return "I am a top level"
}
fun Int.extensionFuction(): Int {
return this * 2
}
fun higherOrderFunctionWithParameter(a: Int, b: Int, f: (Int, Int) -> Int): Int {
return f(a, b)
}
fun higherOrderFunctionWithReturn(functionType: FunctionType): (Int, Int) -> Int {
return when (functionType) {
FunctionType.SUM -> { a, b -> a + b }
FunctionType.MULTIPLY -> { a, b -> a * b }
}
}
fun higherOrderFunctionBoth(a: Double, b: Double, f: (Double, Double) -> Double): Double {
return f(a, b)
}
fun lambdaWithReceiver(arg: Int, func: Int.() -> Int): Int {
return func(arg)
}
enum class FunctionType {
SUM, MULTIPLY
}
As usual, no surprises. It is still useful to report the tests for those who do not usually develop in kotlin.
@Test
fun topLevelFunctionUsage() {
assertEquals("I am a top level", topLevelFunction())
}
@Test
fun extensionFunctionUsage() {
assertEquals(4, 2.extensionFuction())
}
@Test
fun higherOrderFunctionWithParameterUsage() {
assertEquals(5, higherOrderFunctionWithParameter(2, 3) { a, b -> a + b })
}
@Test
fun higherOrderFunctionWithReturnUsage() {
assertEquals(5, higherOrderFunctionWithReturn(FunctionType.SUM)(2, 3))
val higherOrderFunctionWithReturn = higherOrderFunctionWithReturn(FunctionType.MULTIPLY)
assertEquals(6, higherOrderFunctionWithReturn(2, 3))
}
@Test
fun higherOrderFunctionBothUsage() {
assertEquals(25, higherOrderFunctionBoth(2.0, 3.0) { a, b -> (a + b).pow(2) }.toInt())
}
Kotlin top level functions are accessible directly from KotlinLibrary
.
Also the extension functions are accessible via KotlinLibrary
. As we could expect, since kotlin generates objective-c code, extensions can not be invoked directly on the type that extend.
The higher order functions seem to be mapped as function pointers, for this reason the parameters defined as double
in kotlin are now NSNumber
func testTopLevelFunctionUsage() {
XCTAssert("I am a top level" == KotlinLibrary.topLevelFunction())
}
func testExtensionFunctionUsage() {
XCTAssert(4 == KotlinLibrary.extensionFuction(2))
}
func testHigherOrderFunctionWithParameterUsage() {
XCTAssert(5 == KotlinLibrary.higherOrderFunctionBoth(a: 2, b: 3, f: {
NSNumber(value: ($0.doubleValue + $1.doubleValue))
}))
}
func testHigherOrderFunctionWithReturnUsage() {
XCTAssert(5 == KotlinLibrary.higherOrderFunctionWithReturn(functionType: KotlinLibraryFunctionType.sum)(2, 3))
let higherOrderFunctionWithReturn = KotlinLibrary.higherOrderFunctionWithReturn(functionType: KotlinLibraryFunctionType.multiply)
XCTAssert(6 == higherOrderFunctionWithReturn(2, 3))
}
func testHigherOrderFunctionBothUsage() {
XCTAssert(25 == KotlinLibrary.higherOrderFunctionBoth(a: 2, b: 3, f: {
NSNumber(value: pow(($0.doubleValue + $1.doubleValue),2))
}))
}
By using well-named functions as builders in combination with function literals with receiver it is possible to create type-safe, statically-typed builders in >Kotlin.
Type-safe builders allow for creating Kotlin-based domain-specific languages (DSLs) suitable for building complex hierarchical data structures in a semi-declarative way. Some of the example use cases for the builders are:
- Generating markup with Kotlin code, such as HTML or XML;
- Programmatically laying out UI components: Anko
- Configuring routes for a web server: Ktor.
-- from official documentation
Here everything we need for define a micro DSL:
data class Port(var value: Int = 0, var isSecure: Boolean = false) {
operator fun invoke(init: Port.() -> Unit) {
init()
}
}
data class Configuration(var host: String = "", var port: Port = Port())
fun configuration(init: Configuration.() -> Unit): Configuration {
val configuration = Configuration()
configuration.init()
return configuration
}
- Two data class:
Port
andConfiguration
- An operator overload:
operator fun invoke(init: Port.() -> Unit)
- A lambda with receiver
fun configuration(init: Configuration.() -> Unit): Configuration
With the code above we can write a test like:
@Test
fun dslUsage() {
val conf = configuration {
host = "127.0.0.1"
port {
value = 8080
isSecure = false
}
}
assertEquals("127.0.0.1", conf.host)
assertEquals(8080, conf.port.value)
assertEquals(false, conf.port.isSecure)
}
In swift we can achieve a similar result. In my opinion, compared to the kotlin version, we lose a bit of readability but the result is still acceptable.
func testDslUsage() {
let conf = KotlinLibrary.configuration(init: {
$0.host = "127.0.0.1"
$0.port.invoke(init: {
$0.value = 8080
$0.isSecure = false
return KotlinLibraryStdlibUnit()
})
return KotlinLibraryStdlibUnit()
})
XCTAssert(conf.host == "127.0.0.1")
XCTAssert(conf.port.value == 8080)
XCTAssert(conf.port.isSecure == false)
}
Common code:
data class Box<T>(var elemet: T)
data class AnimalBox<T : Animal>(var elemet: T)
val mapIntegersToStrings: MutableMap<Int, String> = mutableMapOf()
interface Animal {
fun speak(): String
}
class Dog : Animal {
override fun speak() = "woof"
}
class Cat : Animal {
override fun speak() = "meow"
}
Box
is a generic container, AnimalBox
is a container that allows only subclasses of Animal
as element.
mapIntegersToStrings
is a top level property, map with int
key and string
values
Nothing to explain, all straightforward
@Test
fun genericsBasicUsage() {
val intBox = Box(8)
val stringBox = Box("Hi")
assertEquals(8, intBox.elemet)
assertEquals("Hi", stringBox.elemet)
// intBox.elemet = "World" <--- compilation error
}
@Test
fun genericsUsage() {
val animalBox: AnimalBox<Animal> = AnimalBox(Do())
assertEquals("woof", animalBox.elemet.speak())
animalBox.elemet = Cat()
assertEquals("meow", animalBox.elemet.speak())
// animalBox.elemet = 2 <--- compilation error
}
@Test
fun genericsMapUsage() {
mapIntegersToStrings[1] = "one"
mapIntegersToStrings[2] = "two"
// mapIntegersToStrings[3] = 3 <--- compilation error
// mapIntegersToStrings["three"] = "three" <---compilation error
}
Apple introduces Generics in Objective-C since 2015. The main goal is to enable Swift Generics to convert to Objective-C syntax without extra overhead (as it is called as βlightweight genericsβ).
-- from Generics in Objective-C
func testGenericsBasicUsage() { //1
let intBox = KotlinLibraryBox(elemet: 8)
let stringBox = KotlinLibraryBox(elemet: "Hi")
XCTAssert(intBox.elemet as! Int == 8)
XCTAssert(stringBox.elemet as! String == "Hi")
intBox.elemet = "World" //<--- NO compilation error!!
XCTAssert(intBox.elemet as! String == "World")
}
func testGenericsUsage() {
let animalBox = KotlinLibraryAnimalBox(elemet: KotlinLibraryDog())
XCTAssert(animalBox.elemet.speak() == "woof")
animalBox.elemet = KotlinLibraryCat()
XCTAssert(animalBox.elemet.speak() == "meow")
//animalBox.elemet = 2 // <--- compilation error
}
func testGenericsMapUsage() {
KotlinLibrary.mapIntegersToStrings[0] = "zero"
KotlinLibrary.mapIntegersToStrings[1] = "one"
KotlinLibrary.mapIntegersToStrings[2] = 2
KotlinLibrary.mapIntegersToStrings["three"] = "three"
XCTAssert("zero" == KotlinLibrary.mapIntegersToStrings[0] as! String)
XCTAssert("one" == KotlinLibrary.mapIntegersToStrings[1] as! String)
XCTAssert(2 == KotlinLibrary.mapIntegersToStrings[2] as! Int)
XCTAssert("three" == KotlinLibrary.mapIntegersToStrings["three"] as! String)
}
testGenericsBasicUsage π
Into testGenericsBasicUsage
we can see that Box<T>
element is mapped as a nullable pointer to any type @property id _Nullable elemet;
. For this reason we can do intBox.elemet = "World"
without compilation errors.
testGenericsUsage π
Here, the AnimalBox<T : Animal>
element is mapped as @property id<KotlinLibraryAnimal> elemet
. So, finally, operations like animalBox.elemet = 2
are forbidden.
testGenericsMapUsage π€
mapIntegersToStrings
in kotlin is a MutableMap<Int, String>
and the objective-c generate code is @property (class, readonly) KotlinLibraryMutableDictionary<NSNumber *, NSString *> *mapIntegersToStrings
. Reading <NSNumber *, NSString *>
I personally thought that it was not possible to do neither KotlinLibrary.mapIntegersToStrings[2] = 2
nor mapIntegersToStrings["three"] = "three"
. Instead, no compilation errors and no failing tests.
class CannotInheritFromMe(val parentValue: Int)
open class YouCanInheritFromMe(val parentValue: Int)
abstract class AbstractClass(val abstractClassValue: Int) {
abstract fun transformValue(): String
fun concreteFunction(): Int{
return transformValue().length
}
}
interface Shape {
var interfaceVariable: String
val interfaceValue: Int
fun area(): Float
fun perim(): Float
}
class Square(val side: Float) : Shape {
override var interfaceVariable: String = "Hi I'm a square with side = $side"
override val interfaceValue: Int = 123
override fun area() = side.pow(2)
override fun perim() = 4 * side
}
Here we have:
class CannotInheritFromMe
that, as design, is finalopen class YouCanInheritFromMe
that, with the open annotation, allows others to inherit from itabstract class AbstractClass
with a default constructor, an abstract functiontransformValue(): String
and a concrete functionconcreteFunction(): Int
interface Shape
Square
that implementsShape
Once, again, no surprises:
@Test
fun inheritanceForFinalClass() {
// class Attempt(val parentValue: Int) : CannotInheritFromMe(parentValue) <--- compilation error
}
@Test
fun inheritanceForOpenClass() {
class Attempt(val childValue: String, parentValue: Int) : YouCanInheritFromMe(parentValue)
val attempt = Attempt(childValue = "value for Child", parentValue = 1)
assertEquals("value for Child", attempt.childValue)
assertEquals(1, attempt.parentValue)
}
@Test
fun inheritanceForAbstractClass() {
class Attempt(val childValue: String, parentValue: Int) : AbstractClass(parentValue) {
override fun transformValue(): String {
return "$childValue : $abstractClassValue"
}
}
val attempt = Attempt(childValue = "value for Child", parentValue = 1)
assertEquals("value for Child", attempt.childValue)
assertEquals(1, attempt.abstractClassValue)
assertEquals("value for Child : 1", attempt.transformValue())
assertEquals(19, attempt.concreteFunction())
}
@Test
fun inheritanceForInterface() {
class Rect(val width: Float, val height: Float) : Shape {
override var interfaceVariable = "Hi I'm a rect with width = $width and height = $height"
override val interfaceValue: Int = 456
override fun area() = width * height
override fun perim() = 2 * (width + height)
}
val rect = Rect(2f, 3f)
val square = Square(2f)
assertTrue(rect is Shape)
assertTrue(square is Shape)
assertEquals(10f, rect.perim())
assertEquals(6f, rect.area())
assertEquals("Hi I'm a rect with width = 2.0 and height = 3.0", rect.interfaceVariable)
assertEquals(456, rect.interfaceValue)
assertEquals(2f, square.side)
assertEquals(8f, square.perim())
assertEquals(4f, square.area())
assertEquals("Hi I'm a square with side = 2.0", square.interfaceVariable)
assertEquals(123, square.interfaceValue)
}
inheritanceForFinalClass
Inheritance from a final class is forbidden
inheritanceForOpenClass
Inheritance from an open class is allowed. Attempt
has a constructor with a val childValue: String
and a parentValue: Int
; parentValue
is used to feed the constructor of YouCanInheritFromMe
inheritanceForAbstractClass
Inheritance from an abstract class is allow. Attempt
has a constructor with a val childValue: String
and a parentValue: Int
; parentValue
is used to feed the constructor of AbstractClass
. In addition to this, Attempt
has also the concrete implementation of transformValue(): String
, the abstract method of AbstractClass
.
inheritanceForInterface
In the common module we have defined Shape
as interface, and Square
as class that implements Shape
.
Into the test case we have Rect
, an other class that implements Shape
.
Once, again, weird things π
Swift:
func testInheritanceForFinalClass(){
class Attempt: KotlinLibraryCannotInheritFromMe { //<--- NO compilation error!!
override init(parentValue: Int32) {
super.init(parentValue: parentValue)
}
}
let attempt = Attempt(parentValue: 24)
XCTAssert(attempt.parentValue == 24)
}
func testInheritanceForOpenClass(){
class Attempt: KotlinLibraryYouCanInheritFromMe {
override init(parentValue: Int32) {
super.init(parentValue: parentValue)
}
}
let attempt = Attempt(parentValue: 24)
XCTAssert(attempt.parentValue == 24)
}
func testInheritanceForAbstractClass(){
class Attempt: KotlinLibraryAbstractClass {
var value : String
init(childValue: String, abstractClassValue: Int32) {
value = childValue
super.init(abstractClassValue: abstractClassValue)
}
override func transformValue() -> String {
return "\(value) : \(abstractClassValue)"
}
}
let attempt = Attempt(childValue: "value for Child", abstractClassValue: 1)
XCTAssert(attempt.abstractClassValue == 1)
XCTAssert(attempt.value == "value for Child")
XCTAssert("value for Child : 1" == attempt.transformValue())
XCTAssert(19 == attempt.concreteFunction())
}
func testInheritanceForInterface(){
class Rect : KotlinLibraryShape {
let width, height: Float
init(width: Float, height: Float) {
self.width = width
self.height = height
}
var interfaceValue: Int32{
get {
return 456
}
}
lazy var interfaceVariable = "Hi I'm a rect with width = \(width) and height = \(height)"
func area() -> Float {
return width * height
}
func perim() -> Float {
return 2 * (width + height)
}
}
let rect = Rect(width: 2.0, height: 3.0)
let square = KotlinLibrarySquare(side:2.0)
XCTAssert(rect is KotlinLibraryShape)
XCTAssert(square is KotlinLibraryShape)
XCTAssert(10.0 == rect.perim())
XCTAssert(6.0 == rect.area())
XCTAssert("Hi I'm a rect with width = 2.0 and height = 3.0" == rect.interfaceVariable)
XCTAssert(456 == rect.interfaceValue, "Failed: rect.interfaceVariable is \(String(describing: rect.interfaceValue))")
XCTAssert(2.0 == square.side)
XCTAssert(8.0 == square.perim())
XCTAssert(4.0 == square.area())
XCTAssert("Hi I'm a square with side = 2.0" == square.interfaceVariable)
XCTAssert(123 == square.interfaceValue)
}
Objective-C:
@import KotlinLibrary;
@interface Attempt: KotlinLibraryCannotInheritFromMe // <--- compilation error : Cannot subclass a class that was declared with the 'objc_subclassing_restricted' attribute
@end
@implementation Attempt
@end
testInheritanceForFinalClass π±
From an objective-c file, inherit from a kotlin final class is forbidden π
From a swift file, instead, the inheritation from a kotlin final class is allowed π¦
If we jump into the definition of KotlinLibraryCannotInheritFromMe
we can see
From KotlinLibrary.h
:
__attribute__((objc_subclassing_restricted))
@interface KotlinLibraryCannotInheritFromMe : KotlinBase
-(instancetype)initWithParentValue:(int32_t)parentValue NS_SWIFT_NAME(init(parentValue:)) NS_DESIGNATED_INITIALIZER;
From the KotlinLibrary
import:
open class KotlinLibraryCannotInheritFromMe : KotlinBase {
public init(parentValue: Int32)
open var parentValue: Int32 { get }
}
Seeing the definitions, it is clear the reason of the two different behaviours. Googling it seems that:
Make an Objective-C class final in Swift
It seems this is impossible right now. Objective-C doesn't support final classes so Swift supposes that every Obj-C class is not final.
-- from stackoverflow
Add
objc_subclassing_restricted
attributeThis patch adds an
objc_subclassing_restricted
attribute into clang. This attribute acts similarly to 'final' - Objective-C classes with this attribute can't be subclassed. However,@interface
declarations that haveobjc_subclassing_restricted
but don't have@implementation
are allowed to inherit other@interface
declarations withobjc_subclassing_restricted
. This is needed to describe the Swift class hierarchy in clang while making sure that the Objective-C classes can't subclass the Swift classes.-- from reviews.llvm.org
testInheritanceForOpenClass
If even the inheritance from a final class is conceded, the inheritance from an open class can only be allowed π
testInheritanceForAbstractClass
Ehm, objective-c has no compile-time enforcement that prevents instantiation of an abstract class, so, yes we can inherit from an abstract class, not override any of the abstract methods and have no compiling errors π
In our case the abstract fun transformValue()
is used into the concrete function so we have wonderful SIGABRT ERROR
at runtime π π
testInheritanceForInterface
Finally a straightforward case! ππ
To support common modules unit testing, Kotlin team made kotlin.test
library. To use it, we need to add the following dependencies into our common module build.gradle
file
testCompile "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version"
Once added the dependencies we can write test in the common module:
class TestExample {
@Test
fun `square should respect the perim and area formulas`() {
val square = Square(2f)
assertEquals(2f, square.side)
assertEquals(8f, square.perim())
assertEquals(4f, square.area())
assertEquals("Hi I'm a square with side = 2.0", square.interfaceVariable)
assertEquals(123, square.interfaceValue)
}
@Test
fun `Greeting()_greeting() should start with 'Hello, '`() {
assertTrue {
Greeting().greeting().startsWith("Hello, ")
}
}
}
To run the tests we have to run them on platform modules
In order to run test in the JVM module we need the following dependencies into the build.gradle
of the android module
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
Now we can run tests on JVM using
./gradlew :kotlinLibrary:android:test
An HTML report is generated in android/build/reports/tests/test/index.html
We have already done the setup to run the test in the iOS environment writing into the build.gradle
of the iOS module
konanArtifacts {
//... STUFF ...
program('NativeTest') {
srcDir 'src/test/kotlin'
commonSourceSet 'test'
libraries {
artifact 'NativeLib'
}
extraOpts '-tr'
}
}
task test(dependsOn: 'compileKonanNativeTestIos_x64', type: Exec) {
def textExecutable = tasks["compileKonanNativeTestIos_x64"].artifactPath
commandLine("xcrun", "simctl", "spawn", "iPhone 8", textExecutable)
}
so, similarly to Android we can run tests on iOS using
./gradlew :kotlinLibrary:ios:test
And the output is
> Task :kotlinLibrary:ios:test
[==========] Running 2 tests from 1 test cases.
[----------] Global test environment set-up.
[----------] 2 tests from com.ndefiorenze.TestExample
[ RUN ] com.ndefiorenze.TestExample.square should respect the perim and area formulas
[ OK ] com.ndefiorenze.TestExample.square should respect the perim and area formulas (2 ms)
[ RUN ] com.ndefiorenze.TestExample.Greeting()_greeting() should start with 'Hello, '
[ OK ] com.ndefiorenze.TestExample.Greeting()_greeting() should start with 'Hello, ' (0 ms)
[----------] 2 tests from com.ndefiorenze.TestExample (3 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test cases ran. (3 ms total)
[ PASSED ] 2 tests.