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

Compose Multiplatform과 Ktor로 플랫폼의 경계를 넘어보자

KwakEuiJin
September 28, 2024

Compose Multiplatform과 Ktor로 플랫폼의 경계를 넘어보자

Compose Multiplatform과 Ktor로 플랫폼의 경계를 넘어보자

KwakEuiJin

September 28, 2024
Tweet

More Decks by KwakEuiJin

Other Decks in Programming

Transcript

  1. ,PUMJO.VMUJQMBUGPSNী ؀ೞৈ पઁ ز੘ పझ౟ ߂ ೐۽ં౟ ҳઑ $MJFOU 4IBSFE

    4FSWFS ,UPSܳ ഝਊೠ 4FSWFS$MJFOU ాन ߊ಴ܳ ળ࠺ೞݶࢲ Ӓېࢲ ,PUMJO 'VMM4UBDL ѐߊ੄ ޷ېח ݾର
  2. ,PUMJO.VMUJQMBUGPSN ੉ۆ ,PUMJO.VMUJQMBUGPSN੉ۆ ৈ۞ ೒ۖಬ "OESPJE J04 ਢ ؘझ௼఑ ١

    ীࢲ ҕా ௏٘ܳ ҕਬೞݶࢲ п ೒ۖಬ੄ ֎੉౭࠳ ӝמਸ ਬ૑ೡ ࣻ ੓ח ӝࣿ ౠ૚ • ௏٘ ੤ࢎਊࢿ ૐо • ֎੉౭࠳ ࢿמ ਬ૑ • ೒ۖಬ߹ ழझఠ݃੉૚ оמ ,PUMJO.VMUJQMBUGPSN
  3. ਺ Ӓۢ ,PUMJO ݽٚ Ѧ աח ,PUMJO݅ ੓ਵݶ ೐۽ં౟ܳ ഒ੗

    ٜ݅ ࣻ ੓ח 'VMM 4UBDLѐߊ੗о ؼ ࣻ ੓૑ ঋਸө ੉ઃ ݋ী ٜ૑ ঋח ࢲߡ ਽׹ чҗ ֢റചػ "1* ޙࢲী ੄ઓೞ૑ ঋইب ؼ Ѫ э਷ؘ ݅ড ,PUMJOਵ۽ $MJFOUm 4FSWFSө૑ ݽف ҕాػ ۽૒ਸ ҕਬೡ ࣻ ੓׮ݶ
  4. ,UPSۆ ࠺زӝ ௿ۄ੉঱౟  ࢲߡ গ೒ܻா੉࣌ਸ औѱ ҳ୷ೡ ࣻ ੓ח

    ೐ۨ੐ਕ௼ .4" ജ҃ࠗఠ ݣ౭೒ۖಬ )UUQDMJFOU ө૑ ૑ਗ
  5. ,UPSۆ ࠺زӝ ௿ۄ੉঱౟  ࢲߡ গ೒ܻா੉࣌ਸ औѱ ҳ୷ೡ ࣻ ੓ח

    ೐ۨ੐ਕ௼ .4" ജ҃ࠗఠ ݣ౭೒ۖಬ )UUQDMJFOU ө૑ ૑ਗ
  6. ,UPSۆ ࠺زӝ ௿ۄ੉঱౟  ࢲߡ গ೒ܻா੉࣌ਸ औѱ ҳ୷ೡ ࣻ ੓ח

    ೐ۨ੐ਕ௼ .4" ജ҃ࠗఠ ݣ౭೒ۖಬ )UUQDMJFOU ө૑ ૑ਗ
  7. 4IBSFE.PEFM$MBTT import kotlinx.serialization.Serializable enum class Priority { Low, Medium, High,

    Vital } @Serializable data class Task( val name: String, val description: String, val priority: Priority )
  8. ,UPSܳ ഝਊೠ $MJFOU4FSWFS ాन  ࢲߡ § embeddedServer: Ktor에서 서버를

    시작하는 함수로, Netty 엔진을 사용하여 설정된 포트에서 서버를 실행 § Application::module: 애플리케이션의 주요 설정과 라우팅을 관리하는 모듈을 지정 § start(wait = true): 서버가 실행되어 클라이언트의 요청을 대기 상태로 유지 fun main() { embeddedServer( Netty, port = SERVER_PORT, host = "0.0.0.0", module = Application::module ).start(wait = true) } fun Application.module() { configureCors() configureJson() configureRoute(repository) }
  9. ,UPS "QQMJDBUJPO .PEVMFҳࢿ Application.module()는 Ktor Application의 전반적인 구조화 담당 각

    모듈 안에 특정 경로를 정의하여 관리하거나 Plugin 설정 등을 통해서 Application 초기화 진행 fun main() { embeddedServer( Netty, port = SERVER_PORT, host = "0.0.0.0", module = Application::module ).start(wait = true) } fun Application.module() { configureCors() configureJson() configureRoute(repository) }
  10. ,UPS "QQMJDBUJPO.PEVMFҳࢿ private fun Application.configureJson() { install(ContentNegotiation) { json() }

    } $POUFOU/FHPUJBUJPO 1MVHJOࢲߡীࢲ +40/ ૒۳ച ߂ ৉૒۳ചܳ ੗زਵ۽ ୊ܻ оמೞѱ Ք ૑ਗೞח ೒۞Ӓੋ private fun Application.configureCors() { install(CORS) { allowHost("kez-lab.github.io") allowMethod(HttpMethod.Get) allowMethod(HttpMethod.Post) allowMethod(HttpMethod.Delete) allowHeader(HttpHeaders.ContentType) allowCredentials = false } } $0341MVHJOࢲߡо ׮ܲ بݫੋীࢲ য়ח ਃ୒ਸ ೲਊೡ ࣻ ੓ب۾ ࢸ੿ೞח ೒۞Ӓੋਵ۽ അ੤ 8BTNਸ ഝਊೞৈ ਢ ࠳ۄ਋੷ীࢲب ೧׼ "1*ܳ ഝਊ೧ঠೞӝ ٸޙী ୶о
  11. "1*3PVUJOHҗ ز੘ ࢸݺ private fun Application.configureRoute(repository: InMemoryTaskRepository) { routing {

    route("/tasks") { get { val tasks = repository.allTasks() call.respond(tasks) } post { val task = call.receive<Task>() repository.addOrUpdateTask(task) call.respond(HttpStatusCode.NoContent) } delete("/{taskName}") { val name = call.parameters["taskName"] if (repository.removeTask(name)) { call.respond(HttpStatusCode.NoContent) } else { call.respond(HttpStatusCode.NotFound) } . . .
  12. "1*3PVUJOHҗ ز੘ ࢸݺ 3PVUJOH੉ۆ § 3PVUJOH ੿੄ § ۄ਋౴ 3PVUJOH

    ਷ ௿ۄ੉঱౟੄ ਃ୒ਸ ୊ܻೞח ߑधҗ ਃ୒੉ ౠ੿ ҃۽۽ ٜয৳ਸ ٸ যڃ ۽૒ਸ ࣻ೯ೡ૑ܳ Ѿ੿ೞח ࢲߡ੄ ҃۽ ࢸ੿ § ৘ܳ ٜয ௿ۄ੉঱౟о UBTLT҃۽۽ (&5ਃ୒ਸ ࠁչਸ ٸ ࢲߡח ೧׼ ਃ୒ਸ ߉ই যڃ ੘সਸ ࣻ೯ೡ૑ܳ ۄ਋౴ ࢸ੿ী ٮۄ ୊ܻ § 3PVUJOH ৉ೡ § ҃۽ ݒೝ63-҃۽৬ ࢲߡ੄ ࠺ૉפझ ۽૒ਸ োѾ § )551ݫࢲ٘ ୊ܻ(&5 1045 165 %&-&5&١ )551ݫࢲ٘ী ٮۄ ׮ܲ ۽૒ प೯ § ؘ੉ఠ ୊ܻ ߂ ਽׹ਃ୒ ؘ੉ఠܳ ߉ই ୊ܻೞҊ ೙ਃೠ ҃਋ ௿ۄ੉঱౟ী ਽׹ਸ ߈ജ
  13. "1*3PVUJOHҗ ز੘ ࢸݺ private fun Application.configureRoute(repository: InMemoryTaskRepository) { routing {

    route("/tasks") { get { val tasks = repository.allTasks() call.respond(tasks) } post { val task = call.receive<Task>() repository.addOrUpdateTask(task) call.respond(HttpStatusCode.NoContent) } delete("/{taskName}") { val name = call.parameters["taskName"] if (repository.removeTask(name)) { call.respond(HttpStatusCode.NoContent) } else { call.respond(HttpStatusCode.NotFound) } . . .
  14. ,UPS $MJFOU (SBEMFࢸ੿ sourceSets { commonMain.dependencies { implementation("io.ktor:ktor-client-core:2.3.12") implementation("io.ktor:ktor-client-content-negotiation:2.3.12") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")

    } androidMain.dependencies { implementation(“io.ktor:ktor-client-android:2.3.12”) } iosMain.dependencies { implementation(l"io.ktor:ktor-client-darwin:2.3.12”) } }
  15. sourceSets { commonMain.dependencies { implementation("io.ktor:ktor-client-core:2.3.12") implementation("io.ktor:ktor-client-content-negotiation:2.3.12") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12") } androidMain.dependencies {

    implementation(“io.ktor:ktor-client-android:2.3.12”) } iosMain.dependencies { implementation(l"io.ktor:ktor-client-darwin:2.3.12”) } } ,UPS $MJFOU (SBEMFࢸ੿ Wasm 설정은 어디있나요??
  16. ,UPS $MJFOUܳ ࢎਊೠ ࢲߡ৬੄ ాन ҳഅ private val httpClient =

    HttpClient { install(ContentNegotiation) { json( Json { encodeDefaults = true isLenient = true coerceInputValues = true ignoreUnknownKeys = true } ) } defaultRequest { url { protocol = URLProtocol.HTTPS } host = "kezlab.site" } } ,UPS )UUQ $MJFOUۆ ׮নೠ ೒ۖಬীࢲ ࠺زӝ ֎౟ਕ௼ ਃ୒ਸ ୊ܻೡ ࣻ ੓ח ё୓ )UUQ$MJFOU ੋझఢझ ࢤࢿ )UUQ$MJFOU ё୓ܳ ࢤࢿೞৈ ௿ۄ੉঱౟ ੋझఢझܳ ୡӝച ߂ ӝࠄ੸ੋ ਃ୒਽׹੄ ୡӝ ࢸ੿ਸ ੿੄
  17. ,UPS $MJFOUܳ ࢎਊೠ ࢲߡ৬੄ ాन ҳഅ private val httpClient =

    HttpClient { install(ContentNegotiation) { json( Json { encodeDefaults = true isLenient = true coerceInputValues = true ignoreUnknownKeys = true } ) } defaultRequest { url { protocol = URLProtocol.HTTPS } host = "kezlab.site" } } ,UPS )UUQ $MJFOUۆ ׮নೠ ೒ۖಬীࢲ ࠺زӝ ֎౟ਕ௼ ਃ୒ਸ ୊ܻೡ ࣻ ੓ח ё୓ )UUQ$MJFOU ੋझఢझ ࢤࢿ )UUQ$MJFOU ё୓ܳ ࢤࢿೞৈ ௿ۄ੉঱౟ ੋझఢझܳ ୡӝച ߂ ӝࠄ੸ੋ ਃ୒਽׹੄ ୡӝ ࢸ੿ਸ ੿੄
  18. ,UPS $MJFOUܳ ࢎਊೠ ࢲߡ৬੄ ాन ҳഅ private val httpClient =

    HttpClient { install(ContentNegotiation) { json( Json { encodeDefaults = true isLenient = true coerceInputValues = true ignoreUnknownKeys = true } ) } defaultRequest { url { protocol = URLProtocol.HTTPS } host = "kezlab.site" } } ,UPS )UUQ $MJFOUۆ ׮নೠ ೒ۖಬীࢲ ࠺زӝ ֎౟ਕ௼ ਃ୒ਸ ୊ܻೡ ࣻ ੓ח ё୓ )UUQ$MJFOU ੋझఢझ ࢤࢿ )UUQ$MJFOU ё୓ܳ ࢤࢿೞৈ ௿ۄ੉঱౟ ੋझఢझܳ ୡӝച ߂ ӝࠄ੸ੋ ਃ୒਽׹੄ ୡӝ ࢸ੿ਸ ੿੄
  19. ,UPS $MJFOU ాन ৘ઁ ௏٘ class TaskApi(private val httpClient: HttpClient)

    { suspend fun getAllTasks(): List<Task> { httpClient.get("tasks").body() } suspend fun removeTask(task: Task) { httpClient.delete("tasks/${task.name}") } suspend fun updateTask(task: Task) { httpClient.post("tasks") { contentType(ContentType.Application.Json) setBody(task) } } }
  20. ӝࠄ 6*ஹನք౟ ҳഅ 8JUI$PNQPTF @Composable fun App() { MaterialTheme {

    LazyColumn(modifier = Modifier.fillMaxSize().padding(8.dp)) { items(tasks) { task -> TaskCard( task, onDelete = { scope.launch { client.removeTask(it) tasks = client.getAllTasks() } }, onUpdate = { currentTask = task } . . .
  21. ӝࠄ 6*ஹನք౟ ҳഅ m "OESPJE 8FC class MainActivity : ComponentActivity()

    { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { App() } } } fun main() { ComposeViewport(document.body!!) { App() } }
  22. ӝࠄ 6*ஹನք౟ ҳഅ m J04 @main struct iOSApp: App {

    var body: some Scene { WindowGroup { ContentView() } } } struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { MainViewControllerKt.MainViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } struct ContentView: View { var body: some View { ComposeView().ignoresSafeArea(.keyboard) } } fun MainViewController() = ComposeUIViewController { App() }
  23. 8BTN ߓನ m (JU)VC1BHFT (JU)VC1BHFTۆ (JU)VC੄ ܻನ૑షܻীࢲ )5.- $44߂ +BWB4DSJQU

    ౵ੌਸ ౵ੌਸ प೯ೞҊ ਢ ࢎ੉౟ܳ ѱदೞח ੿੸ ࢎ੉౟ ഐझ౴ ࢲ࠺झ о੢ ௾ ౠ૚ਵ۽ח ޖܐ۽ ߓನೡ ࣻ ੓׮ח ੼ ੑפ׮
  24. 8BTN ߓನ m (FOFSBUFBSUJGBDUT DPNQPTF"QQ ] 5BTLT ] LPUMJO CSPXTFS

    ীࢲ XBTN+T#SPXTFS%JTUSJCVUJPO ੘স ਸ ࢶఖೞৈ प೯ ഑਷ ఠ޷օীࢲ ૒੽ HSBEMFX DPNQPTF"QQXBTN+T#SPXTFS%J TUSJCVUJPO ݺ۸যܳ ഝਊೞৈ ই౭ಂ౟ܳ ୶୹
  25. ࢤࢿغח ઱ਃ ౵ੌٜ § DPNQPTF"QQXBTNपઁ 8FC"TTFNCMZ ߄੉ցܻ ౵ੌ۽ ਢ গ೒ܻா੉࣌੄

    ઱ਃ ۽૒੉ ನೣ § DPNQPTF"QQKT8BTN ౵ੌਸ ۽٘ೞৈ 8FC"TTFNCMZ ੋझఢझܳ ࢤࢿೞҊ +BWB4DSJQU৬ 8BTN р੄ ࢚ഐ੘ਊਸ ҙܻ § JOEFYIUNMਢ ಕ੉૑ীࢲ গ೒ܻா੉࣌ਸ प೯ೡ ࣻ ੓ח ౵ੌ۽ DPNQPTF"QQKT৬ DPNQPTF"QQXBTNਸ ನೣೞৈ ࠳ۄ਋੷ীࢲ গ೒ܻா੉࣌ਸ ҳزೡ ࣻ ੓ח ૓ੑ੼ ৉ೡ § TUZMFTDTTӝࠄ੸ੋ झఋੌਸ ੿੄ೞח $44౵ੌ 8BTN ߓನ m (FOFSBUFBSUJGBDUT
  26. Q1. Ktor-client와 Ktor-server라는 동일한 Ktor 프레임워크를 사용하여 개발하는 것의 장점은

    무엇일까? Q2. 서버와 클라이언트 모듈을 같은 프로젝트 내에 두는 것의 장점은? ߊ಴ܳ ળ࠺ೞݶࢲ
  27. Q1. Ktor-client와 Ktor-server라는 동일한 Ktor 프레임워크를 사용하여 개발하는 것의 장점은

    무엇일까? A1: 잘 모르겠음 Q2. 서버와 클라이언트 모듈을 같은 프로젝트 내에 두는 것의 장점은? A2: 앞서 얘기한 것 처럼 Model Class를 공유하고, 비즈니스 로직을 공유한다면 유지보수 차원에서 더 효율적이지 않을까?? ߊ಴ܳ ળ࠺ೞݶࢲ
  28. ૕ޙ 2,UPSDMJFOU৬ ,UPSTFSWFSೞח زੌೠ ,UPS ೐ۨ੐ਕ௼ܳ ࢎਊೞৈ ѐߊೞח Ѫ੄ ੢੼਷

    ޖ঺ੋоਃ " § ௿ۄ੉঱౟৬ ࢲߡח ৮੹൤ ܻ࠙غয ੓ӝ ٸޙী п ஏীࢲ যڃ ۄ੉࠳۞ܻٚ ࢎਊೡ ࣻ ੓णפ׮ೞ૑݅ ௾ ੢੼ ઺ ೞաח పझ౟ ೐ۨ੐ਕ௼ੑפ׮ § ,UPS 4FSWFS੄ పझ౟ ೐ۨ੐ਕ௼ח ,UPS $MJFOUܳ ഝਊೞৈ о࢚੄ ਽׹ җ੿ਸ ୊ܻೡ ࣻ ੓ب۾ ૑ਗ೤פ׮ٮۄࢲ ,UPS 4FSWFS৬ ,UPS $MJFOUܳ ೣԋ ࢎਊೡ ҃਋ ੹୓ झఖী Ѧ஘ పझ౟ܳ ࠁ׮ औѱ ੘ࢿೡ ࣻ ੓য పझ౟ ழߡܻ૑ܳ ೱ࢚दఆ ࣻ ੓णפ׮
  29. ,UPS 5FTUJOHXJUI$MJFOU @Test fun testAddTask() = testApplication { application {

    module() } val client = createClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } val newTask = Task("Test Task", "Description", Priority.Medium) val response = client.post("/tasks") { contentType(ContentType.Application.Json) setBody(newTask) } assertEquals(HttpStatusCode.NoContent, response.status) }
  30. ,UPS 5FTUJOHXJUI$MJFOU @Test fun testAddTask() = testApplication { application {

    module() } val client = createClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } val newTask = Task("Test Task", "Description", Priority.Medium) val response = client.post("/tasks") { contentType(ContentType.Application.Json) setBody(newTask) } assertEquals(HttpStatusCode.NoContent, response.status) } Kotlin Multiplatform and Ktor: Developing Client and Server Simultaneously with Integration Testing
  31. 2э਷ ೐۽ં౟ ղীࢲ ௿ۄ੉঱౟৬ ࢲߡ ݽٕਸ ѐߊೞח ੢੼਷ ޖ঺ੋоਃ "

    § ҕਬػ ݽ؛ ਬ౰ܻ౭ ߂ ࢚ࣻ § ௿ۄ੉঱౟৬ ࢲߡо زੌೠ ೐۽ં౟ী ઓ੤ೡ ҃਋ ਬ౰ܻ౭ ௿ېझա ࢚ٜࣻਸ औѱ ҕਬೡ ࣻ ੓णפ׮੉ܳ ా೧ ௏٘੄ ઺ࠂਸ ઴੉Ҋ ف ݽٕ р੄ ੌҙࢿਸ ਬ૑ೡ ࣻ ੓णפ׮ § ࢲߡ৬ ௿ۄ੉঱౟ р੄ ݽ؛ ఋੑਸ ҕਬೡ ࣻ ੓য +40/૒۳ച৉૒۳ച द ߊࢤೡ ࣻ ੓ח ؘ੉ఠ ࠛੌ஖ܳ ޷ܻ ߑ૑ೡ ࣻ ੓णפ׮ ૕ޙ
  32. Ӓۧӝী .71ܳ ࡅܰѱ ઁ੘ೞҊ ѨૐೞҊ੗ ೞח ୡӝ ହস౱ীѱח Ԩա ъ۱ೠ

    ޖӝо ࢜۽ ࢤӟ ו՝ زੌೠ ঱যܳ ӝ߈ਵ۽ ૓೯ೞӝী ѐߊ੸ੋ ழޭפா੉࣌ ஏݶীࢲب ਬܻೞݴ ѐߊ ࢤ࢑ࢿ ژೠ ֫਺ ೞ૑݅ ਋ܻ੄ ೐۽ં౟ח ੺؀ .71ױ҅ী ݥ୾੓૑ח ঋਸ Ѫ੐੉ ࠙ݺೣ ,PUMJO'VMM4UBDL ѐߊ੄ ޷ېח
  33. য۵׮ ൜޷܂׮ ৵ উغ૑ ৵ غ૑ ׮਺ ӝמ ѐߊ ੤߀׮

    ,PUMJO'VMM4UBDL ѐߊਸ ഒ੗ࢲ ૓೯ೞݴ ו՛ ੼ 서버 환경 셋팅 & 내가 왜 Domain을 왜 사야해? 웹 Network 에러 서버비가 갑자기 10$가 결제됨
  34. ѾҴ ഒ੗ࢲח   աח ,PUMJO݅ ੓ਵݶ ظ ӝࣿ ࠗ଻

     ੹ޙࢿ੄ ࠗ઒ աח ,PUMJO݅ ੓ਵݶ ظ ࠁউ ߂ ࢿמ ޙઁ दрҗ ܻࣗझ੄ ࠗ઒ द੘ ѐߊ ઺ ৮ࢿ
  35. ,PUMJO'VMM4UBDL ѐߊী ؀ೠ ѐੋ੸ੋ Ѿۿ਷ § ѐੋ੸ਵ۽ ࢤпೡ ٸ ,.1ী

    ؀ೠ ҕध੸ੋ ૑ਗ੉ ૑ࣘػ׮ݶ ޷ېח ҭ੢൤ ਬݎೡ Ѫ੉ۄҊ ࢤп § ೞ૑݅ 'VMM4UBDL੉ۄח ੉ܴਸ ѦҊ ؀ӏݽ গ೒ܻா੉࣌ਸ ѐߊೞӝ ਤ೧ࢲח ցޖաب ݆਷ ૑ध੉ ೙ਃ § ٮۄࢲ ਋ܻח ೐۽ં౟ ࢿѺী ٮۄ ੸੺ೞѱ ,PUMJO .VMUJQMBUGPSNਸ ഝਊೞৈ ҕాػ ۽૒ਸ ܻ࠙ೞҊ ҕਬೡ ࣻ ੓ח ݒழפ્ਸ ੜ ഝਊ೧ঠ ೣ § ژೠ খࢲ ফӝೠ ࢿמ ߂ উ੿ࢿ੉ ೧ѾغҊ ੸੺ೞѱ ৉ೡਸ ࠙ߓೞৈ ੜ ഝਊೠ׮ݶ ੉݅ఀ ݒ۱੸ੋ ӝࣿਸ ଺ইࠁӝח ൨ٜ Ѫ э׮Ҋ ࢤп
  36. ؊ ݆਷ Ѫਸ ߓ਋Ҋ र׮ݶ § LUPS m %PDVNFOU IUUQTLUPSJPEPDTXFMDPNFIUNM

    § LUPSTBNQMFT IUUQTHJUIVCDPNLUPSJPLUPSTBNQMFT § 'VMMTUBDLEFWFMPQNFOUXJUI,PUMJO.VMUJQMBUGPSN IUUQTLUPSJPEPDTGVMMTUBDLEFWFMPQNFOUXJUI LPUMJONVMUJQMBUGPSNIUNM § Kotlin Multiplatform and Ktor: Developing Client and Server Simultaneously with Integration Testing § ,PUMJO,UPS QSPKFDUBOESVOOJOHJUPO"84&MBTUJD #FBOTUBML4FSWJDF IUUQTNFEJVNDPN!JMJBTIFWUTPWIPXUPTFUVQ BXTFMBTUJDCFBOTUBMLXJUIBLUPSQSPKFDU DDCFCF https://github.com/kez-lab/Kotlin-FullProject