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

Ktor for Mobile Developers: Fear the server no more!

Dan Kim
December 05, 2019

Ktor for Mobile Developers: Fear the server no more!

Building mobile apps is what we love to do, but there’s always one nagging problem — writing server side components to support our apps can be surprisingly complex and difficult. There’s a lot of overhead and a bunch of unfamiliar languages, frameworks, and styles of programming that we're not used to.

But fear the server no more, here comes Ktor!

In this talk we'll walk through a real world example of how Ktor (and your existing knowledge of Kotlin) makes building server side components for your app a breeze. We'll start with a basic introduction of Ktor and its components, but we’ll quickly get to building something real. Using a well known service/API, we’ll walk through everything you normally need to get up and running: authentication, getting data, posting data, and deployment.

By the end of the talk you'll have a good idea of how to hook up your own server side components to pretty much any API out there. Let’s conquer the server with Ktor!

Dan Kim

December 05, 2019
Tweet

More Decks by Dan Kim

Other Decks in Programming

Transcript

  1. "I wish there was a way to do simple server

    stuff with what I already know..."
  2. ktor { deployment { port = 8080 watch = [Dev/kotlin/ktor/src]

    }1 application { modules = [com.dankim.ApplicationKt.module] }1 }1
  3. ktor { deployment { port = 8080 watch = [Dev/kotlin/ktor/src]

    }1 application { modules = [com.dankim.ApplicationKt.module] }1 }1
  4. override fun install(...): DefaultHeaders { ... val feature = DefaultHeaders(config)

    pipeline.intercept(ApplicationCallPipeline.Features) { feature.intercept(call) } ... }
  5. curl -v http://0.0.0.0:8080/ < HTTP/1.1 404 Not Found < Date:

    Wed, 13 Nov 2019 02:08:13 GMT < Server: ktor-server-core/1.2.4 ktor-server-core/ 1.2.4 < Content-Length: 0
  6. override fun install(...): Routing { ... val routing = Routing(pipeline).apply(configure)

    pipeline.intercept(ApplicationCallPipeline.Call) { routing.interceptor(this) } ... }
  7. curl -v http://0.0.0.0:8080/ < HTTP/1.1 404 Not Found < Date:

    Wed, 13 Nov 2019 02:08:13 GMT < Server: ktor-server-core/1.2.4 ktor-server-core/ 1.2.4 < Content-Length: 0
  8. curl -v http://0.0.0.0:8080/ < HTTP/1.1 200 OK < Date: Tue,

    03 Dec 2019 15:33:23 GMT < Server: ktor-server-core/1.2.4 ktor-server-core/ 1.2.4 ... Hello KotlinConf!
  9. @JsonClass(generateAdapter = true)11 data class GmailThreads( @Json(name = "threads") val

    threads: List<GmailThread> )111 @JsonClass(generateAdapter = true) data class GmailThread( @Json(name = "id") val id: String, @Json(name = "snippet") val snippet: String )1
  10. private fun getGmailData(): GmailThreads? { val response = getGoogleServicesInstance() .getGmailThreads("[email protected]")

    .execute() return when { response.isSuccessful -> response.body() else -> { handleError(response.errorBody().toString()) null }1 }1 }1
  11. private fun getGmailData(): GmailThreads? { val response = getGoogleServicesInstance() .getGmailThreads("[email protected]")

    .execute() return when { response.isSuccessful -> response.body() else -> { handleError(response.errorBody().toString()) null }1 }1 }1
  12. private fun getGmailData(): GmailThreads? { val response = getGoogleServicesInstance() .getGmailThreads("[email protected]")

    .execute() return when { response.isSuccessful -> response.body() else -> { handleError(response.errorBody().toString()) null }1 }1 }1
  13. fun Application.module() { install(DefaultHeaders) install(Routing) { get("/dashboard.json") { val gmailData

    = getGmailData() call.respondText { gmailData.toJson() }1111 }111 }11 }1
  14. @JsonClass(generateAdapter = true)111 data class GoogleTasks( @Json(name = "items") val

    tasks: List<GoogleTask>? = null )111 @JsonClass(generateAdapter = true)11 data class GoogleTask( @Json(name = "id") val id: String? = null, @Json(name = "title") val title: String? = null, @Json(name = "status") val status: String? = null )11
  15. @JsonClass(generateAdapter = true)111 data class GoogleTasks( @Json(name = "items") val

    tasks: List<GoogleTask>? = null )111 @JsonClass(generateAdapter = true)11 data class GoogleTask( @Json(name = "id") val id: String? = null, @Json(name = "title") val title: String? = null, @Json(name = "status") val status: String? = null )11 @JsonClass(generateAdapter = true)1 data class MyGoogleDashboard( @Json(name = "tasks") val tasks: List<GoogleTask>, @Json(name = "emails") val emails: List<GmailThread> )1
  16. private fun getTaskList(listId: String): GoogleTasks? { val response = getGoogleServicesInstance().getTasks(listId).execute()

    return if (response.isSuccessful) { response.body() } else { handleError(response.errorBody().toString()) null }1 }1
  17. private fun getTaskList(listId: String): GoogleTasks? { val response = getGoogleServicesInstance().getTasks(listId).execute()

    return if (response.isSuccessful) { response.body() } else { handleError(response.errorBody().toString()) null }1 }1
  18. private fun getTaskList(listId: String): GoogleTasks? { val response = getGoogleServicesInstance().getTasks(listId).execute()

    return if (response.isSuccessful) { response.body() } else { handleError(response.errorBody().toString()) null }1 }1
  19. get("/dashboard.json") { val gmailData = getGmailData() val taskList = getTaskList("<listId>")

    val myGoogleDashboard = MyGoogleDashboard( tasks = taskList?.tasks, emails = gmailData?.threads ) call.respondText { myGoogleDashboard.toJson() }1 }1
  20. get("/dashboard.json") { val gmailData = getGmailData() val taskList = getTaskList("<listId>")

    val myGoogleDashboard = MyGoogleDashboard( tasks = taskList?.tasks, emails = gmailData?.threads ) call.respondText { myGoogleDashboard.toJson() }1 }1
  21. get("/dashboard.json") { val gmailData = getGmailData() val taskList = getTaskList("<listId>")

    val myGoogleDashboard = MyGoogleDashboard( tasks = taskList?.tasks, emails = gmailData?.threads ) call.respondText { myGoogleDashboard.toJson() }1 }1
  22. get("/dashboard.json") { val gmailData = getGmailData() val taskList = getTaskList("<listId>")

    val myGoogleDashboard = MyGoogleDashboard( tasks = taskList?.tasks, emails = gmailData?.threads ) call.respondText { myGoogleDashboard.toJson() }1 }1
  23. post("/tasks/new") { val postParams = call.receiveParameters() val taskName = postParams.entries()

    .find { it.key == "taskName" }?.value?.first() val task = GoogleTask(title = taskName) val response = getGoogleServicesInstance().createTask("<id>",task).execute() return if (response.isSuccessful) { call.respond(HttpStatusCode.OK) } else { handleError(response.errorBody().toString()) null }1 }1
  24. post("/tasks/new") { val postParams = call.receiveParameters() val taskName = postParams.entries()

    .find { it.key == "taskName" }?.value?.first() val task = GoogleTask(title = taskName) val response = getGoogleServicesInstance().createTask("<id>",task).execute() return if (response.isSuccessful) { call.respond(HttpStatusCode.OK) } else { handleError(response.errorBody().toString()) null }1 }1
  25. post("/tasks/new") { val postParams = call.receiveParameters() val taskName = postParams.entries()

    .find { it.key == "taskName" }?.value?.first() val task = GoogleTask(title = taskName) val response = getGoogleServicesInstance().createTask("<id>",task).execute() return if (response.isSuccessful) { call.respond(HttpStatusCode.OK) } else { handleError(response.errorBody().toString()) null }1 }1
  26. post("/tasks/new") { val postParams = call.receiveParameters() val taskName = postParams.entries()

    .find { it.key == "taskName" }?.value?.first() val task = GoogleTask(title = taskName) val response = getGoogleServicesInstance().createTask("<listId>",task).execute() return if (response.isSuccessful) { call.respond(HttpStatusCode.OK) } else { handleError(response.errorBody().toString()) null }1 }1
  27. post("/tasks/new") { val postParams = call.receiveParameters() val taskName = postParams.entries()

    .find { it.key == "taskName" }?.value?.first() val task = GoogleTask(title = taskName) val response = getGoogleServicesInstance().createTask("<listId>",task).execute() return if (response.isSuccessful) { call.respond(HttpStatusCode.OK) } else { handleError(response.errorBody().toString()) null }1 }1
  28. curl -v -F 'taskName=This is super cool!' -H "Accept: application/json"

    http://0.0.0.0:8080/tasks/new < HTTP/1.1 200 OK < Date: Tue, 19 Nov 2019 02:58:41 GMT < Server: ktor-server-core/1.2.4 ktor-server-core/1.2.4 < Content-Length: 0
  29. val googleOauthProvider = OAuthServerSettings.OAuth2ServerSettings( name = "google", authorizeUrl = "https://accounts.google.com/o/oauth2/auth",

    accessTokenUrl = "https://www.googleapis.com/oauth2/v3/token", requestMethod = HttpMethod.Post, clientId = "<yourClientId>", clientSecret = "<yourClientSecret>", defaultScopes = listOf( "profile", "email", "https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/tasks" ) )
  30. fun Application.module() { // ... install(Authentication) { oauth("google-oauth") { client

    = HttpClient(Apache) providerLookup = { googleOauthProvider } urlProvider = { redirectUrl("/login") } } } }
  31. fun Application.module() { ... install(Routing) { authenticate("google-oauth") { route("/login") {

    handle { val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>() ?: error("No principal") saveOAuthToken(principal.accessToken) call.respondRedirect("/") }11111 }1111 }1111 }11 }11
  32. fun Application.module() { ... install(Routing) { authenticate("google-oauth") { route("/login") {

    handle { val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>() ?: error("No principal") saveOAuthToken(principal.accessToken) call.respondRedirect("/") }11111 }1111 }111 }11 }11
  33. fun Application.module() { ... install(Routing) { authenticate("google-oauth") { route("/login") {

    handle { val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>() ?: error("No principal") saveOAuthToken(principal.accessToken) call.respondRedirect("/") }1 }1 }1 }1 }1
  34. call.respondHtml { body { p { h1 { +"Dashboard" }

    h2 { +"Tasks:" } ul { taskList?.tasks?.forEach { li { +it.title!! } }1 }1 h2 { +"Emails:" } ul { gmailData?.threads?.forEach { li { +StringEscapeUtils.unescapeHtml4(it.snippet) } }1 }1 }1 }1 }1
  35. body { p { h1 { +"Dashboard" } h2 {

    +"Tasks:" } ul { taskList?.tasks?.forEach { li { +it.title } }11 }11 h2 { +"Emails:" } ul { gmailData?.threads?.forEach { li { +StringEscapeUtils.unescapeHtml4(it.snippet) } }1 }1 }1 }1
  36. body { p { h1 { +"Dashboard" } h2 {

    +"Tasks:" } ul {11 taskList?.tasks?.forEach { li { +it.title } }11 }11 h2 { +"Emails:" } ul {1 gmailData?.threads?.forEach { li { +StringEscapeUtils.unescapeHtml4(it.snippet) } }1 }1 }1 }1
  37. body { p { h1 { +"Dashboard" } h2 {

    +"Tasks:" } ul {11 taskList?.tasks?.forEach { li { +it.title } }11 }11 h2 { +"Emails:" } ul {1 gmailData?.threads?.forEach { li { +StringEscapeUtils.unescapeHtml4(it.snippet) } }1 }1 }1 }1
  38. get("/tasks/new") { call.respondHtml { body { form("/tasks/new", encType = FormEncType.applicationXWwwFormUrlEncoded,

    method = FormMethod.post ) { acceptCharset = "utf-8" p { label { +"Task: " } textInput { name = "taskName" } }1 p { submitInput { value = "send" } }1 }1 }1 }1 }1
  39. get("/tasks/new") { call.respondHtml { body { form("/tasks/new", encType = FormEncType.applicationXWwwFormUrlEncoded,

    method = FormMethod.post ) { acceptCharset = "utf-8" p { label { +"Task: " } textInput { name = "taskName" } }111111 p { submitInput { value = "send" } }11111 }1111 }111 }111 }1
  40. get("/tasks/new") { call.respondHtml { body { form("/tasks/new", encType = FormEncType.applicationXWwwFormUrlEncoded,

    method = FormMethod.post ) { acceptCharset = "utf-8" p { label { +"Task: " } textInput { name = "taskName" } }111111 p { submitInput { value = "send" } }11111 }1111 }111 }11 }1
  41. post("/tasks/new") { val postParams = call.receiveParameters() val taskName = postParams.entries()

    .find { it.key == "taskName" }?.value?.first() val acceptHeader = call.request.headers.get("Accept") val task = GoogleTask(title = taskName) val response = getGoogleServicesInstance().createTask( "<listId>", task).execute() when (acceptHeader) { "application/json" -> call.respond(HttpStatusCode.OK) else -> call.respondRedirect("/dashboard") }1 }1
  42. post("/tasks/new") { val postParams = call.receiveParameters() val taskName = postParams.entries()

    .find { it.key == "taskName" }?.value?.first() val acceptHeader = call.request.headers.get("Accept") val task = GoogleTask(title = taskName) val response = getGoogleServicesInstance().createTask( "<listId>", task).execute() when (acceptHeader) { "application/json" -> call.respond(HttpStatusCode.OK) else -> call.respondRedirect("/dashboard") }1 }1
  43. post("/tasks/new") { val postParams = call.receiveParameters() val taskName = postParams.entries()

    .find { it.key == "taskName" }?.value?.first() val acceptHeader = call.request.headers.get("Accept") val task = GoogleTask(title = taskName) val response = getGoogleServicesInstance().createTask( "<listId>", task).execute() when (acceptHeader) { "application/json" -> call.respond(HttpStatusCode.OK) else -> call.respondRedirect("/dashboard") }1 }1