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

From Mobile to Backend with Kotlin and Ktor - p...

From Mobile to Backend with Kotlin and Ktor - plDroid

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

May 31, 2023
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. @marcoGomier From Mobile to Backend with Kotlin and Ktor 👨💻

    Senior Android Engineer @ TIER 
 Google Developer Expert for Kotlin Marco Gomiero
  2. @marcoGomier • A career change 🤷 • Side project •

    Understand how things works “on the other side” • Help you teammates 🤔 Why?
  3. @marcoGomier Resource Repository Local Data Source Remote Data Source Application

    Activity/Fragment ViewModel Repository Local Data Source Remote Data Source Application
  4. @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
  5. @marcoGomier Application class MyApp : Application() { override fun onCreate()

    { super.onCreate() if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } initAnalytics() initCrashReporting() initRandomLib() } }
  6. @marcoGomier Application fun Application.module() { install(Koin) { slf4jLogger() modules(koinModules) }

    setupConfig() setupDatabase() install(ContentNegotiation) { json() } install(CallLogging) { level = Level.INFO } install(Locations) routing { setupEndpoint() } }
  7. @marcoGomier Plugin https://ktor.io/docs/plugins.html • Add a specific feature to your

    backend • Highly customisable • No plugin activated by default
  8. @marcoGomier Application install(ContentNegotiation) { json() } install(CallLogging) { level =

    Level.INFO }ㅤ install(Locations) routing { setupEndpoint() } }
  9. @marcoGomier Presentation class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... } }
  10. @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") } }
  11. @marcoGomier Domain class JokeRepositoryImpl( private val jokeLocalDataSource: JokeLocalDataSource ) :

    JokeRepository { override suspend fun getRandomJoke(): JokeDTO { // ... } }
  12. @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
  13. @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 )
  14. @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> }
  15. @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) }
  16. @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) }
  17. @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 }
  18. @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 }
  19. @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() } } }
  20. @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() } } }
  21. @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)
  22. @marcoGomier class MyApplication : Application() { override fun onCreate() {

    super.onCreate() startKoin { androidLogger() androidContext(this@MyApplication) modules(appModule) } } }
  23. @marcoGomier class MainActivity : AppCompatActivity() { val firstPresenter: MySimplePresenter by

    inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... } }
  24. @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") } }
  25. @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
  26. @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)
  27. @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") } }
  28. @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
  29. @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
  30. <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" /> />
  31. @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>
  32. @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 ?: "")}\"")
  33. @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
  34. 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
  35. @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 } }
  36. @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 } }
  37. @marcoGomier data class ServerConfig( val isProd: Boolean ) class AppConfig

    { lateinit var serverConfig: ServerConfig // Place here other configurations }
  38. @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) }
  39. @marcoGomier • Unit Test -> Just regular Kotlin Unit tests

    • androidTest -> TestEngine Testing https://ktor.io/docs/testing.html
  40. @marcoGomier @Test fun testRoot() = testApplication { val response =

    client.get("/") assertEquals(HttpStatusCode.OK, response.status) assertEquals("Hello, world!", response.bodyAsText()) } https://ktor.io/docs/testing.html
  41. @marcoGomier • Fat JAR • Executable JVM application • WAR

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

    • GraalVM Deploy https://ktor.io/docs/deploy.html
  43. @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
  44. Bibliography / Useful Links • https: / / ktor.io/ •

    https: / / ktor.io/learn/ • https: / / ktor.io/docs/welcome.html • https: / / ktor.io/docs/migrating-2.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-jobs-quartz/ • https: / / www.marcogomiero.com/posts/2022/backend-from-mobile-ktor/
  45. @marcoGomier Thank you! > Twitter: @marcoGomier 
 > Github: prof18

    
 > Website: marcogomiero.com 
 > Mastodon: androiddev.social/@marcogom Marco Gomiero 👨💻 Senior Android Engineer @ TIER 
 Google Developer Expert for Kotlin