Slide 1

Slide 1 text

Marco Gomiero FOSDEM From Mobile to Backend with Kotlin and Ktor

Slide 2

Slide 2 text

FOSDEM - @marcoGomier Marco Gomiero 👨💻 Android Engineer @ TIER 🛴 🇩🇪 🇮🇹 
 Google Developer Expert for Kotlin > Twitter: @marcoGomier 
 > Github: prof18 
 > Website: marcogomiero.com

Slide 3

Slide 3 text

FOSDEM - @marcoGomier 🤔 Why?

Slide 4

Slide 4 text

FOSDEM - @marcoGomier • Side project • Understand how things works “on the other side” • Help you teammates 🤔 Why?

Slide 5

Slide 5 text

FOSDEM - @marcoGomier * based on a true story

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

FOSDEM - @marcoGomier Ktor • Kotlin • Lightweight and flexible • Asynchronous with Coroutines • Unopinionated

Slide 8

Slide 8 text

FOSDEM - @marcoGomier Unopinionated • Whatever architecture • Whatever pattern Knowledge Transfer

Slide 9

Slide 9 text

FOSDEM @marcoGomier Architecture

Slide 10

Slide 10 text

FOSDEM - @marcoGomier Android Activity/Fragment ViewModel Repository Local Data Source Remote Data Source Application

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

FOSDEM - @marcoGomier Resource Repository Local Data Source Remote Data Source Application Activity/Fragment ViewModel Repository Local Data Source Remote Data Source Application

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

FOSDEM - @marcoGomier Application class MyApp : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } initAnalytics() initCrashReporting() initRandomLib() } }

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

FOSDEM - @marcoGomier Plugin https://ktor.io/docs/plugins.html • Add a specific feature to your backend • Highly customisable • No plugin activated by default

Slide 17

Slide 17 text

FOSDEM - @marcoGomier Plugin https://ktor.io/docs/

Slide 18

Slide 18 text

FOSDEM - @marcoGomier Domain Data Presentation Application

Slide 19

Slide 19 text

FOSDEM - @marcoGomier Presentation class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... } }

Slide 20

Slide 20 text

FOSDEM - @marcoGomier fun Route.jokeEndpoint() { val jokeRepository by inject() get { call.respond(jokeRepository.getRandomJoke()) } post { apiCallParams -> val name = apiCallParams.name jokeRepository.watch(name) call.respond("Ok") } } Presentation

Slide 21

Slide 21 text

FOSDEM - @marcoGomier Domain Data Presentation Application

Slide 22

Slide 22 text

FOSDEM - @marcoGomier Domain class JokeRepositoryImpl( private val jokeLocalDataSource: JokeLocalDataSource ) : JokeRepository { override suspend fun getRandomJoke(): JokeDTO { // ... } }

Slide 23

Slide 23 text

FOSDEM - @marcoGomier Domain Data Presentation Application

Slide 24

Slide 24 text

FOSDEM - @marcoGomier Room class LocalDatasource( private val db: RoomDatabase ) { suspend fun getAllowedApps(): List { 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 } Data

Slide 25

Slide 25 text

FOSDEM - @marcoGomier Exposed https://github.com/JetBrains/Exposed

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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)

Slide 28

Slide 28 text

FOSDEM @marcoGomier Dependency Injection

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

FOSDEM - @marcoGomier val appModule = module { single { HelloRepositoryImpl() } factory { MySimplePresenter(get()) } }

Slide 31

Slide 31 text

FOSDEM - @marcoGomier class MyApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidLogger() androidContext(this@MyApplication) modules(appModule) } } }

Slide 32

Slide 32 text

FOSDEM - @marcoGomier class MainActivity : AppCompatActivity() { val firstPresenter: MySimplePresenter by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... } }

Slide 33

Slide 33 text

FOSDEM - @marcoGomier val appModule = module { single { JokeLocalDataSourceImpl() } single { JokeRepositoryImpl(get()) } }

Slide 34

Slide 34 text

FOSDEM - @marcoGomier fun Application.module() { install(Koin) { slf4jLogger() modules(koinModules) } ... }

Slide 35

Slide 35 text

FOSDEM - @marcoGomier fun Route.jokeEndpoint() { val jokeRepository by inject() get { call.respond(jokeRepository.getRandomJoke()) } post { apiCallParams -> val name = apiCallParams.name jokeRepository.watch(name) call.respond("Ok") } }

Slide 36

Slide 36 text

FOSDEM @marcoGomier Logging

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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!") }

Slide 40

Slide 40 text

FOSDEM - @marcoGomier val logger = LoggerFactory.getLogger(MyClass :: class.java) inline fun T.getLogger(): Logger { return LoggerFactory.getLogger(T :: class.java) } class MyClass { private val logger = getLogger() fun main() { logger.info("Hello World") } }

Slide 41

Slide 41 text

FOSDEM - @marcoGomier logger.trace("Message") logger.debug("Message") logger.info("Message") logger.warn("Message") logger.error("Message") SLF4J

Slide 42

Slide 42 text

FOSDEM - @marcoGomier logback.xml

Slide 43

Slide 43 text

FOSDEM - @marcoGomier logback.xml %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ${LOG_DEST}/ktor-chuck-norris-sample.log ${LOG_DEST}/ktor-chuck-norris-sample.%d{yyyy-MM-dd}.log ${LOG_MAX_HISTORY} 3GB %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

Slide 44

Slide 44 text

FOSDEM @marcoGomier Secrets

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

FOSDEM - @marcoGomier tmdbKey="private_key" buildConfigField("String", "TMDB_KEY", "\"${"tmdbKey".byProperty ?: "")}\"") val myKey: String = BuildConfig.TMDB_KEY NetworkApiInterceptor.kt

Slide 48

Slide 48 text

FOSDEM - @marcoGomier application.conf

Slide 49

Slide 49 text

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 } }

Slide 50

Slide 50 text

FOSDEM - @marcoGomier fun Application.setupConfig() { val appConfig by inject() // 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 }

Slide 51

Slide 51 text

FOSDEM - @marcoGomier fun Application.module() { ... setupConfig() val appConfig by inject() ... }

Slide 52

Slide 52 text

FOSDEM - @marcoGomier java -jar ktor-backend.jar -config=/config-folder/application.conf

Slide 53

Slide 53 text

FOSDEM @marcoGomier Testing

Slide 54

Slide 54 text

FOSDEM - @marcoGomier • Unit Test -> Just regular Kotlin Unit tests • androidTest -> TestEngine Testing https://ktor.io/docs/testing.html

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

FOSDEM @marcoGomier Deploy

Slide 57

Slide 57 text

FOSDEM - @marcoGomier Deploy • aar • apk • aab

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

FOSDEM - @marcoGomier build.gradle.kts https://github.com/johnrengelman/shadow plugins { ... id("com.github.johnrengelman.shadow") version "7.0.0" }

Slide 62

Slide 62 text

FOSDEM - @marcoGomier https://github.com/johnrengelman/shadow tasks { shadowJar { manifest { attributes(Pair("Main-Class", "io.ktor.server.netty.EngineMain")) } } } build.gradle.kts

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

FOSDEM - @marcoGomier https://github.com/johnrengelman/shadow ./gradlew shadowJar

Slide 65

Slide 65 text

FOSDEM - @marcoGomier

Slide 66

Slide 66 text

FOSDEM @marcoGomier Conclusions

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

FOSDEM - @marcoGomier https://github.com/prof18/ktor-chuck-norris-sample

Slide 69

Slide 69 text

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/

Slide 70

Slide 70 text

Marco Gomiero FOSDEM Thank you! > Twitter: @marcoGomier 
 > Github: prof18 
 > Website: marcogomiero.com