Depending on Native Implementations

Source Code

Once again the game logic looks like some kind of code which could be shared between Android and iOS. So let’s try to move this into our common project.

class Game {

    var afterNextGenerationCalculated: () -> Unit = {}
    val board: Board
    private var timer: GolTimer? = null

    init {

        val columns = 30
        val rows = 50
        board = Board(columns, rows)
        board.setCells(
            """
                ***_*
                *____
                ___**
                _**_*
                *_*_*
                """.trimIndent().cells().translatedTo(columns / 2 - 2, rows / 2 - 2)
        )

    }
    fun start() {
        if (timer != null) return
        timer = GolTimer {
            board.calculateNextGeneration()
            afterNextGenerationCalculated()
        }
    }
    fun stop() {
        timer?.stop()
        timer = null
    }
}

Looks good, but there is a problem. The Kotlin standard library does not provide a Timer class or similar which we could use to implement our game loop.

Kotlin Multi Platform has a nice solution for this kind of problems. If we mark a class or a function with the keyword expect we can use it in the common code but without a multi platform implementation. Think of it as an interface which is required to have an implementation in all platforms configured as targets.

expect class GolTimer(action: () -> Unit) {
    fun stop()
}

We then implement the actual class in the Java and the iOS specific code using the tools that these platforms provide.

import kotlin.concurrent.fixedRateTimer

actual class GolTimer actual constructor(action: () -> Unit) {

    private val timer: Timer =
        fixedRateTimer("GolTimer", false, period = 500L) {
            action()
        }
    
    actual fun stop() {
        timer.cancel()
    }

}
import platform.Foundation.NSTimer

actual class GolTimer actual constructor(action: () -> Unit) {

    private val nativeTimer: NSTimer =
        NSTimer.scheduledTimerWithTimeInterval(1.0, true) {
            action()
        }

    actual fun stop() {
        nativeTimer.invalidate()
    }

}

Note that in the implementation for iOS we get access to the iOS APIs from Kotlin.

Now we can use the common Game class in the iOS and Android project.

class MainActivity : AppCompatActivity() {

    private val game = Game()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        boardView.cellSize = 14.0f * resources.displayMetrics.density
        boardView.board = game.board

        game.afterNextGenerationCalculated = { boardView.invalidate() }
        game.start()

    }

    override fun onResume() {
        super.onResume()
        game.start()
    }

    override fun onPause() {
        super.onPause()
        game.stop()
    }

}
class BoardStore: ObservableObject {

    let objectWillChange = ObservableObjectPublisher()

    var board: Board? = nil

    func calculateNextGeneration() {
        self.objectWillChange.send()
    }

}

struct ContentView: View {

    
    let game = Game()
    @ObservedObject var boardStore = BoardStore()

    var body: some View {
        BoardView(board: $boardStore.board)
            .onAppear(perform: {
                self.boardStore.board = self.game.board
                self.game.afterNextGenerationCalculated = {
                    self.boardStore.calculateNextGeneration()
                }
                self.game.start()
            })
            .onDisappear(perform: {
                self.game.stop()
            })
    }
}