Upgrade to Pro — share decks privately, control downloads, hide ads and more …

From Mobile to Backend with Kotlin and Ktor - FOSDEM 2022

From Mobile to Backend with Kotlin and Ktor - FOSDEM 2022

As mobile developers, we often see the backend world as something magical that “does things”. But what if I tell you that you can easily build a backend even if mobile is your thing?

With this talk, I want to show how it is possible to bring your mobile knowledge (and shift it a little bit) to build a backend with Kotlin and Ktor. I will show how to structure the project, set up Dependency Injection, connect to a database and test everything to have a working backend ready to be deployed.

Marco Gomiero

February 06, 2022
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. Marco Gomiero
    FOSDEM
    From Mobile to
    Backend with
    Kotlin and Ktor

    View Slide

  2. FOSDEM - @marcoGomier
    Marco Gomiero
    👨💻 Android Engineer @ TIER 🛴 🇩🇪 🇮🇹

    Google Developer Expert for Kotlin
    > Twitter: @marcoGomier

    > Github: prof18

    > Website: marcogomiero.com

    View Slide

  3. FOSDEM - @marcoGomier
    🤔 Why?

    View Slide

  4. FOSDEM - @marcoGomier
    • Side project


    • Understand how things works “on the other side”


    • Help you teammates
    🤔 Why?

    View Slide

  5. FOSDEM - @marcoGomier
    * based on a true story

    View Slide

  6. FOSDEM - @marcoGomier
    Ktor
    https://ktor.io/

    View Slide

  7. FOSDEM - @marcoGomier
    Ktor
    • Kotlin


    • Lightweight and flexible


    • Asynchronous with Coroutines


    • Unopinionated

    View Slide

  8. FOSDEM - @marcoGomier
    Unopinionated
    • Whatever architecture


    • Whatever pattern
    Knowledge Transfer

    View Slide

  9. FOSDEM
    @marcoGomier
    Architecture

    View Slide

  10. FOSDEM - @marcoGomier
    Android
    Activity/Fragment
    ViewModel
    Repository
    Local Data Source Remote Data Source
    Application

    View Slide

  11. FOSDEM - @marcoGomier
    Resource*
    Repository
    Local Data Source Remote Data Source
    Application
    * or Controller
    Ktor

    View Slide

  12. FOSDEM - @marcoGomier
    Resource
    Repository
    Local Data Source Remote Data Source
    Application
    Activity/Fragment
    ViewModel
    Repository
    Local Data Source Remote Data Source
    Application

    View Slide

  13. FOSDEM - @marcoGomier
    Domain
    Data
    Resource
    Repository
    Local Data Source Remote Data Source
    Application
    Activity/Fragment
    ViewModel
    Repository
    Local Data Source Remote Data Source
    Application
    Presentation
    Application

    View Slide

  14. FOSDEM - @marcoGomier
    Application
    class MyApp : Application() {


    override fun onCreate() {


    super.onCreate()


    if (BuildConfig.DEBUG) {


    Timber.plant(Timber.DebugTree())


    }


    initAnalytics()


    initCrashReporting()


    initRandomLib()


    }


    }

    View Slide

  15. FOSDEM - @marcoGomier
    fun Application.module() {


    install(Koin) {


    slf4jLogger()


    modules(koinModules)


    }


    setupConfig()


    setupDatabase()


    install(ContentNegotiation) {


    json()


    }


    install(CallLogging) {


    level = Level.INFO


    }


    install(Locations)


    routing {


    setupEndpoint()


    }


    }
    Application

    View Slide

  16. FOSDEM - @marcoGomier
    Plugin
    https://ktor.io/docs/plugins.html
    • Add a specific feature to your backend


    • Highly customisable


    • No plugin activated by default

    View Slide

  17. FOSDEM - @marcoGomier
    Plugin
    https://ktor.io/docs/

    View Slide

  18. FOSDEM - @marcoGomier
    Domain
    Data
    Presentation
    Application

    View Slide

  19. FOSDEM - @marcoGomier
    Presentation
    class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContentView(R.layout.activity_main)


    ...

    }


    }

    View Slide

  20. FOSDEM - @marcoGomier
    fun Route.jokeEndpoint() {


    val jokeRepository by inject()


    get {


    call.respond(jokeRepository.getRandomJoke())


    }


    post { apiCallParams
    ->

    val name = apiCallParams.name


    jokeRepository.watch(name)


    call.respond("Ok")


    }


    }
    Presentation

    View Slide

  21. FOSDEM - @marcoGomier
    Domain
    Data
    Presentation
    Application

    View Slide

  22. FOSDEM - @marcoGomier
    Domain
    class JokeRepositoryImpl(


    private val jokeLocalDataSource: JokeLocalDataSource


    ) : JokeRepository {


    override suspend fun getRandomJoke(): JokeDTO {


    // ...

    }


    }

    View Slide

  23. FOSDEM - @marcoGomier
    Domain
    Data
    Presentation
    Application

    View Slide

  24. FOSDEM - @marcoGomier
    Room
    class LocalDatasource(


    private val db: RoomDatabase


    ) {


    suspend fun getAllowedApps(): List {


    return db.allowedAppDAO().getAllAllowedApps()


    }


    }
    @Entity(tableName = "allowed_app")


    data class AllowedApp(


    @PrimaryKey val packageName: String,


    @ColumnInfo val appName: String


    )
    @Dao


    interface AllowedAppDAO {


    @Query("SELECT * FROM allowed_app")


    suspend fun getAllAllowedApps(): List


    }
    Data

    View Slide

  25. FOSDEM - @marcoGomier
    Exposed
    https://github.com/JetBrains/Exposed

    View Slide

  26. FOSDEM - @marcoGomier
    Exposed
    class JokeLocalDataSourceImpl : JokeLocalDataSource {


    override suspend fun getAllJokes(): List {


    val joke = newSuspendedTransaction {


    val query = JokeTable.selectAll()


    Joke.wrapRows(query).toList()


    }


    }


    }
    object JokeTable: IdTable(name = "joke") {


    val createdAt = datetime("created_at")


    val updatedAt = datetime("updated_at")


    val value = text("value")


    override val id: Column>>
    = varchar("joke_id", 255).entityId()


    override val primaryKey: PrimaryKey = PrimaryKey(id)


    }
    class Joke(id: EntityID): Entity(id) {


    companion object: EntityClass(JokeTable)


    var createdAt by JokeTable.createdAt


    var updatedAt by JokeTable.updatedAt


    var value by JokeTable.value


    }
    Data

    View Slide

  27. FOSDEM - @marcoGomier
    EmailThreadTable.join(EmailFolderTable, JoinType.INNER, additionalConstraint = {


    (EmailThreadTable.id eq EmailFolderTable.threadId).and(


    EmailThreadTable.account eq EmailFolderTable.accountId


    )


    })


    .select { EmailThreadTable.account eq account.id.value and (EmailFolderTable.folderId eq folderId) }


    .orderBy(EmailThreadTable.lastEmailDate, SortOrder.DESC)


    .limit(n = pageSize)


    View Slide

  28. FOSDEM
    @marcoGomier
    Dependency Injection

    View Slide

  29. FOSDEM - @marcoGomier https://insert-koin.io/docs/quickstart/ktor
    Koin

    View Slide

  30. FOSDEM - @marcoGomier
    val appModule = module {


    single { HelloRepositoryImpl() }


    factory { MySimplePresenter(get()) }


    }

    View Slide

  31. FOSDEM - @marcoGomier
    class MyApplication : Application() {


    override fun onCreate() {


    super.onCreate()




    startKoin {


    androidLogger()


    androidContext([email protected])


    modules(appModule)


    }


    }


    }

    View Slide

  32. FOSDEM - @marcoGomier
    class MainActivity : AppCompatActivity() {


    val firstPresenter: MySimplePresenter by inject()


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContentView(R.layout.activity_main)


    ...

    }


    }

    View Slide

  33. FOSDEM - @marcoGomier
    val appModule = module {


    single { JokeLocalDataSourceImpl() }


    single { JokeRepositoryImpl(get()) }


    }

    View Slide

  34. FOSDEM - @marcoGomier
    fun Application.module() {


    install(Koin) {


    slf4jLogger()


    modules(koinModules)


    }


    ...

    }

    View Slide

  35. FOSDEM - @marcoGomier
    fun Route.jokeEndpoint() {


    val jokeRepository by inject()


    get {


    call.respond(jokeRepository.getRandomJoke())


    }


    post { apiCallParams
    ->

    val name = apiCallParams.name


    jokeRepository.watch(name)


    call.respond("Ok")


    }


    }

    View Slide

  36. FOSDEM
    @marcoGomier
    Logging

    View Slide

  37. FOSDEM - @marcoGomier
    Timber
    class MyApplication : Application() {


    override fun onCreate() {


    super.onCreate()


    if (BuildConfig.DEBUG) {


    Timber.plant(Timber.DebugTree())


    } else {


    //
    Log to somewhere else


    }


    }


    }
    https://github.com/JakeWharton/timber

    View Slide

  38. FOSDEM - @marcoGomier https://github.com/JakeWharton/timber
    Timber.d(“Message")


    Timber.v("Message")


    Timber.i("Message")


    Timber.w("Message")


    Timber.wtf("Message")


    Timber.e(exception)
    Timber

    View Slide

  39. FOSDEM - @marcoGomier
    SLF4J
    http://www.slf4j.org/index.html
    routing {


    get("/api/v1") {


    call.application.environment.log.info("Hello from /api/v1!")


    }


    }
    fun Application.module(testing: Boolean = false) {


    log.info("Hello from module!")


    }

    View Slide

  40. FOSDEM - @marcoGomier
    val logger = LoggerFactory.getLogger(MyClass
    ::
    class.java)
    inline fun T.getLogger(): Logger {


    return LoggerFactory.getLogger(T
    ::
    class.java)


    }
    class MyClass {


    private val logger = getLogger()


    fun main() {


    logger.info("Hello World")


    }


    }

    View Slide

  41. FOSDEM - @marcoGomier
    logger.trace("Message")


    logger.debug("Message")


    logger.info("Message")


    logger.warn("Message")


    logger.error("Message")
    SLF4J

    View Slide

  42. FOSDEM - @marcoGomier
    logback.xml

    View Slide

  43. FOSDEM - @marcoGomier
    logback.xml









    %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

    pattern>



    encoder>



    appender>





    ${LOG_DEST}/ktor-chuck-norris-sample.log

    file>







    ${LOG_DEST}/ktor-chuck-norris-sample.%d{yyyy-MM-dd}.log

    fileNamePattern>




    ${LOG_MAX_HISTORY}

    maxHistory>


    3GB

    totalSizeCap>



    rollingPolicy>





    %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

    pattern>



    encoder>



    appender>





    />

    />


    root>


    />

    />

    />


    configuration>


    View Slide

  44. FOSDEM
    @marcoGomier
    Secrets

    View Slide

  45. FOSDEM - @marcoGomier
    local.properties
    tmdbKey="private_key"

    View Slide

  46. FOSDEM - @marcoGomier
    val String.byProperty: String?


    get() {


    val local = java.util.Properties()


    val localProperties: File = rootProject.file("local.properties")


    if (localProperties.exists()) {


    localProperties.inputStream().use { local.load(it) }


    return local.getProperty(this)


    }


    return null


    }
    tmdbKey="private_key"
    buildConfigField("String", "TMDB_KEY", "\"${"tmdbKey".byProperty
    ?:
    "")}\"")
    build.gradle.kts

    View Slide

  47. FOSDEM - @marcoGomier
    tmdbKey="private_key"
    buildConfigField("String", "TMDB_KEY", "\"${"tmdbKey".byProperty
    ?:
    "")}\"")
    val myKey: String = BuildConfig.TMDB_KEY
    NetworkApiInterceptor.kt

    View Slide

  48. FOSDEM - @marcoGomier
    application.conf

    View Slide

  49. FOSDEM - @marcoGomier
    application.conf
    ktor {


    deployment {


    port = 8080


    port = ${?PORT}


    }


    application {


    modules = [com.prof18.ktor.chucknorris.sample.ApplicationKt.module]


    }


    server {


    isProd = false


    }


    database {


    driverClass = "com.mysql.cj.jdbc.Driver"


    url = "jdbc:mysql:
    /
    /
    localhost:3308/chucknorris?useUnicode=true&characterEncoding=UTF-8"


    user = "root"


    password = "password"


    maxPoolSize = 3


    }


    }

    View Slide

  50. FOSDEM - @marcoGomier
    fun Application.setupConfig() {


    val appConfig by inject()


    //
    Server


    val serverObject = environment.config.config("ktor.server")


    val isProd = serverObject.property("isProd").getString().toBoolean()


    appConfig.serverConfig = ServerConfig(isProd)


    }
    data class ServerConfig(


    val isProd: Boolean


    )
    class AppConfig {


    lateinit var serverConfig: ServerConfig


    //
    Place here other configurations


    }

    View Slide

  51. FOSDEM - @marcoGomier
    fun Application.module() {


    ...



    setupConfig()


    val appConfig by inject()


    ...

    }


    View Slide

  52. FOSDEM - @marcoGomier
    java -jar ktor-backend.jar -config=/config-folder/application.conf

    View Slide

  53. FOSDEM
    @marcoGomier
    Testing

    View Slide

  54. FOSDEM - @marcoGomier
    • Unit Test
    ->
    Just regular Kotlin Unit tests


    • androidTest
    ->
    TestEngine
    Testing
    https://ktor.io/docs/testing.html

    View Slide

  55. FOSDEM - @marcoGomier
    @Test


    fun testRequests() = withTestApplication(module(testing = true)) {


    with(handleRequest(HttpMethod.Get, "/")) {


    assertEquals(HttpStatusCode.OK, response.status())


    assertEquals("Hello from Ktor Testable sample application", response.content)


    }
    https://ktor.io/docs/testing.html

    View Slide

  56. FOSDEM
    @marcoGomier
    Deploy

    View Slide

  57. FOSDEM - @marcoGomier
    Deploy
    • aar


    • apk


    • aab

    View Slide

  58. FOSDEM - @marcoGomier https://github.com/johnrengelman/shadow
    ./gradlew assembleRelease
    ./gradlew bundleRelease

    View Slide

  59. FOSDEM - @marcoGomier
    Deploy
    • Fat JAR


    • Executable JVM application


    • WAR


    • GraalVM
    https://ktor.io/docs/deploy.html

    View Slide

  60. FOSDEM - @marcoGomier
    • Fat JAR


    • Executable JVM application


    • WAR


    • GraalVM
    https://ktor.io/docs/deploy.html
    Deploy

    View Slide

  61. FOSDEM - @marcoGomier
    build.gradle.kts
    https://github.com/johnrengelman/shadow
    plugins {


    ...

    id("com.github.johnrengelman.shadow") version "7.0.0"


    }

    View Slide

  62. FOSDEM - @marcoGomier https://github.com/johnrengelman/shadow
    tasks {


    shadowJar {


    manifest {


    attributes(Pair("Main-Class", "io.ktor.server.netty.EngineMain"))


    }


    }


    }
    build.gradle.kts

    View Slide

  63. FOSDEM - @marcoGomier https://github.com/johnrengelman/shadow
    tasks {


    shadowJar {


    manifest {


    attributes(Pair("Main-Class", "io.ktor.server.netty.EngineMain"))


    }


    }


    }
    application {


    mainClass.set("io.ktor.server.netty.EngineMain")


    }
    build.gradle.kts

    View Slide

  64. FOSDEM - @marcoGomier https://github.com/johnrengelman/shadow
    ./gradlew shadowJar

    View Slide

  65. FOSDEM - @marcoGomier

    View Slide

  66. FOSDEM
    @marcoGomier
    Conclusions

    View Slide

  67. FOSDEM - @marcoGomier
    • Ktor is flexible and unopinionated
    ->
    Knowledge Transfer


    • Mobile knowledge can be adapted


    • Effective scaling and deploying can be hard


    • “Going to the other side” enrich your dev vision
    Conclusions

    View Slide

  68. FOSDEM - @marcoGomier https://github.com/prof18/ktor-chuck-norris-sample

    View Slide

  69. Bibliography / Useful Links
    • https:
    //
    ktor.io/


    • https:
    //
    ktor.io/learn/


    • https:
    //
    ktor.io/docs/welcome.html


    • https:
    //
    github.com/JetBrains/Exposed/wiki


    • https:
    //
    www.marcogomiero.com/posts/2021/ktor-project-structure/


    • https:
    //
    www.marcogomiero.com/posts/2021/ktor-logging-on-disk/


    • https:
    //
    www.marcogomiero.com/posts/2021/ktor-in-memory-db-testing/

    View Slide

  70. Marco Gomiero
    FOSDEM
    Thank you!


    > Twitter: @marcoGomier

    > Github: prof18

    > Website: marcogomiero.com

    View Slide