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.

9da5d5cc4b6a9f28058152e28364b02a?s=128

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
  2. FOSDEM - @marcoGomier Marco Gomiero 👨💻 Android Engineer @ TIER

    🛴 🇩🇪 🇮🇹 
 Google Developer Expert for Kotlin > Twitter: @marcoGomier 
 > Github: prof18 
 > Website: marcogomiero.com
  3. FOSDEM - @marcoGomier 🤔 Why?

  4. FOSDEM - @marcoGomier • Side project • Understand how things

    works “on the other side” • Help you teammates 🤔 Why?
  5. FOSDEM - @marcoGomier * based on a true story

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

  7. FOSDEM - @marcoGomier Ktor • Kotlin • Lightweight and flexible

    • Asynchronous with Coroutines • Unopinionated
  8. FOSDEM - @marcoGomier Unopinionated • Whatever architecture • Whatever pattern

    Knowledge Transfer
  9. FOSDEM @marcoGomier Architecture

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

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

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

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

    fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } initAnalytics() initCrashReporting() initRandomLib() } }
  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
  16. FOSDEM - @marcoGomier Plugin https://ktor.io/docs/plugins.html • Add a specific feature

    to your backend • Highly customisable • No plugin activated by default
  17. FOSDEM - @marcoGomier Plugin https://ktor.io/docs/

  18. FOSDEM - @marcoGomier Domain Data Presentation Application

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

    fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... } }
  20. FOSDEM - @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") } } Presentation
  21. FOSDEM - @marcoGomier Domain Data Presentation Application

  22. FOSDEM - @marcoGomier Domain class JokeRepositoryImpl( private val jokeLocalDataSource: JokeLocalDataSource

    ) : JokeRepository { override suspend fun getRandomJoke(): JokeDTO { // ... } }
  23. FOSDEM - @marcoGomier Domain Data Presentation Application

  24. FOSDEM - @marcoGomier Room 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> } Data
  25. FOSDEM - @marcoGomier Exposed https://github.com/JetBrains/Exposed

  26. FOSDEM - @marcoGomier Exposed 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 } Data
  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)
  28. FOSDEM @marcoGomier Dependency Injection

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

  30. FOSDEM - @marcoGomier val appModule = module { single<HelloRepository> {

    HelloRepositoryImpl() } factory { MySimplePresenter(get()) } }
  31. FOSDEM - @marcoGomier class MyApplication : Application() { override fun

    onCreate() { super.onCreate() startKoin { androidLogger() androidContext(this@MyApplication) modules(appModule) } } }
  32. FOSDEM - @marcoGomier class MainActivity : AppCompatActivity() { val firstPresenter:

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

    JokeLocalDataSourceImpl() } single<JokeRepository> { JokeRepositoryImpl(get()) } }
  34. FOSDEM - @marcoGomier fun Application.module() { install(Koin) { slf4jLogger() modules(koinModules)

    } ... }
  35. FOSDEM - @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") } }
  36. FOSDEM @marcoGomier Logging

  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
  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
  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!") }
  40. FOSDEM - @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") } }
  41. FOSDEM - @marcoGomier logger.trace("Message") logger.debug("Message") logger.info("Message") logger.warn("Message") logger.error("Message") SLF4J

  42. FOSDEM - @marcoGomier logback.xml

  43. FOSDEM - @marcoGomier logback.xml <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>
  44. FOSDEM @marcoGomier Secrets

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

  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
  47. FOSDEM - @marcoGomier tmdbKey="private_key" buildConfigField("String", "TMDB_KEY", "\"${"tmdbKey".byProperty ?: "")}\"") val

    myKey: String = BuildConfig.TMDB_KEY NetworkApiInterceptor.kt
  48. FOSDEM - @marcoGomier application.conf

  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 } }
  50. FOSDEM - @marcoGomier 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) } data class ServerConfig( val isProd: Boolean ) class AppConfig { lateinit var serverConfig: ServerConfig // Place here other configurations }
  51. FOSDEM - @marcoGomier fun Application.module() { ... setupConfig() val appConfig

    by inject<AppConfig>() ... }
  52. FOSDEM - @marcoGomier java -jar ktor-backend.jar -config=/config-folder/application.conf

  53. FOSDEM @marcoGomier Testing

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

    Unit tests • androidTest -> TestEngine Testing https://ktor.io/docs/testing.html
  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
  56. FOSDEM @marcoGomier Deploy

  57. FOSDEM - @marcoGomier Deploy • aar • apk • aab

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

  59. FOSDEM - @marcoGomier Deploy • Fat JAR • Executable JVM

    application • WAR • GraalVM https://ktor.io/docs/deploy.html
  60. FOSDEM - @marcoGomier • Fat JAR • Executable JVM application

    • WAR • GraalVM https://ktor.io/docs/deploy.html Deploy
  61. FOSDEM - @marcoGomier build.gradle.kts https://github.com/johnrengelman/shadow plugins { ... id("com.github.johnrengelman.shadow") version

    "7.0.0" }
  62. FOSDEM - @marcoGomier https://github.com/johnrengelman/shadow tasks { shadowJar { manifest {

    attributes(Pair("Main-Class", "io.ktor.server.netty.EngineMain")) } } } build.gradle.kts
  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
  64. FOSDEM - @marcoGomier https://github.com/johnrengelman/shadow ./gradlew shadowJar

  65. FOSDEM - @marcoGomier

  66. FOSDEM @marcoGomier Conclusions

  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
  68. FOSDEM - @marcoGomier https://github.com/prof18/ktor-chuck-norris-sample

  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/
  70. Marco Gomiero FOSDEM Thank you! > Twitter: @marcoGomier 
 >

    Github: prof18 
 > Website: marcogomiero.com