$30 off During Our Annual Pro Sale. View Details »

From Mobile to Backend with Kotlin and Ktor - Ktor Hammer

From Mobile to Backend with Kotlin and Ktor - Ktor Hammer

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

August 27, 2022
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. Marco Gomiero
    Ktor Hammer
    From Mobile to
    Backend with
    Kotlin and Ktor
    👨💻 Senior Android Engineer @ TIER

    Google Developer Expert for Kotlin
    > Twitter: @marcoGomier

    > Github: prof18

    > Website: marcogomiero.com

    View Slide

  2. Ktor Hammer - @marcoGomier
    🤔 Why?

    View Slide

  3. Ktor Hammer - @marcoGomier
    • A career change 🤷


    • Side project


    • Understand how things works “on the other side”


    • Help you teammates
    🤔 Why?

    View Slide

  4. Ktor Hammer - @marcoGomier
    * based on a true story

    View Slide

  5. Ktor Hammer - @marcoGomier
    Ktor
    https://ktor.io/

    View Slide

  6. Ktor Hammer - @marcoGomier
    Ktor
    • Kotlin


    • Lightweight and flexible


    • Asynchronous with Coroutines


    • Unopinionated

    View Slide

  7. Ktor Hammer - @marcoGomier
    Unopinionated
    • Whatever architecture


    • Whatever pattern
    Knowledge Transfer

    View Slide

  8. Ktor Hammer - @marcoGomier
    Disclaimer
    • Everything is based on Ktor 1.6.x


    • Version 2.x has breaking changes!


    • Migrating from 1.6.x to 2.0.
    0 

    https:
    /
    /
    ktor.io/docs/migrating-2.html

    View Slide

  9. Ktor Hammer @marcoGomier
    Architecture

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. Ktor Hammer - @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. Ktor Hammer - @marcoGomier
    Application
    class MyApp : Application() {


    override fun onCreate() {


    super.onCreate()


    if (BuildConfig.DEBUG) {


    Timber.plant(Timber.DebugTree())


    }


    initAnalytics()


    initCrashReporting()


    initRandomLib()


    }


    }

    View Slide

  15. Ktor Hammer - @marcoGomier
    Application
    fun Application.module() {


    install(Koin) {


    slf4jLogger()


    modules(koinModules)


    }


    setupConfig()


    setupDatabase()


    install(ContentNegotiation) {


    json()


    }


    install(CallLogging) {


    level = Level.INFO


    }


    install(Locations)


    routing {


    setupEndpoint()


    }


    }

    View Slide

  16. Ktor Hammer - @marcoGomier
    Application


    slf4jLogger()


    modules(koinModules)


    }


    setupConfig()


    setupDatabase()


    install(ContentNegotiation) {


    json()


    }


    install(CallLogging) {


    level = Level.INFO


    }


    install(Locations)




    View Slide

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


    • Highly customisable


    • No plugin activated by default

    View Slide

  18. Ktor Hammer - @marcoGomier
    Plugin
    https://ktor.io/docs/

    View Slide

  19. Ktor Hammer - @marcoGomier
    Application
    setupConfig()


    setupDatabase()


    install(ContentNegotiation) {


    json()


    }


    install(CallLogging) {


    level = Level.INFO


    }


    install(Locations)


    routing {


    setupEndpoint()


    }


    }

    View Slide

  20. Ktor Hammer - @marcoGomier
    Application
    install(ContentNegotiation) {


    json()


    }


    install(CallLogging) {


    level = Level.INFO


    }


    install(Locations)


    routing {


    setupEndpoint()


    }


    }

    View Slide

  21. Ktor Hammer - @marcoGomier
    Domain
    Data
    Presentation
    Application

    View Slide

  22. Ktor Hammer - @marcoGomier
    Presentation
    class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContentView(R.layout.activity_main)


    ...

    }


    }

    View Slide

  23. Ktor Hammer - @marcoGomier
    Presentation
    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

  24. Ktor Hammer - @marcoGomier
    Domain
    Data
    Presentation
    Application

    View Slide

  25. Ktor Hammer - @marcoGomier
    Domain
    class JokeRepositoryImpl(


    private val jokeLocalDataSource: JokeLocalDataSource


    ) : JokeRepository {


    override suspend fun getRandomJoke(): JokeDTO {


    // ...

    }


    }

    View Slide

  26. Ktor Hammer - @marcoGomier
    Domain
    Data
    Presentation
    Application

    View Slide

  27. Ktor Hammer - @marcoGomier
    @Entity(tableName = "allowed_app")


    data class AllowedApp(


    @PrimaryKey val packageName: String,


    @ColumnInfo val appName: String


    )
    Room
    https://developer.android.com/training/data-storage/room
    Data

    View Slide

  28. Ktor Hammer - @marcoGomier
    @Dao


    interface AllowedAppDAO {


    @Query("SELECT * FROM allowed_app")


    suspend fun getAllAllowedApps(): List


    }
    @Entity(tableName = "allowed_app")


    data class AllowedApp(


    @PrimaryKey val packageName: String,


    @ColumnInfo val appName: String


    )

    View Slide

  29. Ktor Hammer - @marcoGomier
    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


    }

    View Slide

  30. Ktor Hammer - @marcoGomier
    Exposed
    https://github.com/JetBrains/Exposed

    View Slide

  31. Ktor Hammer - @marcoGomier
    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)


    }

    View Slide

  32. Ktor Hammer - @marcoGomier
    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


    }
    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)


    }

    View Slide

  33. Ktor Hammer - @marcoGomier
    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


    }

    View Slide

  34. Ktor Hammer - @marcoGomier
    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


    }

    View Slide

  35. Ktor Hammer - @marcoGomier
    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


    }
    class JokeLocalDataSourceImpl : JokeLocalDataSource {


    override suspend fun getAllJokes(): List {


    val joke = newSuspendedTransaction {


    val query = JokeTable.selectAll()


    Joke.wrapRows(query).toList()


    }


    }


    }

    View Slide

  36. Ktor Hammer - @marcoGomier
    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


    }
    class JokeLocalDataSourceImpl : JokeLocalDataSource {


    override suspend fun getAllJokes(): List {


    val joke = newSuspendedTransaction {


    val query = JokeTable.selectAll()


    Joke.wrapRows(query).toList()


    }


    }


    }

    View Slide

  37. Ktor Hammer - @marcoGomier
    Joke.new {


    this.createdAt = LocalDateTime.now()


    this.updatedAt = LocalDateTime.now()


    value = "A Joke"


    }

    View Slide

  38. Ktor Hammer - @marcoGomier
    EmailTable.deleteWhere {


    EmailTable.account.eq(account.id.value) and EmailTable.id.eq(emailId)


    }

    View Slide

  39. Ktor Hammer - @marcoGomier
    EmailTable.deleteWhere {


    EmailTable.account.eq(account.id.value) and EmailTable.id.eq(emailId)


    }
    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

  40. Ktor Hammer @marcoGomier
    Dependency Injection

    View Slide

  41. Ktor Hammer - @marcoGomier https://insert-koin.io/docs/quickstart/ktor
    Koin

    View Slide

  42. Ktor Hammer - @marcoGomier
    val appModule = module {


    single { HelloRepositoryImpl() }


    factory { MySimplePresenter(get()) }


    }

    View Slide

  43. Ktor Hammer - @marcoGomier
    class MyApplication : Application() {


    override fun onCreate() {


    super.onCreate()




    startKoin {


    androidLogger()


    androidContext(this@MyApplication)


    modules(appModule)


    }


    }


    }

    View Slide

  44. Ktor Hammer - @marcoGomier
    class MainActivity : AppCompatActivity() {


    val firstPresenter: MySimplePresenter by inject()


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContentView(R.layout.activity_main)


    ...

    }


    }

    View Slide

  45. Ktor Hammer - @marcoGomier
    val appModule = module {


    single { JokeLocalDataSourceImpl() }


    single { JokeRepositoryImpl(get()) }


    }

    View Slide

  46. Ktor Hammer - @marcoGomier
    fun Application.module() {


    install(Koin) {


    slf4jLogger()


    modules(koinModules)


    }


    ...

    }

    View Slide

  47. Ktor Hammer - @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

  48. Ktor Hammer @marcoGomier
    Logging

    View Slide

  49. Ktor Hammer - @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

  50. Ktor Hammer - @marcoGomier
    Timber
    https://github.com/JakeWharton/timber
    Timber.d(“Message")


    Timber.v("Message")


    Timber.i("Message")


    Timber.w("Message")


    Timber.wtf("Message")


    Timber.e(exception)

    View Slide

  51. Ktor Hammer - @marcoGomier
    SLF4J
    http://www.slf4j.org/index.html
    fun Application.module(testing: Boolean = false) {


    log.info("Hello from module!")


    }

    View Slide

  52. Ktor Hammer - @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

  53. Ktor Hammer - @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!")


    }
    val logger = LoggerFactory.getLogger(MyClass
    ::
    class.java)

    View Slide

  54. Ktor Hammer - @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

  55. Ktor Hammer - @marcoGomier
    logger.trace("Message")


    logger.debug("Message")


    logger.info("Message")


    logger.warn("Message")


    logger.error("Message")
    SLF4J

    View Slide

  56. Ktor Hammer - @marcoGomier
    logback.xml

    View Slide

  57. Ktor Hammer - @marcoGomier









    %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>


    logback.xml

    View Slide

  58. Ktor Hammer - @marcoGomier









    %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>




    logback.xml

    View Slide



  59. %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>





    />

    />

    View Slide

  60. Ktor Hammer - @marcoGomier



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

    pattern>



    encoder>



    appender>





    />

    />


    root>


    />

    />

    />


    configuration>


    View Slide

  61. Ktor Hammer @marcoGomier
    Secrets

    View Slide

  62. Ktor Hammer - @marcoGomier
    local.properties
    tmdbKey="private_key"

    View Slide

  63. Ktor Hammer - @marcoGomier
    build.gradle.kts
    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
    ?:
    "")}\"")

    View Slide

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

    View Slide

  65. Ktor Hammer - @marcoGomier
    application.conf

    View Slide

  66. Ktor Hammer - @marcoGomier
    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


    }


    }
    application.conf

    View Slide

  67. 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"


    //

    application.conf

    View Slide

  68. Ktor Hammer - @marcoGomier
    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=UT


    user = "root"


    password = "password"


    maxPoolSize = 3


    }


    }

    View Slide

  69. Ktor Hammer - @marcoGomier
    }


    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=UT


    user = "root"


    password = "password"


    maxPoolSize = 3


    }


    }

    View Slide

  70. Ktor Hammer - @marcoGomier
    data class ServerConfig(


    val isProd: Boolean


    )

    View Slide

  71. Ktor Hammer - @marcoGomier
    data class ServerConfig(


    val isProd: Boolean


    )
    class AppConfig {


    lateinit var serverConfig: ServerConfig


    //
    Place here other configurations


    }

    View Slide

  72. Ktor Hammer - @marcoGomier
    data class ServerConfig(


    val isProd: Boolean


    )
    class AppConfig {


    lateinit var serverConfig: ServerConfig


    //
    Place here other configurations


    }
    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)


    }

    View Slide

  73. Ktor Hammer - @marcoGomier
    fun Application.module() {


    ...



    setupConfig()


    val appConfig by inject()


    ...

    }


    View Slide

  74. Ktor Hammer - @marcoGomier
    java -jar ktor-backend.jar -config=/config-folder/application.conf

    View Slide

  75. Ktor Hammer @marcoGomier
    Testing

    View Slide

  76. Ktor Hammer - @marcoGomier
    • Unit Test
    ->
    Just regular Kotlin Unit tests


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

    View Slide

  77. Ktor Hammer - @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

  78. Ktor Hammer @marcoGomier
    Deploy

    View Slide

  79. Ktor Hammer - @marcoGomier
    • aar


    • apk


    • aab
    Deploy

    View Slide

  80. Ktor Hammer - @marcoGomier https://github.com/johnrengelman/shadow
    ./gradlew assembleRelease
    ./gradlew bundleRelease

    View Slide

  81. Ktor Hammer - @marcoGomier
    • Fat JAR


    • Executable JVM application


    • WAR


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

    View Slide

  82. Ktor Hammer - @marcoGomier
    • Fat JAR


    • Executable JVM application


    • WAR


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

    View Slide

  83. Ktor Hammer - @marcoGomier https://github.com/johnrengelman/shadow
    build.gradle.kts
    plugins {


    ...

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


    }

    View Slide

  84. Ktor Hammer - @marcoGomier https://github.com/johnrengelman/shadow
    build.gradle.kts
    tasks {


    shadowJar {


    manifest {


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


    }


    }


    }
    application {


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


    }

    View Slide

  85. Ktor Hammer - @marcoGomier https://github.com/johnrengelman/shadow
    ./gradlew shadowJar

    View Slide

  86. Ktor Hammer - @marcoGomier

    View Slide

  87. Ktor Hammer @marcoGomier
    Conclusions

    View Slide

  88. Ktor Hammer - @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

  89. Ktor Hammer - @marcoGomier https://github.com/prof18/ktor-chuck-norris-sample

    View Slide

  90. 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/


    • https:
    //
    www.marcogomiero.com/posts/2022/ktor-migration-liquibase/


    • https:
    //
    www.marcogomiero.com/posts/2022/ktor-setup-documentation/


    • https:
    //
    www.marcogomiero.com/posts/2022/ktor-setup-documentation/


    • https:
    //
    www.marcogomiero.com/posts/2022/backend-from-mobile-ktor/

    View Slide

  91. Marco Gomiero
    Ktor Hammer
    Thank you!
    > Twitter: @marcoGomier

    > Github: prof18

    > Website: marcogomiero.com
    👨💻 Senior Android Engineer @ TIER

    Google Developer Expert for Kotlin

    View Slide