/kotlin-bars

Primary LanguageKotlinApache License 2.0Apache-2.0

Kotlin Bars

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"
Loading

Kotlin Multiplatform Clients

API Server

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

Prod-Like

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

Web Server

Run the web asset server:

./gradlew -t :web:jsBrowserRun

Android Client

NOTE: You must use JDK 11 or higher

Start the server:

./gradlew :server:bootRun

Run the client:

  1. Download Android Command Line Tools

  2. 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
  3. 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
  4. Run the build from this project's dir:

    ./gradlew :android:build
  5. 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 on MainActivity and select Run.

    • Physical Device + Local Server:

      • From the command line:

        1. Setup adb
        2. ./gradlew :android:installDebug -PbarsUrl=http://YOUR_MACHINE_IP:8080/
      • From Android Studio / IntelliJ:

        1. Create a local.properties file in your root project directory containing:

          barsUrl=http://YOUR_MACHINE_IP:8080/
        2. Navigate to android/src/main/kotlin/kotlinbars/android and right-click on MainActivity and select Run.

    • Emulator or Physical Device + Cloud:

      • From the command line:

        1. setup adb
        2. ./gradlew :android:installDebug -PbarsUrl=https://YOUR_SERVER/api/bars
      • From Android Studio / IntelliJ:

        1. Create a local.properties file in your root project directory containing:

          barsUrl=https://YOUR_SERVER/api/bars
        2. Navigate to android/src/main/kotlin/kotlinbars/android and right-click on MainActivity and select Run.

iOS Client

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

Desktop Client

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

CLI Client

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

TUI Client

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

GitHub Actions

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.

iOS / Mac Signing

  1. Get App Store Team ID from: https://developer.apple.com/account#MembershipDetailsCard

  2. Store App Store Team ID in GitHub Secret: APPSTORE_TEAM_ID

  3. Create Identifier for App IDs https://developer.apple.com/account/resources/identifiers/list

  4. Store in GitHub Secret: BUNDLE_ID

  5. Create App Store Connect API in the Keys menu https://appstoreconnect.apple.com/access/api

  6. Store Issuer ID in GitHub Secret: APPSTORE_ISSUER_ID

  7. Generate API Key with Access App Manager

  8. Store Key ID in GitHub Secret: APPSTORE_KEY_ID

  9. Store Private Key in GitHub Secret: APPSTORE_PRIVATE_KEY

  10. Generate a Private Key for signing: openssl genrsa -out mykey.key 2048

  11. 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"
    
  12. Add a new Certificate of Apple Development https://developer.apple.com/account/resources/certificates/add

  13. Upload the CertificateSigningRequest.certSigningRequest

  14. Download the certificate

  15. Convert the cert to PEM format: openssl x509 -in development.cer -inform DER -out development.pem -outform PEM

  16. Add a new Certificate of Apple Distribution https://developer.apple.com/account/resources/certificates/add

  17. Upload the CertificateSigningRequest.certSigningRequest

  18. Download the certificate

  19. Convert the cert to PEM format: openssl x509 -in distribution.cer -inform DER -out distribution.pem -outform PEM

  20. Combine the two certs: cat development.pem distribution.pem > cert.pem

  21. Export the p12 remembering the password: openssl pkcs12 -export -legacy -inkey mykey.key -in cert.pem -out cert.p12

  22. Store the password in GitHub Secret: CERTIFICATES_PASSWORD

  23. Convert the cert to Base64 and store in GitHub Secret: CERTIFICATES_P12 base64 cert.p12

  24. Create iOS App Development Provisioning Profile: https://developer.apple.com/account/resources/profiles/add

  25. Store the name of the provisioning profile in GitHub Secret: DEV_PROVISIONING_PROFILE_NAME

  26. Create App Store Provisioning Profile: https://developer.apple.com/account/resources/profiles/add

  27. Store the name of the provisioning profile in GitHub Secret: DIST_PROVISIONING_PROFILE_NAME

Testing GitHub Actions:

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