SlideShare uma empresa Scribd logo
1 de 54
Baixar para ler offline
范聖佑
JetBrains
Developer Advocate
打造多平台應⽤程式
以 Compose 及 otlin Multiplatform
DevFest 2023 Kaohsiung
2023/11/25
多平台開發痛點
—
• 各種型式的前端
多平台開發痛點
—
• 各種型式的前端
• 需要適應各種開發語⾔
多平台開發痛點
—
• 各種型式的前端
• 需要適應各種開發語⾔
• 重複實作的業務邏輯
DTO
Validation
Service
HTTP
Kotlin 編譯器設計
—
透過 Kotlin Multiplatform 共⽤業務邏輯
—
Server Web Desktop Android iOS
OS API Browser API OS API Android API iOS API
以 Kotlin Multiplatform 共⽤業務邏輯
共⽤ UI?
從 Jetpack Compose 到 Compose Multiplatform
—
多平台
Jetpack Compose
建立⼿機應⽤程式介⾯
可是缺 iOS 啊?
🙄
Compose for Web
Compose for Desktop
建立桌⾯應⽤程式介⾯ 建立網路應⽤程式介⾯
Compose for iOS 來啦!
—
https://youtu.be/FWVi4aV36d8
• KotlinConf’23 ⼤會發佈 Alpha 版
• 預計 2024 年發佈 Beta 版
Kotlin Multiplatform 全版圖
—
Server Web Desktop Android iOS
OS API Browser API OS API Android API iOS API
以 Kotlin Multiplatform 共⽤業務邏輯
以 Compose Multiplatform 共⽤ UI
Android View
Swing SwiftUI
Kotlin Multiplatform 可上 Production!
—
🧑💻👩💻
實戰 Kotlin Multiplatform
本⽇範例
—
• Android
• iOS
• Desktop
• Backend API
Backend
Desktop
Android iOS
HTTPs Request/Response
JSON
Client
Server
範例畫⾯ - LoginScreen
—
範例畫⾯ - HomeScreen
—
⽰範重點
—
• 多平台共⽤ UI - Compose Multiplatform
• 平台專⽤ API - 偵測平台名稱
• 共享業務邏輯 - ViewModel、HTTP、Data Class
• 整合 Multiplatform 函式庫 - Ktor Client、Voyager、Kamel
• 後端 API 服務 - Ktor Server
建立開發環境
—
• Mac with macOS
• JDK
• Android Studio
• Xcode (+ SDK)
• Cocoapods
透過 Homebrew 安裝 kdoctor,可檢查環境是否符合 KMP 開發需求?
kdoctor 指令⼯具
全新 KMP 開發⼯具
—
JetBrains Fleet
• 開啟 Kotlin Multiplatform Wizard
• 勾選⽬標平台
• 下載 Zip 檔
• 解壓縮
• 以 JetBrains Fleet 開啟專案
建立 Kotlin Multiplatform 專案
—
https:
/
/
kmp.jetbrains.com
專案資料夾結構
—
• composeApp → Compose 多平台主程式
- commonMain → 多平台共⽤實作
- androidMain → Android 平台專⽤實作
- iosMain → iOS 平台專⽤實作
- desktopMain → JVM/Desktop 平台專⽤實作
• iosApp → iOS 主程式進入點
• server → 後端 API 主程式
👈
• Ktor - HTTP Client
• kotlinx.serialization - JSON serialization/deserialization
• kotlinx.coroutines - Coroutine
• Voyager - Navigation、ViewModel
• Kamel - Asynchronous Media Loading
整合 Multiplatform 函式庫
—
安裝相依套件
—
kotlin {
sourceSets {
val desktopMain by getting
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
@OptIn(ExperimentalComposeLibrary
:
:
class)
implementation(compose.components.resources)
implementation("cafe.adriel.voyager:voyager-navigator:$ver")
implementation("media.kamel:kamel-image:$ver")
implementation("io.ktor:ktor-client-core:$ver")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$ver")
implementation("io.ktor:ktor-client-content-negotiation:$ver")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ver")
}
androidMain.dependencies {
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation("io.ktor:ktor-client-okhttp:$ver")
}
iosMain.dependencies {
後端 API - Ktor Server
—
• 語法簡單易上⼿
• 輕量級 Web 框架
• ⽀援 Async 功能
共⽤ Data Class
—
@Serializable
data class LoginRequest(
val username: String,
val password: String,
)
@Serializable
data class LoginResponse(
val result: Boolean,
val message: String,
val user: User? = null,
)
@Serializable
data class User(
val id: Int,
val username: String,
val password: String,
val email: String,
val displayName: String,
val profileImageUrl: String,
)
composeApp
- commonMain
- androidMain
- iosMain
- desktopMain
Ktor Server
—
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
install(ContentNegotiation) {
json()
}
// Ktor plugins
configureLogin()
}.start(wait = true)
}
實作 API Service
—
fun Application.configureLogin() {
routing {
post("...") {
// 接收 HTTP Request
// 反序列化成 Data Class
val req = call.receive<LoginRequest>()
// 登入驗證程式碼
// 回傳 HTTP Response
call.respond(
LoginResponse(
result = ...,
message = "...",
user = loggedInUser,
)
)
}
}
}
多平台共⽤ UI
—
實作⾴⾯
—
object LoginScreen : Screen {
@Composable
override fun Content() {
/
/
.
.
.
}
}
各區塊元件化
—
Logo()
DemoOnText()
Spacer()
LoginForm(
/
*
.
.
.
*
/
)
Spacer()
PasswordForgetLink()
SignUpLink()
TextField(
value =
.
.
.
,
label = { Text(text = "Username") },
onValueChange = {
/
*
.
.
.
*
/
},
modifier =
.
.
.
,
)
TextField(
value =
.
.
.
,
label = { Text(text = "Password") },
visualTransformation =,
keyboardOptions =,
onValueChange = {
/
*
.
.
.
*
/
},
modifier =
.
.
.
,
)
Button(
onClick = {
/
*
.
.
.
*
/
},
shape = RoundedCornerShape(50.dp),
modifier =
.
.
.
,
) {
Text(text = "Login")
}
排版差異化 (Mobile)
—
Column(
modifier =
.
.
.
,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
/
/
.
.
.
Column(
modifier =
.
.
.
,
) {
/
/
.
.
.
}
/
/
.
.
.
}
Box(
modifier =
.
.
.
,
) {
/
/
.
.
.
}
排版差異化 (Desktop)
—
Row(
modifier =
.
.
.
,
horizontalArrangement = Arrangement.Center
) {
Column(
modifier =
.
.
.
,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
/
/
.
.
.
}
Column(
modifier =
.
.
.
,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
/
/
.
.
.
}
}
載入網路圖片
—
KamelImage(
resource = asyncPainterResource(
data = Url(user.profileImageUrl)
),
modifier =
.
.
.
,
contentDescription =
.
.
.
,
)
平台專⽤ API
—
expect/actual 各平台實作
—
偵測平台版本
(expect 部份)
—
expect fun getPlatformName(): String
composeApp
- commonMain
- androidMain
- iosMain
- desktopMain
偵測平台版本
(actual 部份)
—
// androidMain
actual fun getPlatformName(): String =
"Andriod (API ${android.os.Build.VERSION.SDK_INT})"
// iosMain
actual fun getPlatformName(): String =
"iOS (${UIDevice.currentDevice.systemVersion})"
// desktopMain
actual fun getPlatformName(): String =
"Desktop (JVM ${Runtime.version()})"
composeApp
- commonMain
- androidMain
- iosMain
- desktopMain
偵測平台版本
(UI 部份)
— @Composable
fun DemoOnText() {
Text(
text = "Demo on ${getPlatformName()}",
style = TextStyle(
fontSize =
.
.
.
,
fontFamily =
.
.
.
,
color =
.
.
.
,
),
)
}
共享業務邏輯
—
API
發送 HTTP Request 接收 HTTP Response UI 換⾴
載入網路圖片
Ktor Client
—
• HTTP Client
• ⽀援多平台
• 可搭配
- kotlinx.serialization
- kotlinx.coroutines
實作 ViewModel
—
class LoginScreenModel :
StateScreenModel<LoginScreenModel.State>(State.Init) {
private val httpClient = HttpClient {
install(ContentNegotiation) {
json()
}
defaultRequest {
url(apiBaseUrl)
}
expectSuccess = true
}
sealed class State {
data object Init : State()
data class LoggedIn(val user: User) : State()
}
fun login(username: String, password: String) {
coroutineScope.launch {
val loginResponse: LoginResponse = httpClient.post("
.
.
.
") {
setBody(LoginRequest(username, password))
contentType(ContentType.Application.Json)
• 設定 HTTP Client
• 設定 State
• 發送 HTTP Req/Res
• 關閉 HTTP Client
使⽤ ViewModel
—
object LoginScreen : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { LoginScreenModel() }
val username = remember { mutableStateOf(TextFieldValue()) }
val password = remember { mutableStateOf(TextFieldValue()) }
Button(
onClick = {
screenModel.login(
username.value.text, password.value.text
)
},
shape = RoundedCornerShape(50.dp),
modifier =
.
.
.
,
) {
Text(text = "Login")
}
}
}
依狀態換⾴
—
object LoginScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { LoginScreenModel() }
val state by screenModel.state.collectAsState()
val username = remember { mutableStateOf(TextFieldValue()) }
val password = remember { mutableStateOf(TextFieldValue()) }
when (val loggedInState = state) {
is LoginScreenModel.State.Init
-
>
{}
is LoginScreenModel.State.LoggedIn
-
>
{
navigator.push(HomeScreen(loggedInState.user))
}
}
}
}
🤓
本⽇回顧
Kotlin Multiplatform 全版圖
—
Server Web Desktop Android iOS
OS API Browser API OS API Android API iOS API
以 Kotlin Multiplatform 共⽤業務邏輯
以 Compose Multiplatform 共⽤ UI
Android View
Swing SwiftUI
⽰範情境及實作重點
—
• 多平台共⽤ UI
• 平台專⽤ API
• 共享業務邏輯
• 整合多平台函式庫
• 後端 API 服務
Kotlin Multiplatform 優勢
—
• ⼀個語⾔跨多個平台
• 原⽣效能
• 可依需求調整共享程式碼的比例
對比其他解決⽅案
—
Place your text here
four columns
maximum
—
採⽤ Kotlin Multiplatform 技術的團隊
—
Kotlin Multiplatform 學習資源
—
Kotlin Multiplatform 官⽅⾴⾯ Compose Multiplatform 官⽅⾴⾯
本⽉ KMP 技術直播活動
—
線上預約提醒
更多 Kotlin Multiplatform 範例集
—
Compose Multiplatform 範例集 Kotlin Multiplatform 範例集
Kotlin Multiplatform ⽣態系
—
Kotlin is Awesome! Kotlin Multiplatform Libraries
Kotlin 爐邊漫談 - 聆聽業界實績案例的最佳來源
—
⼿機開發編年史 (携程)
來⾃阿⾥巴巴及美团的
Kotlin Multiplatform 應⽤案例
#8
Kotlin 爐邊漫談 Podcast
https:
/
/
podcast.kotlin.tips/
#5
歡迎參加 Kotlin 社群
—
Line 群 Telegram 群
加入討論群取得最新資訊
tw.kotlin.tips
感謝參與.歡迎交流
shengyou.fan@jetbrains.com
fb.me/shengyoufan
shengyoufan
@shengyou

Mais conteúdo relacionado

Mais procurados

git, repo, Gerrit 基礎教學
git, repo, Gerrit 基礎教學git, repo, Gerrit 基礎教學
git, repo, Gerrit 基礎教學
Doremi Lin
 

Mais procurados (20)

[JCConf 2020] 用 Kotlin 跨入 Serverless 世代
[JCConf 2020] 用 Kotlin 跨入 Serverless 世代[JCConf 2020] 用 Kotlin 跨入 Serverless 世代
[JCConf 2020] 用 Kotlin 跨入 Serverless 世代
 
[PHP 也有 Day #64] PHP 升級指南
[PHP 也有 Day #64] PHP 升級指南[PHP 也有 Day #64] PHP 升級指南
[PHP 也有 Day #64] PHP 升級指南
 
[COSCUP 2022] Kotlin Collection 遊樂園
[COSCUP 2022] Kotlin Collection 遊樂園[COSCUP 2022] Kotlin Collection 遊樂園
[COSCUP 2022] Kotlin Collection 遊樂園
 
BindableProperty 書くのクソダリーんだけど、 あいつなんやねん(仮)
BindableProperty書くのクソダリーんだけど、あいつなんやねん(仮)BindableProperty書くのクソダリーんだけど、あいつなんやねん(仮)
BindableProperty 書くのクソダリーんだけど、 あいつなんやねん(仮)
 
用 Kotlin 做自動化工具
用 Kotlin 做自動化工具用 Kotlin 做自動化工具
用 Kotlin 做自動化工具
 
Kotlin - scope functions and collections
Kotlin - scope functions and collectionsKotlin - scope functions and collections
Kotlin - scope functions and collections
 
Coroutines for Kotlin Multiplatform in Practise
Coroutines for Kotlin Multiplatform in PractiseCoroutines for Kotlin Multiplatform in Practise
Coroutines for Kotlin Multiplatform in Practise
 
Kotlin Collections
Kotlin CollectionsKotlin Collections
Kotlin Collections
 
[Effective Kotlin 讀書會] 第八章 Efficient collection processing 導讀
[Effective Kotlin 讀書會] 第八章 Efficient collection processing 導讀[Effective Kotlin 讀書會] 第八章 Efficient collection processing 導讀
[Effective Kotlin 讀書會] 第八章 Efficient collection processing 導讀
 
A quick and fast intro to Kotlin
A quick and fast intro to Kotlin A quick and fast intro to Kotlin
A quick and fast intro to Kotlin
 
Declarative UIs with Jetpack Compose
Declarative UIs with Jetpack ComposeDeclarative UIs with Jetpack Compose
Declarative UIs with Jetpack Compose
 
from Source to Binary: How GNU Toolchain Works
from Source to Binary: How GNU Toolchain Worksfrom Source to Binary: How GNU Toolchain Works
from Source to Binary: How GNU Toolchain Works
 
git, repo, Gerrit 基礎教學
git, repo, Gerrit 基礎教學git, repo, Gerrit 基礎教學
git, repo, Gerrit 基礎教學
 
給 iOS 工程師的 Flutter 開發
給 iOS 工程師的 Flutter 開發給 iOS 工程師的 Flutter 開發
給 iOS 工程師的 Flutter 開發
 
MVC MVVM MVVMC
MVC MVVM MVVMCMVC MVVM MVVMC
MVC MVVM MVVMC
 
炎炎夏日學 Android 課程 - Part1: Kotlin 語法介紹
炎炎夏日學 Android 課程 -  Part1: Kotlin 語法介紹炎炎夏日學 Android 課程 -  Part1: Kotlin 語法介紹
炎炎夏日學 Android 課程 - Part1: Kotlin 語法介紹
 
Threading Made Easy! A Busy Developer’s Guide to Kotlin Coroutines
Threading Made Easy! A Busy Developer’s Guide to Kotlin CoroutinesThreading Made Easy! A Busy Developer’s Guide to Kotlin Coroutines
Threading Made Easy! A Busy Developer’s Guide to Kotlin Coroutines
 
Flutter do zero a publicacao
Flutter do zero a publicacaoFlutter do zero a publicacao
Flutter do zero a publicacao
 
Kubernetes presentation
Kubernetes presentationKubernetes presentation
Kubernetes presentation
 
C#のキモイ高速プログラミング
C#のキモイ高速プログラミングC#のキモイ高速プログラミング
C#のキモイ高速プログラミング
 

Semelhante a [GDG Kaohsiung DevFest 2023] 以 Compose 及 Kotlin Multiplatform 打造多平台應用程式

Script with engine
Script with engineScript with engine
Script with engine
Webrebuild
 
透過 Windows Azure Mobile Services 開發各平台 Apps
透過 Windows Azure Mobile Services 開發各平台 Apps透過 Windows Azure Mobile Services 開發各平台 Apps
透過 Windows Azure Mobile Services 開發各平台 Apps
Eric ShangKuan
 
一步一步开发Html5 mobile apps
一步一步开发Html5 mobile apps一步一步开发Html5 mobile apps
一步一步开发Html5 mobile apps
Adam Lu
 
使用NodeJS构建静态资源管理系统
使用NodeJS构建静态资源管理系统使用NodeJS构建静态资源管理系统
使用NodeJS构建静态资源管理系统
Frank Xu
 
廖雪峰 Saa s ovp
廖雪峰 Saa s ovp廖雪峰 Saa s ovp
廖雪峰 Saa s ovp
drewz lin
 
Node getting-started
Node getting-startedNode getting-started
Node getting-started
lylijincheng
 
Kind editor设计思路
Kind editor设计思路Kind editor设计思路
Kind editor设计思路
taobao.com
 

Semelhante a [GDG Kaohsiung DevFest 2023] 以 Compose 及 Kotlin Multiplatform 打造多平台應用程式 (20)

ASP.NET Core 2.1設計新思維與新發展
ASP.NET  Core 2.1設計新思維與新發展ASP.NET  Core 2.1設計新思維與新發展
ASP.NET Core 2.1設計新思維與新發展
 
利用Signalr打造即時通訊@Tech day geek
利用Signalr打造即時通訊@Tech day geek利用Signalr打造即時通訊@Tech day geek
利用Signalr打造即時通訊@Tech day geek
 
Script with engine
Script with engineScript with engine
Script with engine
 
透過 Windows Azure Mobile Services 開發各平台 Apps
透過 Windows Azure Mobile Services 開發各平台 Apps透過 Windows Azure Mobile Services 開發各平台 Apps
透過 Windows Azure Mobile Services 開發各平台 Apps
 
HTML+COIMOTION 開發跨平台 app
HTML+COIMOTION 開發跨平台 appHTML+COIMOTION 開發跨平台 app
HTML+COIMOTION 開發跨平台 app
 
ASP.NET Core MVC 2.2從開發到測試 - Development & Unit Testing
ASP.NET Core MVC 2.2從開發到測試 - Development & Unit TestingASP.NET Core MVC 2.2從開發到測試 - Development & Unit Testing
ASP.NET Core MVC 2.2從開發到測試 - Development & Unit Testing
 
I os 16
I os 16I os 16
I os 16
 
Uliweb cheat sheet_0.1
Uliweb cheat sheet_0.1Uliweb cheat sheet_0.1
Uliweb cheat sheet_0.1
 
一步一步开发Html5 mobile apps
一步一步开发Html5 mobile apps一步一步开发Html5 mobile apps
一步一步开发Html5 mobile apps
 
使用NodeJS构建静态资源管理系统
使用NodeJS构建静态资源管理系统使用NodeJS构建静态资源管理系统
使用NodeJS构建静态资源管理系统
 
Arduino Yún使用Http restful api控制io
Arduino Yún使用Http restful api控制ioArduino Yún使用Http restful api控制io
Arduino Yún使用Http restful api控制io
 
2023-netconf-deploy-azure-function-with-KEDA-on-aks
2023-netconf-deploy-azure-function-with-KEDA-on-aks2023-netconf-deploy-azure-function-with-KEDA-on-aks
2023-netconf-deploy-azure-function-with-KEDA-on-aks
 
Berserk js
Berserk jsBerserk js
Berserk js
 
微软Bot framework简介
微软Bot framework简介微软Bot framework简介
微软Bot framework简介
 
廖雪峰 Saa s ovp
廖雪峰 Saa s ovp廖雪峰 Saa s ovp
廖雪峰 Saa s ovp
 
以HTML5和COIMOTION打造跨平台App
以HTML5和COIMOTION打造跨平台App以HTML5和COIMOTION打造跨平台App
以HTML5和COIMOTION打造跨平台App
 
RESTful
RESTfulRESTful
RESTful
 
Entity framework 入門第一課
Entity framework 入門第一課Entity framework 入門第一課
Entity framework 入門第一課
 
Node getting-started
Node getting-startedNode getting-started
Node getting-started
 
Kind editor设计思路
Kind editor设计思路Kind editor设计思路
Kind editor设计思路
 

Mais de Shengyou Fan

Mais de Shengyou Fan (17)

[Kotlin 讀書會第五梯次] 深入淺出 Kotlin 第一章導讀
[Kotlin 讀書會第五梯次] 深入淺出 Kotlin 第一章導讀[Kotlin 讀書會第五梯次] 深入淺出 Kotlin 第一章導讀
[Kotlin 讀書會第五梯次] 深入淺出 Kotlin 第一章導讀
 
[WebConf Taiwan 2023] 一份 Zend Engine 外帶!透過 Micro 讓一次打包、多處運行變得可能
[WebConf Taiwan 2023] 一份 Zend Engine 外帶!透過 Micro 讓一次打包、多處運行變得可能[WebConf Taiwan 2023] 一份 Zend Engine 外帶!透過 Micro 讓一次打包、多處運行變得可能
[WebConf Taiwan 2023] 一份 Zend Engine 外帶!透過 Micro 讓一次打包、多處運行變得可能
 
Using the Exposed SQL Framework to Manage Your Database
Using the Exposed SQL Framework to Manage Your DatabaseUsing the Exposed SQL Framework to Manage Your Database
Using the Exposed SQL Framework to Manage Your Database
 
[COSCUP 2022] 讓黑畫面再次偉大 - 用 PHP 寫 CLI 工具
[COSCUP 2022] 讓黑畫面再次偉大 - 用 PHP 寫 CLI 工具[COSCUP 2022] 讓黑畫面再次偉大 - 用 PHP 寫 CLI 工具
[COSCUP 2022] 讓黑畫面再次偉大 - 用 PHP 寫 CLI 工具
 
Composer 經典食譜
Composer 經典食譜Composer 經典食譜
Composer 經典食譜
 
[Kotlin Serverless 工作坊] 單元 4 - 實作 RSS Aggregator
[Kotlin Serverless 工作坊] 單元 4 - 實作 RSS Aggregator[Kotlin Serverless 工作坊] 單元 4 - 實作 RSS Aggregator
[Kotlin Serverless 工作坊] 單元 4 - 實作 RSS Aggregator
 
[Kotlin Serverless 工作坊] 單元 3 - 實作 JSON API
[Kotlin Serverless 工作坊] 單元 3 - 實作 JSON API[Kotlin Serverless 工作坊] 單元 3 - 實作 JSON API
[Kotlin Serverless 工作坊] 單元 3 - 實作 JSON API
 
[Kotlin Serverless 工作坊] 單元 2 - 簡介 Kotlin Serverless
[Kotlin Serverless 工作坊] 單元 2 - 簡介 Kotlin Serverless[Kotlin Serverless 工作坊] 單元 2 - 簡介 Kotlin Serverless
[Kotlin Serverless 工作坊] 單元 2 - 簡介 Kotlin Serverless
 
[Kotlin Serverless 工作坊] 單元 1 - 開發環境建置
[Kotlin Serverless 工作坊] 單元 1 - 開發環境建置[Kotlin Serverless 工作坊] 單元 1 - 開發環境建置
[Kotlin Serverless 工作坊] 單元 1 - 開發環境建置
 
用 Kotlin 打造讀書會小幫手
用 Kotlin 打造讀書會小幫手用 Kotlin 打造讀書會小幫手
用 Kotlin 打造讀書會小幫手
 
Kotlin 讀書會第三梯次第一章
Kotlin 讀書會第三梯次第一章Kotlin 讀書會第三梯次第一章
Kotlin 讀書會第三梯次第一章
 
用 OPENRNDR 將 Chatbot 訊息視覺化
用 OPENRNDR 將 Chatbot 訊息視覺化用 OPENRNDR 將 Chatbot 訊息視覺化
用 OPENRNDR 將 Chatbot 訊息視覺化
 
[PHP 也有 Day] 垃圾留言守城記 - 用 Laravel 阻擋 SPAM 留言的奮鬥史
[PHP 也有 Day] 垃圾留言守城記 - 用 Laravel 阻擋 SPAM 留言的奮鬥史[PHP 也有 Day] 垃圾留言守城記 - 用 Laravel 阻擋 SPAM 留言的奮鬥史
[PHP 也有 Day] 垃圾留言守城記 - 用 Laravel 阻擋 SPAM 留言的奮鬥史
 
Ktor 部署攻略 - 老派 Fat Jar 大法
Ktor 部署攻略 - 老派 Fat Jar 大法Ktor 部署攻略 - 老派 Fat Jar 大法
Ktor 部署攻略 - 老派 Fat Jar 大法
 
以 Kotlin 快速打造 Mobile Backend
以 Kotlin 快速打造 Mobile Backend以 Kotlin 快速打造 Mobile Backend
以 Kotlin 快速打造 Mobile Backend
 
Kotlin 一條龍 - 打造全平台應用
Kotlin 一條龍 - 打造全平台應用Kotlin 一條龍 - 打造全平台應用
Kotlin 一條龍 - 打造全平台應用
 
[HKOSCon 2020] Build an api service using ktor rapidly
[HKOSCon 2020] Build an api service using ktor rapidly[HKOSCon 2020] Build an api service using ktor rapidly
[HKOSCon 2020] Build an api service using ktor rapidly
 

[GDG Kaohsiung DevFest 2023] 以 Compose 及 Kotlin Multiplatform 打造多平台應用程式