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

From Mobile to Backend with Kotlin and Ktor | d...

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

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

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

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

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

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

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

    } install(CallLogging) { level = Level.INFO } install(Locations) routing { setupEndpoint() } }
  11. droidcon webinar - @marcoGomier Application install(ContentNegotiation) { json() } install(CallLogging)

    { level = Level.INFO } install(Locations) routing { setupEndpoint() } }
  12. droidcon webinar - @marcoGomier Presentation class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... } }
  13. droidcon webinar - @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") } }
  14. droidcon webinar - @marcoGomier Domain class JokeRepositoryImpl( private val jokeLocalDataSource:

    JokeLocalDataSource ) : JokeRepository { override suspend fun getRandomJoke(): JokeDTO { // ... } }
  15. 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
  16. droidcon webinar - @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 )
  17. droidcon webinar - @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> }
  18. droidcon webinar - @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) }
  19. droidcon webinar - @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) }
  20. droidcon webinar - @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 }
  21. droidcon webinar - @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. droidcon webinar - @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() } } }
  23. droidcon webinar - @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. 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)
  25. droidcon webinar - @marcoGomier val appModule = module { single<HelloRepository>

    { HelloRepositoryImpl() } factory { MySimplePresenter(get()) } }
  26. droidcon webinar - @marcoGomier class MyApplication : Application() { override

    fun onCreate() { super.onCreate() startKoin { androidLogger() androidContext(this@MyApplication) modules(appModule) } } }
  27. droidcon webinar - @marcoGomier class MainActivity : AppCompatActivity() { val

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

    { JokeLocalDataSourceImpl() } single<JokeRepository> { JokeRepositoryImpl(get()) } }
  29. droidcon webinar - @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") } }
  30. 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
  31. 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!") }
  32. 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)
  33. droidcon webinar - @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") } }
  34. droidcon webinar - @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
  35. droidcon webinar - @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
  36. <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" /> />
  37. droidcon webinar - @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>
  38. 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 ?: "")}\"")
  39. 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
  40. 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
  41. 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 } }
  42. 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 } }
  43. droidcon webinar - @marcoGomier data class ServerConfig( val isProd: Boolean

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

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

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

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


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