This sample uses Kotlin Multiplatform for a data-oriented application sharing as much code as possible across: API server, web app, Android app, iPhone app, desktop app, and a CLI. Releases are automated using GitHub Actions, deploying server pieces on Google Cloud and client binaries to GitHub and TestFlight.
graph TD
common(<b>common</b><br/><i>data classes</i>)
rpc(<b>rpc</b><br/><i>Ktor REST client</i>)
server{{<b>server</b><br/><i>Spring Boot REST</i>}}
cli[<b>cli</b><br/><i>GraalVM Native Image<br/>Windows, Mac, Linux</i>]
tui[<b>tui</b><br/><i>Kotlin/Native<br/>Windows, Mac, Linux</i>]
compose(<b>compose</b><br/><i>Shared UI</i>)
web[<b>web</b><br/><i>Browser App</i>]
iosApp[<b>iosApp</b><br/><i>SwiftUI iOS App</i>]
android[<b>android</b><br/><i>Android App</i>]
desktop[<b>desktop</b><br/><i>Desktop App<br/>Windows, Mac, Linux</i>]
dev([<b>dev</b><br/><i>Testcontainer Server</i>])
common --> server
common --> cli
common --> rpc ---> web & tui
rpc --> compose --> android & desktop & iosApp
server -.- dev -...- cli & tui & web & desktop
classDef lib fill:#f96;
classDef client fill:#96f;
classDef server fill:#9f6;
classDef dev fill:#69f;
class common,rpc,compose lib;
class cli,tui,desktop,android,web,iosApp client;
class server,postgresql server;
class dev dev;
click common "https://github.com/jamesward/kotlin-bars/tree/main/common"
click rpc "https://github.com/jamesward/kotlin-bars/tree/main/rpc"
click server "https://github.com/jamesward/kotlin-bars/tree/main/server"
click cli "https://github.com/jamesward/kotlin-bars/tree/main/cli"
click tui "https://github.com/jamesward/kotlin-bars/tree/main/tui"
click compose "https://github.com/jamesward/kotlin-bars/tree/main/compose"
click web "https://github.com/jamesward/kotlin-bars/tree/main/web"
click iosApp "https://github.com/jamesward/kotlin-bars/tree/main/iosApp"
click android "https://github.com/jamesward/kotlin-bars/tree/main/android"
click desktop "https://github.com/jamesward/kotlin-bars/tree/main/desktop"
click dev "https://github.com/jamesward/kotlin-bars/tree/main/dev"
Run the api server:
./gradlew :server:bootRun
Create a bar:
curl -X POST http://localhost:8080/api/bars \
-H 'Content-Type: application/json' \
-d '{"name": "Test"}'
Fetch the bars: localhost:8080/api/bars
Start Postgres manually:
docker run --rm -ePOSTGRES_PASSWORD=password -p5432:5432 --name my-postgres postgres:13.3
Create a native exec (with GraalVM installed / in PATH):
./gradlew :server:nativeCompile
SPRING_R2DBC_URL=r2dbc:postgresql://localhost/postgres \
SPRING_R2DBC_USERNAME=postgres \
SPRING_R2DBC_PASSWORD=password \
server/build/native/nativeCompile/server
Create container:
./gradlew :server:bootBuildImage
Start the server:
docker run -it --network host \
-eSPRING_R2DBC_URL=r2dbc:postgresql://localhost/postgres \
-eSPRING_R2DBC_USERNAME=postgres \
-eSPRING_R2DBC_PASSWORD=password \
kotlin-bars-server
Run the web asset server:
./gradlew -t :web:jsBrowserRun
NOTE: You must use JDK 11 or higher
Start the server:
./gradlew :server:bootRun
Run the client:
-
Install the SDK:
mkdir android-sdk cd android-sdk unzip PATH_TO_SDK_ZIP/sdk-tools-linux-VERSION.zip mv cmdline-tools latest mkdir cmdline-tools mv latest cmdline-tools cmdline-tools/latest/bin/sdkmanager --update cmdline-tools/latest/bin/sdkmanager "platforms;android-31" "build-tools;31.0.0" "extras;google;m2repository" "extras;android;m2repository" cmdline-tools/latest/bin/sdkmanager --licenses
-
Set an env var pointing to the
android-sdk
export ANDROID_SDK_ROOT=PATH_TO_SDK/android-sdk echo "sdk.dir=$(realpath $ANDROID_SDK_ROOT)" > local.properties
-
Run the build from this project's dir:
./gradlew :android:build
-
You can either run on an emulator or a physical device and you can either connect to the server running on your local machine, or connect to a server you deployed on the cloud.
-
Emulator + Local Server:
-
From the command line:
./gradlew :android:installDebug
-
From Android Studio / IntelliJ, navigate to
android/src/main/kotlin/kotlinbars/android
and right-click onMainActivity
and selectRun
.
-
-
Physical Device + Local Server:
-
From the command line:
- Setup adb
./gradlew :android:installDebug -PbarsUrl=http://YOUR_MACHINE_IP:8080/
-
From Android Studio / IntelliJ:
-
Create a
local.properties
file in your root project directory containing:barsUrl=http://YOUR_MACHINE_IP:8080/
-
Navigate to
android/src/main/kotlin/kotlinbars/android
and right-click onMainActivity
and selectRun
.
-
-
-
Emulator or Physical Device + Cloud:
-
From the command line:
- setup adb
./gradlew :android:installDebug -PbarsUrl=https://YOUR_SERVER/api/bars
-
From Android Studio / IntelliJ:
-
Create a
local.properties
file in your root project directory containing:barsUrl=https://YOUR_SERVER/api/bars
-
Navigate to
android/src/main/kotlin/kotlinbars/android
and right-click onMainActivity
and selectRun
.
-
-
-
Open iosApp/Kotlin_Bars.xcodeproj
in XCode.
You will need a "bars" API server for the app to connect to so, start the server:
./gradlew :server:bootRun
If you run in an emulator, the app will connect by default to localhost
so no additional configuration is needed. For running on a physical device you need to set the following in your local.properties
file:
barsUrl=http://YOUR_IP:8080/api/bars
Run, connecting to the default http://localhost:8080/api/bars
url (if barsUrl
is not set in the local.properties
file):
./gradlew :desktop:run
Run, connecting to the specified url:
./gradlew :desktop:run -PbarsUrl=http://YOUR_URL:8080/api/bars
Start a server with Testcontainers and connect to it:
./gradlew :desktop:dev
Package a native app and run it: Note: Requires JDK 15+
./gradlew :desktop:runDistributable
Package a native app (for the current platform): Note: Requires JDK 15+
./gradlew :desktop:package
Run, connecting to the default http://localhost:8080/api/bars
url (if barsUrl
is not set in the local.properties
file):
./gradlew :cli:run -q --console=plain
Run, connecting to the specified url:
./gradlew :cli:run -q --console=plain -PbarsUrl=http://YOUR_URL:8080/api/bars
Start a server with Testcontainers and connect to it:
./gradlew :cli:dev -q --console=plain
Build the native image executable:
./gradlew :cli:nativeImage
Run it:
cli/build/graal/kotlin-bars
The Terminal User Interface will eventually be a rich / interactive CLI for Kotlin Bars but today is just a basic CLI.
Run, connecting to the default http://localhost:8080/api/bars
:
./gradlew :tui:linkDebugExecutable && tui/build/bin/linuxX64/debugExecutable/tui.kexe
Run, connecting to the specified url:
./gradlew :tui:linkDebugExecutable -PbarsUrl=http://YOUR_URL:8080/api/bars && tui/build/bin/linuxX64/debugExecutable/tui.kexe
export PROJECT_ID=YOUR_GCP_PROJECT
Enable APIs via gcloud:
gcloud services enable \
servicenetworking.googleapis.com \
sqladmin.googleapis.com \
vpcaccess.googleapis.com \
run.googleapis.com \
containerregistry.googleapis.com \
--project=$PROJECT_ID
Create a Service Account:
gcloud iam service-accounts create kotlin-bars-gha --project=$PROJECT_ID
export SA_EMAIL="kotlin-bars-gha@$PROJECT_ID.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/container.admin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/storage.admin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/run.admin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/compute.networkAdmin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/compute.loadBalancerAdmin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/compute.instanceAdmin.v1
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/cloudsql.admin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/iam.serviceAccountUser
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/iam.serviceAccountAdmin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/vpcaccess.admin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$SA_EMAIL \
--role=roles/iam.securityAdmin
Create a key for the service account:
gcloud iam service-accounts keys create /dev/stdout \
--iam-account=$SA_EMAIL \
--project=$PROJECT_ID 2>/dev/null
Copy the JSON key from STDOUT and use it as the value of GCP_CREDENTIALS
in the next step.
Create GitHub secrets for:
GCP_PROJECT
GCP_REGION
GCP_CREDENTIALS
DOMAINS
Your DOMAINS
then need to be mapped to the IP listed in the output of the cloud
GitHub Action.
-
Get App Store Team ID from: https://developer.apple.com/account#MembershipDetailsCard
-
Store App Store Team ID in GitHub Secret:
APPSTORE_TEAM_ID
-
Create Identifier for App IDs https://developer.apple.com/account/resources/identifiers/list
-
Store in GitHub Secret:
BUNDLE_ID
-
Create App Store Connect API in the Keys menu https://appstoreconnect.apple.com/access/api
-
Store Issuer ID in GitHub Secret:
APPSTORE_ISSUER_ID
-
Generate API Key with Access
App Manager
-
Store Key ID in GitHub Secret:
APPSTORE_KEY_ID
-
Store Private Key in GitHub Secret:
APPSTORE_PRIVATE_KEY
-
Generate a Private Key for signing:
openssl genrsa -out mykey.key 2048
-
Generate a Certificate Signing Request:
export EMAIL=YOUR_EMAIL export NAME="YOUR_NAME" openssl req -new -key mykey.key -out CertificateSigningRequest.certSigningRequest -subj "/emailAddress=$EMAIL, CN=$NAME, C=US"
-
Add a new Certificate of Apple Development https://developer.apple.com/account/resources/certificates/add
-
Upload the
CertificateSigningRequest.certSigningRequest
-
Download the certificate
-
Convert the cert to PEM format:
openssl x509 -in development.cer -inform DER -out development.pem -outform PEM
-
Add a new Certificate of Apple Distribution https://developer.apple.com/account/resources/certificates/add
-
Upload the
CertificateSigningRequest.certSigningRequest
-
Download the certificate
-
Convert the cert to PEM format:
openssl x509 -in distribution.cer -inform DER -out distribution.pem -outform PEM
-
Combine the two certs:
cat development.pem distribution.pem > cert.pem
-
Export the p12 remembering the password:
openssl pkcs12 -export -legacy -inkey mykey.key -in cert.pem -out cert.p12
-
Store the password in GitHub Secret:
CERTIFICATES_PASSWORD
-
Convert the cert to Base64 and store in GitHub Secret:
CERTIFICATES_P12
base64 cert.p12
-
Create
iOS App Development
Provisioning Profile: https://developer.apple.com/account/resources/profiles/add -
Store the name of the provisioning profile in GitHub Secret:
DEV_PROVISIONING_PROFILE_NAME
-
Create
App Store
Provisioning Profile: https://developer.apple.com/account/resources/profiles/add -
Store the name of the provisioning profile in GitHub Secret:
DIST_PROVISIONING_PROFILE_NAME
git tag -d v1.0.0; git push --delete origin v1.0.0; git commit -a --allow-empty-message --no-edit; git tag v1.0.0; git push; git push origin v1.0.0