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.

Marco Gomiero

February 06, 2022
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. FOSDEM - @marcoGomier Marco Gomiero 👨💻 Android Engineer @ TIER

    🛴 🇩🇪 🇮🇹 
 Google Developer Expert for Kotlin > Twitter: @marcoGomier 
 > Github: prof18 
 > Website: marcogomiero.com
  2. FOSDEM - @marcoGomier • Side project • Understand how things

    works “on the other side” • Help you teammates 🤔 Why?
  3. FOSDEM - @marcoGomier Ktor • Kotlin • Lightweight and flexible

    • Asynchronous with Coroutines • Unopinionated
  4. FOSDEM - @marcoGomier Resource Repository Local Data Source Remote Data

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

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

    to your backend • Highly customisable • No plugin activated by default
  9. FOSDEM - @marcoGomier Presentation class MainActivity : AppCompatActivity() { override

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

    ) : JokeRepository { override suspend fun getRandomJoke(): JokeDTO { // ... } }
  12. 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
  13. 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
  14. 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)
  15. FOSDEM - @marcoGomier val appModule = module { single<HelloRepository> {

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

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

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

    JokeLocalDataSourceImpl() } single<JokeRepository> { JokeRepositoryImpl(get()) } }
  19. 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") } }
  20. 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
  21. 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!") }
  22. 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") } }
  23. 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>
  24. 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
  25. 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 } }
  26. 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 }
  27. FOSDEM - @marcoGomier • Unit Test -> Just regular Kotlin

    Unit tests • androidTest -> TestEngine Testing https://ktor.io/docs/testing.html
  28. 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
  29. FOSDEM - @marcoGomier Deploy • Fat JAR • Executable JVM

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

    • WAR • GraalVM https://ktor.io/docs/deploy.html Deploy
  31. FOSDEM - @marcoGomier https://github.com/johnrengelman/shadow tasks { shadowJar { manifest {

    attributes(Pair("Main-Class", "io.ktor.server.netty.EngineMain")) } } } build.gradle.kts
  32. 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
  33. 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
  34. 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/
  35. Marco Gomiero FOSDEM Thank you! > Twitter: @marcoGomier 
 >

    Github: prof18 
 > Website: marcogomiero.com