Slide 1

Slide 1 text

KTOR FOR MOBILE DEVELOPERS DAN KIM @dankim

Slide 2

Slide 2 text

Server side code? Me?

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

It's been...a while

Slide 5

Slide 5 text

The paradox of choice

Slide 6

Slide 6 text

"I wish there was a way to do simple server stuff with what I already know..."

Slide 7

Slide 7 text

Something familiar

Slide 8

Slide 8 text

Something that maintains my focus

Slide 9

Slide 9 text

Something lightweight and simple

Slide 10

Slide 10 text

Enter Ktor!

Slide 11

Slide 11 text

So here's what we're gonna do...

Slide 12

Slide 12 text

A Google "Dashboard"

Slide 13

Slide 13 text

RESTful API

Slide 14

Slide 14 text

Bonus: WebApp!

Slide 15

Slide 15 text

Let's build the RESTful API!

Slide 16

Slide 16 text

Things we'll be using

Slide 17

Slide 17 text

Ktor OAuth2 Retrofit Moshi Google APIs Google Cloud

Slide 18

Slide 18 text

Things we're not going to get into

Slide 19

Slide 19 text

Quickstart Database Sessions Testing

Slide 20

Slide 20 text

Assumptions

Slide 21

Slide 21 text

You've setup an application on Google Cloud

Slide 22

Slide 22 text

You've authenticated to Google's OAuth2

Slide 23

Slide 23 text

Ktor's basic structure

Slide 24

Slide 24 text

ApplicationEngine

Slide 25

Slide 25 text

CIO Jetty Netty Tomcat

Slide 26

Slide 26 text

Netty CIO Jetty Tomcat

Slide 27

Slide 27 text

fun main(args: Array) { io.ktor.server.netty.EngineMain.main(args) }

Slide 28

Slide 28 text

ApplicationEnvironment

Slide 29

Slide 29 text

application.conf

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

fun main(args: Array) { io.ktor.server.netty.EngineMain.main(args) } fun Application.module() { }

Slide 33

Slide 33 text

Application

Slide 34

Slide 34 text

class Application(val environment: ApplicationEnvironment) : ApplicationCallPipeline(), CoroutineScope { ... }1

Slide 35

Slide 35 text

class Application(val environment: ApplicationEnvironment) : ApplicationCallPipeline(), CoroutineScope { ... }1

Slide 36

Slide 36 text

ApplicationCallPipeline

Slide 37

Slide 37 text

ApplicationCallPipeline

Slide 38

Slide 38 text

Phase 1 Phase 2 Phase 3 Phase 5 Phase 4

Slide 39

Slide 39 text

Interceptor Interceptor Interceptor Interceptor Interceptor Interceptor Interceptor Interceptor

Slide 40

Slide 40 text

1. Setup 2. Monitoring 3. Features 4. Call 5. Fallback

Slide 41

Slide 41 text

Features Call

Slide 42

Slide 42 text

Features

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

DefaultHeaders Feature

Slide 45

Slide 45 text

fun Application.module() { }1

Slide 46

Slide 46 text

curl -v http://0.0.0.0:8080/ > < HTTP/1.1 404 Not Found < Content-Length: 0

Slide 47

Slide 47 text

fun Application.module() { }1

Slide 48

Slide 48 text

fun Application.module() { install(DefaultHeaders) }1

Slide 49

Slide 49 text

override fun install(...): DefaultHeaders { ... val feature = DefaultHeaders(config) pipeline.intercept(ApplicationCallPipeline.Features) { feature.intercept(call) } ... }

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Routes

Slide 52

Slide 52 text

fun Application.module() { install(DefaultHeaders) }1

Slide 53

Slide 53 text

fun Application.module() { install(DefaultHeaders) install(Routing) }1

Slide 54

Slide 54 text

override fun install(...): Routing { ... val routing = Routing(pipeline).apply(configure) pipeline.intercept(ApplicationCallPipeline.Call) { routing.interceptor(this) } ... }

Slide 55

Slide 55 text

Features Call

Slide 56

Slide 56 text

fun Application.module() { install(DefaultHeaders) install(Routing) }

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

fun Application.module() { install(DefaultHeaders) install(Routing) }11

Slide 59

Slide 59 text

fun Application.module() { install(DefaultHeaders) install(Routing) { get("/dashboard.json") { call.respondText { "Hello KotlinConf!" }11 }11 }11 }11

Slide 60

Slide 60 text

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!

Slide 61

Slide 61 text

Let's get some data

Slide 62

Slide 62 text

GET Gmail Data

Slide 63

Slide 63 text

@JsonClass(generateAdapter = true)11 data class GmailThreads( @Json(name = "threads") val threads: List )111

Slide 64

Slide 64 text

@JsonClass(generateAdapter = true)11 data class GmailThreads( @Json(name = "threads") val threads: List )111 @JsonClass(generateAdapter = true) data class GmailThread( @Json(name = "id") val id: String, @Json(name = "snippet") val snippet: String )1

Slide 65

Slide 65 text

@GET("gmail/v1/users/{user}/threads") fun getGmailThreads(@Path("user") user: String): Call

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

fun Application.module() { install(DefaultHeaders) install(Routing) { }11 }1

Slide 70

Slide 70 text

fun Application.module() { install(DefaultHeaders) install(Routing) { get("/dashboard.json") { val gmailData = getGmailData() call.respondText { gmailData.toJson() }1111 }111 }11 }1

Slide 71

Slide 71 text

fun Application.module() { install(DefaultHeaders) install(Routing) { get("/dashboard.json") { val gmailData = getGmailData() call.respondText { gmailData.toJson() }1 }1 }1 }1

Slide 72

Slide 72 text

fun Application.module() { install(DefaultHeaders) install(Routing) { get("/dashboard.json") { val gmailData = getGmailData() call.respondText { gmailData.toJson() }1 }1 }1 }1

Slide 73

Slide 73 text

curl -H "Authorization: Bearer " http://0.0.0.0:8080/dashboard.json

Slide 74

Slide 74 text

curl -H "Authorization: Bearer " http://0.0.0.0:8080/dashboard.json

Slide 75

Slide 75 text

GET Gtasks Data

Slide 76

Slide 76 text

@JsonClass(generateAdapter = true)111 data class GoogleTasks( @Json(name = "items") val tasks: List? = null )111

Slide 77

Slide 77 text

@JsonClass(generateAdapter = true)111 data class GoogleTasks( @Json(name = "items") val tasks: List? = 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

Slide 78

Slide 78 text

@JsonClass(generateAdapter = true)111 data class GoogleTasks( @Json(name = "items") val tasks: List? = 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, @Json(name = "emails") val emails: List )1

Slide 79

Slide 79 text

@GET("tasks/v1/lists/{listId}/tasks") fun getTasks(@Path("listId") listId: String): Call

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

curl -H "Authorization: Bearer " http://0.0.0.0:8080/dashboard.json

Slide 88

Slide 88 text

curl -H "Authorization: Bearer " http://0.0.0.0:8080/dashboard.json

Slide 89

Slide 89 text

Gtasks POST

Slide 90

Slide 90 text

@POST("tasks/v1/lists/{listId}/tasks") fun createTask(@Path("listId") listId: String, @Body task: GoogleTask): Call

Slide 91

Slide 91 text

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("",task).execute() return if (response.isSuccessful) { call.respond(HttpStatusCode.OK) } else { handleError(response.errorBody().toString()) null }1 }1

Slide 92

Slide 92 text

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("",task).execute() return if (response.isSuccessful) { call.respond(HttpStatusCode.OK) } else { handleError(response.errorBody().toString()) null }1 }1

Slide 93

Slide 93 text

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("",task).execute() return if (response.isSuccessful) { call.respond(HttpStatusCode.OK) } else { handleError(response.errorBody().toString()) null }1 }1

Slide 94

Slide 94 text

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("",task).execute() return if (response.isSuccessful) { call.respond(HttpStatusCode.OK) } else { handleError(response.errorBody().toString()) null }1 }1

Slide 95

Slide 95 text

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("",task).execute() return if (response.isSuccessful) { call.respond(HttpStatusCode.OK) } else { handleError(response.errorBody().toString()) null }1 }1

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

No content

Slide 98

Slide 98 text

Webapp

Slide 99

Slide 99 text

Authentication

Slide 100

Slide 100 text

OAuth 2

Slide 101

Slide 101 text

No content

Slide 102

Slide 102 text

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 = "", clientSecret = "", defaultScopes = listOf( "profile", "email", "https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/tasks" ) )

Slide 103

Slide 103 text

fun Application.module() { // ... install(Authentication) { oauth("google-oauth") { client = HttpClient(Apache) providerLookup = { googleOauthProvider } urlProvider = { redirectUrl("/login") } } } }

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

fun Application.module() { ... install(Routing) { authenticate("google-oauth") { route("/login") { handle { val principal = call.authentication.principal() ?: error("No principal") saveOAuthToken(principal.accessToken) call.respondRedirect("/") }1 }1 }1 }1 }1

Slide 107

Slide 107 text

Web: showing the data

Slide 108

Slide 108 text

get("/dashboard") { val gmailData = getGmailData() val taskList = getTaskList("") ... }

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

No content

Slide 114

Slide 114 text

Web: posting data

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

No content

Slide 119

Slide 119 text

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( "", task).execute() when (acceptHeader) { "application/json" -> call.respond(HttpStatusCode.OK) else -> call.respondRedirect("/dashboard") }1 }1

Slide 120

Slide 120 text

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( "", task).execute() when (acceptHeader) { "application/json" -> call.respond(HttpStatusCode.OK) else -> call.respondRedirect("/dashboard") }1 }1

Slide 121

Slide 121 text

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( "", task).execute() when (acceptHeader) { "application/json" -> call.respond(HttpStatusCode.OK) else -> call.respondRedirect("/dashboard") }1 }1

Slide 122

Slide 122 text

No content

Slide 123

Slide 123 text

Deployment

Slide 124

Slide 124 text

Google Cloud

Slide 125

Slide 125 text

https://cloud.google.com/ community/tutorials/kotlin-ktor-app- engine-java8

Slide 126

Slide 126 text

XML config files

Slide 127

Slide 127 text

../src/main/webapp/WEB-INF/appengine-web.xml

Slide 128

Slide 128 text

../src/main/webapp/WEB-INF/appengine-web.xml ../src/main/webapp/WEB-INF/web.xml

Slide 129

Slide 129 text

Cloud projects cost money!

Slide 130

Slide 130 text

No content

Slide 131

Slide 131 text

gcloud projects delete [PROJECT_ID]

Slide 132

Slide 132 text

Wrap it up!

Slide 133

Slide 133 text

THANK YOU AND REMEMBER TO VOTE Dan Kim @dankim