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

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
  2. Ktor Hammer - @marcoGomier • A career change 🤷 •

    Side project • Understand how things works “on the other side” • Help you teammates 🤔 Why?
  3. Ktor Hammer - @marcoGomier Ktor • Kotlin • Lightweight and

    flexible • Asynchronous with Coroutines • Unopinionated
  4. 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
  5. Ktor Hammer - @marcoGomier Resource Repository Local Data Source Remote

    Data Source Application Activity/Fragment ViewModel Repository Local Data Source Remote Data Source Application
  6. 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
  7. Ktor Hammer - @marcoGomier Application class MyApp : Application() {

    override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } initAnalytics() initCrashReporting() initRandomLib() } }
  8. 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() } }
  9. Ktor Hammer - @marcoGomier Application slf4jLogger() modules(koinModules) } setupConfig() setupDatabase()

    install(ContentNegotiation) { json() } install(CallLogging) { level = Level.INFO } install(Locations)
  10. Ktor Hammer - @marcoGomier Plugin https://ktor.io/docs/plugins.html • Add a specific

    feature to your backend • Highly customisable • No plugin activated by default
  11. Ktor Hammer - @marcoGomier Application setupConfig() setupDatabase() install(ContentNegotiation) { json()

    } install(CallLogging) { level = Level.INFO } install(Locations) routing { setupEndpoint() } }
  12. Ktor Hammer - @marcoGomier Application install(ContentNegotiation) { json() } install(CallLogging)

    { level = Level.INFO } install(Locations) routing { setupEndpoint() } }
  13. Ktor Hammer - @marcoGomier Presentation class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... } }
  14. Ktor Hammer - @marcoGomier Presentation fun Route.jokeEndpoint() { val jokeRepository

    by inject<JokeRepository>() get<JokeEndpoint.Random> { call.respond(jokeRepository.getRandomJoke()) } post<JokeEndpoint.Watch> { apiCallParams -> val name = apiCallParams.name jokeRepository.watch(name) call.respond("Ok") } }
  15. Ktor Hammer - @marcoGomier Domain class JokeRepositoryImpl( private val jokeLocalDataSource:

    JokeLocalDataSource ) : JokeRepository { override suspend fun getRandomJoke(): JokeDTO { // ... } }
  16. 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
  17. Ktor Hammer - @marcoGomier @Dao interface AllowedAppDAO { @Query("SELECT *

    FROM allowed_app") suspend fun getAllAllowedApps(): List<AllowedApp> } @Entity(tableName = "allowed_app") data class AllowedApp( @PrimaryKey val packageName: String, @ColumnInfo val appName: String )
  18. Ktor Hammer - @marcoGomier class LocalDatasource( private val db: RoomDatabase

    ) { suspend fun getAllowedApps(): List<AllowedApp> { 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<AllowedApp> }
  19. Ktor Hammer - @marcoGomier object JokeTable: IdTable<String>(name = "joke") {

    val createdAt = datetime("created_at") val updatedAt = datetime("updated_at") val value = text("value") override val id: Column<EntityID<String >> = varchar("joke_id", 255).entityId() override val primaryKey: PrimaryKey = PrimaryKey(id) }
  20. Ktor Hammer - @marcoGomier class Joke(id: EntityID<String>): Entity<String>(id) { companion

    object: EntityClass<String, Joke>(JokeTable) var createdAt by JokeTable.createdAt var updatedAt by JokeTable.updatedAt var value by JokeTable.value } object JokeTable: IdTable<String>(name = "joke") { val createdAt = datetime("created_at") val updatedAt = datetime("updated_at") val value = text("value") override val id: Column<EntityID<String >> = varchar("joke_id", 255).entityId() override val primaryKey: PrimaryKey = PrimaryKey(id) }
  21. Ktor Hammer - @marcoGomier class JokeLocalDataSourceImpl : JokeLocalDataSource { override

    suspend fun getAllJokes(): List<Joke> { val joke = newSuspendedTransaction { val query = JokeTable.selectAll() Joke.wrapRows(query).toList() } } } object JokeTable: IdTable<String>(name = "joke") { val createdAt = datetime("created_at") val updatedAt = datetime("updated_at") val value = text("value") override val id: Column<EntityID<String >> = varchar("joke_id", 255).entityId() override val primaryKey: PrimaryKey = PrimaryKey(id) } class Joke(id: EntityID<String>): Entity<String>(id) { companion object: EntityClass<String, Joke>(JokeTable) var createdAt by JokeTable.createdAt var updatedAt by JokeTable.updatedAt var value by JokeTable.value }
  22. Ktor Hammer - @marcoGomier class JokeLocalDataSourceImpl : JokeLocalDataSource { override

    suspend fun getAllJokes(): List<Joke> { val joke = newSuspendedTransaction { val query = JokeTable.selectAll() Joke.wrapRows(query).toList() } } } object JokeTable: IdTable<String>(name = "joke") { val createdAt = datetime("created_at") val updatedAt = datetime("updated_at") val value = text("value") override val id: Column<EntityID<String >> = varchar("joke_id", 255).entityId() override val primaryKey: PrimaryKey = PrimaryKey(id) } class Joke(id: EntityID<String>): Entity<String>(id) { companion object: EntityClass<String, Joke>(JokeTable) var createdAt by JokeTable.createdAt var updatedAt by JokeTable.updatedAt var value by JokeTable.value }
  23. Ktor Hammer - @marcoGomier object JokeTable: IdTable<String>(name = "joke") {

    val createdAt = datetime("created_at") val updatedAt = datetime("updated_at") val value = text("value") override val id: Column<EntityID<String >> = varchar("joke_id", 255).entityId() override val primaryKey: PrimaryKey = PrimaryKey(id) } class Joke(id: EntityID<String>): Entity<String>(id) { companion object: EntityClass<String, Joke>(JokeTable) var createdAt by JokeTable.createdAt var updatedAt by JokeTable.updatedAt var value by JokeTable.value } class JokeLocalDataSourceImpl : JokeLocalDataSource { override suspend fun getAllJokes(): List<Joke> { val joke = newSuspendedTransaction { val query = JokeTable.selectAll() Joke.wrapRows(query).toList() } } }
  24. Ktor Hammer - @marcoGomier object JokeTable: IdTable<String>(name = "joke") {

    val createdAt = datetime("created_at") val updatedAt = datetime("updated_at") val value = text("value") override val id: Column<EntityID<String >> = varchar("joke_id", 255).entityId() override val primaryKey: PrimaryKey = PrimaryKey(id) } class Joke(id: EntityID<String>): Entity<String>(id) { companion object: EntityClass<String, Joke>(JokeTable) var createdAt by JokeTable.createdAt var updatedAt by JokeTable.updatedAt var value by JokeTable.value } class JokeLocalDataSourceImpl : JokeLocalDataSource { override suspend fun getAllJokes(): List<Joke> { val joke = newSuspendedTransaction { val query = JokeTable.selectAll() Joke.wrapRows(query).toList() } } }
  25. 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)
  26. Ktor Hammer - @marcoGomier val appModule = module { single<HelloRepository>

    { HelloRepositoryImpl() } factory { MySimplePresenter(get()) } }
  27. Ktor Hammer - @marcoGomier class MyApplication : Application() { override

    fun onCreate() { super.onCreate() startKoin { androidLogger() androidContext(this@MyApplication) modules(appModule) } } }
  28. Ktor Hammer - @marcoGomier class MainActivity : AppCompatActivity() { val

    firstPresenter: MySimplePresenter by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... } }
  29. Ktor Hammer - @marcoGomier val appModule = module { single<JokeLocalDataSource>

    { JokeLocalDataSourceImpl() } single<JokeRepository> { JokeRepositoryImpl(get()) } }
  30. Ktor Hammer - @marcoGomier fun Route.jokeEndpoint() { val jokeRepository by

    inject<JokeRepository>() get<JokeEndpoint.Random> { call.respond(jokeRepository.getRandomJoke()) } post<JokeEndpoint.Watch> { apiCallParams -> val name = apiCallParams.name jokeRepository.watch(name) call.respond("Ok") } }
  31. 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
  32. 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!") }
  33. 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)
  34. Ktor Hammer - @marcoGomier val logger = LoggerFactory.getLogger(MyClass :: class.java)

    inline fun <reified T> T.getLogger(): Logger { return LoggerFactory.getLogger(T :: class.java) } class MyClass { private val logger = getLogger() fun main() { logger.info("Hello World") } }
  35. Ktor Hammer - @marcoGomier <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{YYYY-MM-dd

    HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n </ pattern> </ encoder> </ appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_DEST}/ktor-chuck-norris-sample.log </ file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- daily rollover --> <fileNamePattern>${LOG_DEST}/ktor-chuck-norris-sample.%d{yyyy-MM-dd}.log </ fileNamePattern> <!-- keep 90 days' worth of history capped at 3GB total size --> <maxHistory>${LOG_MAX_HISTORY} </ maxHistory> <totalSizeCap>3GB </ totalSizeCap> </ rollingPolicy> <encoder> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n </ pattern> </ encoder> </ appender> <root level="info"> <appender-ref ref="STDOUT" /> <appender-ref ref="FILE" /> </ root> <logger name="org.eclipse.jetty" level="INFO" /> <logger name="io.netty" level="INFO" /> <logger name="org.quartz" level="INFO" /> </ configuration> logback.xml
  36. Ktor Hammer - @marcoGomier <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{YYYY-MM-dd

    HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n </ pattern> </ encoder> </ appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_DEST}/ktor-chuck-norris-sample.log </ file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- daily rollover --> <fileNamePattern>${LOG_DEST}/ktor-chuck-norris-sample.%d{yyyy-MM-dd}.log </ fileNamePattern> <!-- keep 90 days' worth of history capped at 3GB total size --> <maxHistory>${LOG_MAX_HISTORY} </ maxHistory> <totalSizeCap>3GB </ totalSizeCap> </ rollingPolicy> logback.xml
  37. <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n </ pattern> </

    encoder> </ appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_DEST}/ktor-chuck-norris-sample.log </ file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- daily rollover --> <fileNamePattern>${LOG_DEST}/ktor-chuck-norris-sample.%d{yyyy-MM-dd}.log </ fileNamePattern> <!-- keep 90 days' worth of history capped at 3GB total size --> <maxHistory>${LOG_MAX_HISTORY} </ maxHistory> <totalSizeCap>3GB </ totalSizeCap> </ rollingPolicy> <encoder> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n </ pattern> </ encoder> </ appender> <root level="info"> <appender-ref ref="STDOUT" /> />
  38. Ktor Hammer - @marcoGomier <encoder> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}

    - % </ pattern> </ encoder> </ appender> <root level="info"> <appender-ref ref="STDOUT" /> <appender-ref ref="FILE" /> </ root> <logger name="org.eclipse.jetty" level="INFO" /> <logger name="io.netty" level="INFO" /> <logger name="org.quartz" level="INFO" /> </ configuration>
  39. 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 ?: "")}\"")
  40. 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
  41. 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
  42. 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 } }
  43. 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 } }
  44. Ktor Hammer - @marcoGomier data class ServerConfig( val isProd: Boolean

    ) class AppConfig { lateinit var serverConfig: ServerConfig // Place here other configurations }
  45. 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<AppConfig>() // Server val serverObject = environment.config.config("ktor.server") val isProd = serverObject.property("isProd").getString().toBoolean() appConfig.serverConfig = ServerConfig(isProd) }
  46. Ktor Hammer - @marcoGomier • Unit Test -> Just regular

    Kotlin Unit tests • androidTest -> TestEngine Testing https://ktor.io/docs/testing.html
  47. 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
  48. Ktor Hammer - @marcoGomier • Fat JAR • Executable JVM

    application • WAR • GraalVM Deploy https://ktor.io/docs/deploy.html
  49. Ktor Hammer - @marcoGomier • Fat JAR • Executable JVM

    application • WAR • GraalVM Deploy https://ktor.io/docs/deploy.html
  50. 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") }
  51. 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
  52. 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/
  53. Marco Gomiero Ktor Hammer Thank you! > Twitter: @marcoGomier 


    > Github: prof18 
 > Website: marcogomiero.com 👨💻 Senior Android Engineer @ TIER 
 Google Developer Expert for Kotlin