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

From Mobile to Backend with Kotlin and Ktor | droidcon Webinar

From Mobile to Backend with Kotlin and Ktor | droidcon Webinar

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

June 16, 2022
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. Marco Gomiero
    droidcon webinar
    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. droidcon webinar - @marcoGomier
    🤔 Why?

    View Slide

  3. droidcon webinar - @marcoGomier
    • A career change 🤷


    • Side project


    • Understand how things works “on the other side”


    • Help you teammates
    🤔 Why?

    View Slide

  4. droidcon webinar - @marcoGomier
    * based on a true story

    View Slide

  5. droidcon webinar - @marcoGomier
    Ktor
    https://ktor.io/

    View Slide

  6. droidcon webinar - @marcoGomier
    Ktor
    • Kotlin


    • Lightweight and flexible


    • Asynchronous with Coroutines


    • Unopinionated

    View Slide

  7. droidcon webinar - @marcoGomier
    Unopinionated
    • Whatever architecture


    • Whatever pattern
    Knowledge Transfer

    View Slide

  8. droidcon webinar @marcoGomier
    Architecture

    View Slide

  9. droidcon webinar - @marcoGomier
    Android
    Activity/Fragment
    ViewModel
    Repository
    Local Data Source Remote Data Source
    Application

    View Slide

  10. droidcon webinar - @marcoGomier
    Resource*
    Repository
    Local Data Source Remote Data Source
    Application
    * or Controller
    Ktor

    View Slide

  11. droidcon webinar - @marcoGomier
    Resource
    Repository
    Local Data Source Remote Data Source
    Application
    Activity/Fragment
    ViewModel
    Repository
    Local Data Source Remote Data Source
    Application

    View Slide

  12. droidcon webinar - @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

  13. droidcon webinar - @marcoGomier
    Application
    class MyApp : Application() {


    override fun onCreate() {


    super.onCreate()


    if (BuildConfig.DEBUG) {


    Timber.plant(Timber.DebugTree())


    }


    initAnalytics()


    initCrashReporting()


    initRandomLib()


    }


    }

    View Slide

  14. droidcon webinar - @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

  15. droidcon webinar - @marcoGomier
    Application


    slf4jLogger()


    modules(koinModules)


    }


    setupConfig()


    setupDatabase()


    install(ContentNegotiation) {


    json()


    }


    install(CallLogging) {


    level = Level.INFO


    }


    install(Locations)




    View Slide

  16. droidcon webinar - @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. droidcon webinar - @marcoGomier
    Plugin
    https://ktor.io/docs/

    View Slide

  18. droidcon webinar - @marcoGomier
    Application
    setupConfig()


    setupDatabase()


    install(ContentNegotiation) {


    json()


    }


    install(CallLogging) {


    level = Level.INFO


    }


    install(Locations)


    routing {


    setupEndpoint()


    }


    }

    View Slide

  19. droidcon webinar - @marcoGomier
    Application
    install(ContentNegotiation) {


    json()


    }


    install(CallLogging) {


    level = Level.INFO


    }


    install(Locations)


    routing {


    setupEndpoint()


    }


    }

    View Slide

  20. droidcon webinar - @marcoGomier
    Domain
    Data
    Presentation
    Application

    View Slide

  21. droidcon webinar - @marcoGomier
    Presentation
    class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContentView(R.layout.activity_main)


    ...

    }


    }

    View Slide

  22. droidcon webinar - @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

  23. droidcon webinar - @marcoGomier
    Domain
    Data
    Presentation
    Application

    View Slide

  24. droidcon webinar - @marcoGomier
    Domain
    class JokeRepositoryImpl(


    private val jokeLocalDataSource: JokeLocalDataSource


    ) : JokeRepository {


    override suspend fun getRandomJoke(): JokeDTO {


    // ...

    }


    }

    View Slide

  25. droidcon webinar - @marcoGomier
    Domain
    Data
    Presentation
    Application

    View Slide

  26. droidcon webinar - @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

  27. droidcon webinar - @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

  28. droidcon webinar - @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

  29. droidcon webinar - @marcoGomier
    Exposed
    https://github.com/JetBrains/Exposed

    View Slide

  30. droidcon webinar - @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

  31. droidcon webinar - @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

  32. droidcon webinar - @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

  33. droidcon webinar - @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. droidcon webinar - @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

  35. droidcon webinar - @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. droidcon webinar - @marcoGomier
    Joke.new {


    this.createdAt = LocalDateTime.now()


    this.updatedAt = LocalDateTime.now()


    value = "A Joke"


    }

    View Slide

  37. droidcon webinar - @marcoGomier
    EmailTable.deleteWhere {


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


    }

    View Slide

  38. droidcon webinar - @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

  39. droidcon webinar @marcoGomier
    Dependency Injection

    View Slide

  40. droidcon webinar - @marcoGomier https://insert-koin.io/docs/quickstart/ktor
    Koin

    View Slide

  41. droidcon webinar - @marcoGomier
    val appModule = module {


    single { HelloRepositoryImpl() }


    factory { MySimplePresenter(get()) }


    }

    View Slide

  42. droidcon webinar - @marcoGomier
    class MyApplication : Application() {


    override fun onCreate() {


    super.onCreate()




    startKoin {


    androidLogger()


    androidContext([email protected])


    modules(appModule)


    }


    }


    }

    View Slide

  43. droidcon webinar - @marcoGomier
    class MainActivity : AppCompatActivity() {


    val firstPresenter: MySimplePresenter by inject()


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContentView(R.layout.activity_main)


    ...

    }


    }

    View Slide

  44. droidcon webinar - @marcoGomier
    val appModule = module {


    single { JokeLocalDataSourceImpl() }


    single { JokeRepositoryImpl(get()) }


    }

    View Slide

  45. droidcon webinar - @marcoGomier
    fun Application.module() {


    install(Koin) {


    slf4jLogger()


    modules(koinModules)


    }


    ...

    }

    View Slide

  46. droidcon webinar - @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

  47. droidcon webinar @marcoGomier
    Logging

    View Slide

  48. droidcon webinar - @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

  49. droidcon webinar - @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

  50. droidcon webinar - @marcoGomier
    SLF4J
    http://www.slf4j.org/index.html
    fun Application.module(testing: Boolean = false) {


    log.info("Hello from module!")


    }

    View Slide

  51. droidcon webinar - @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

  52. droidcon webinar - @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

  53. droidcon webinar - @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

  54. droidcon webinar - @marcoGomier
    logger.trace("Message")


    logger.debug("Message")


    logger.info("Message")


    logger.warn("Message")


    logger.error("Message")
    SLF4J

    View Slide

  55. droidcon webinar - @marcoGomier
    logback.xml

    View Slide

  56. droidcon webinar - @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

  57. droidcon webinar - @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



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

  59. droidcon webinar - @marcoGomier



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

    pattern>



    encoder>



    appender>





    />

    />


    root>


    />

    />

    />


    configuration>


    View Slide

  60. droidcon webinar @marcoGomier
    Secrets

    View Slide

  61. droidcon webinar - @marcoGomier
    local.properties
    tmdbKey="private_key"

    View Slide

  62. droidcon webinar - @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

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

    View Slide

  64. droidcon webinar - @marcoGomier
    application.conf

    View Slide

  65. droidcon webinar - @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

  66. 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

  67. droidcon webinar - @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

  68. droidcon webinar - @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

  69. droidcon webinar - @marcoGomier
    data class ServerConfig(


    val isProd: Boolean


    )

    View Slide

  70. droidcon webinar - @marcoGomier
    data class ServerConfig(


    val isProd: Boolean


    )
    class AppConfig {


    lateinit var serverConfig: ServerConfig


    //
    Place here other configurations


    }

    View Slide

  71. droidcon webinar - @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

  72. droidcon webinar - @marcoGomier
    fun Application.module() {


    ...



    setupConfig()


    val appConfig by inject()


    ...

    }


    View Slide

  73. droidcon webinar - @marcoGomier
    java -jar ktor-backend.jar -config=/config-folder/application.conf

    View Slide

  74. droidcon webinar @marcoGomier
    Testing

    View Slide

  75. droidcon webinar - @marcoGomier
    • Unit Test
    ->
    Just regular Kotlin Unit tests


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

    View Slide

  76. droidcon webinar - @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

  77. droidcon webinar @marcoGomier
    Deploy

    View Slide

  78. droidcon webinar - @marcoGomier
    • aar


    • apk


    • aab
    Deploy

    View Slide

  79. droidcon webinar - @marcoGomier https://github.com/johnrengelman/shadow
    ./gradlew assembleRelease
    ./gradlew bundleRelease

    View Slide

  80. droidcon webinar - @marcoGomier
    • Fat JAR


    • Executable JVM application


    • WAR


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

    View Slide

  81. droidcon webinar - @marcoGomier
    • Fat JAR


    • Executable JVM application


    • WAR


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

    View Slide

  82. droidcon webinar - @marcoGomier https://github.com/johnrengelman/shadow
    build.gradle.kts
    plugins {


    ...

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


    }

    View Slide

  83. droidcon webinar - @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

  84. droidcon webinar - @marcoGomier https://github.com/johnrengelman/shadow
    ./gradlew shadowJar

    View Slide

  85. droidcon webinar - @marcoGomier

    View Slide

  86. droidcon webinar @marcoGomier
    Conclusions

    View Slide

  87. droidcon webinar - @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

  88. droidcon webinar - @marcoGomier https://github.com/prof18/ktor-chuck-norris-sample

    View Slide

  89. 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

  90. Marco Gomiero
    droidcon webinar
    Thank you!
    > Twitter: @marcoGomier

    > Github: prof18

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

    Google Developer Expert for Kotlin

    View Slide