Developing cross-platform applications with Kotlin Multiplatform and Jetpack Compose is an exciting way to target multiple platforms from a single codebase. However, to ensure the reliability and maintainability of such applications, Continuous Integration (CI) is essential. This post will guide you through setting up a CI pipeline for a Compose Multiplatform project, providing a structured approach to automated building, testing, and deployment.
What is Continuous Integration (CI)?
Continuous Integration (CI) is a development practice where developers regularly merge their code changes into a central repository, after which automated builds and tests are run. CI aims to detect and address integration issues early in the development cycle, leading to more reliable software and faster release cycles.
Why Use CI for Compose Multiplatform?
- Early Bug Detection: Identifies issues across all supported platforms quickly.
- Automated Testing: Ensures that changes do not introduce regressions.
- Consistency: Provides a consistent and automated build process.
- Faster Feedback Loops: Gives developers rapid feedback on their code changes.
Choosing a CI Tool
Several CI tools are available, each with its strengths. Popular choices include:
- GitHub Actions: Integrated directly into GitHub repositories.
- GitLab CI: Part of the GitLab suite, suitable for projects using GitLab for version control.
- Jenkins: A highly customizable, open-source automation server.
- CircleCI: A cloud-based CI/CD platform known for its ease of use.
For this guide, we will use GitHub Actions due to its tight integration with GitHub and ease of configuration.
Setting Up CI with GitHub Actions
Step 1: Project Structure
Ensure your project is structured in a way that supports multiplatform development. A typical Compose Multiplatform project structure looks like this:
MyComposeMultiplatformApp/
├── androidApp/
├── iosApp/
├── commonMain/
├── desktopApp/
├── build.gradle.kts
├── settings.gradle.kts
└── gradle.properties
Step 2: Create a GitHub Actions Workflow File
Create a new file in the .github/workflows directory of your repository, named ci.yml. This file will define your CI workflow.
Step 3: Define the Workflow
Open .github/workflows/ci.yml and add the following content to define your workflow:
name: CI - Compose Multiplatform
on:
push:
branches: [ "main", "develop" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build and Test
run: ./gradlew check
Explanation of the YAML file:
- name: Name of the workflow.
- on: Defines when the workflow will be triggered. This workflow runs on pushes to the
mainanddevelopbranches, and on pull requests to themainbranch. - jobs: Defines the jobs to be executed.
- build: The job that builds and tests the application.
- runs-on: Specifies the runner environment (
ubuntu-latest). - steps: List of steps to perform:
actions/checkout@v3: Checks out the repository.actions/setup-java@v3: Sets up the Java Development Kit (JDK) version 17.chmod +x gradlew: Grants execute permissions to thegradlewscript.gradle/wrapper-validation-action@v1: Validates the Gradle Wrapper to ensure it’s safe to use../gradlew check: Runs all checks and tests defined in your Gradle configuration.
Step 4: Customize Build Tasks
The ./gradlew check command in the ci.yml file runs the default check task in your Gradle configuration. Customize your build.gradle.kts file to include relevant build and test tasks.
tasks.register("check") {
dependsOn("test", "lintKotlin", "allTests")
}
tasks.register("allTests") {
dependsOn(":commonMain:test", ":androidApp:testDebugUnitTest")
}
Here:
testruns unit tests for all platforms (e.g., Kotlin JVM, Android).lintKotlinperforms code linting.allTestsis a custom task aggregating tests from thecommonMainandandroidAppmodules. You may need to adapt this task to the names and locations of your test tasks across other platforms (e.g., iOS).
Step 5: Testing UI Components
Compose UI testing is crucial to ensure the user interface behaves as expected. Set up UI tests for both Android and Desktop applications using frameworks like JUnit and Compose UI testing libraries.
Android UI Tests
dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.5.0")
}
android {
testOptions {
animationsDisabled = true // Optional, for smoother UI tests
execution "ANDROIDX_TEST_ORCHESTRATOR" // For test isolation
}
}
Step 6: Running Tests on Multiple Platforms
Ensure your tests cover all platforms by creating dedicated tasks in your build.gradle.kts file for each platform.
tasks.register("desktopTest") {
dependsOn(":desktopApp:test")
}
tasks.register("androidTest") {
dependsOn(":androidApp:testDebugUnitTest")
}
tasks.register("iosTest") {
// Add logic for iOS tests if applicable
}
tasks.named("check") {
dependsOn("desktopTest", "androidTest", "iosTest")
}
Step 7: Secret Management
When your CI process requires sensitive information (API keys, passwords), store them as GitHub Secrets. Access these secrets in your workflow file.
steps:
- name: Send Slack Notification
uses: actions/github-script@v6
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Add secrets in your repository’s settings under the “Secrets” tab.
Step 8: Complete CI Workflow
The final CI workflow file will combine all steps. For example:
name: CI - Compose Multiplatform
on:
push:
branches: [ "main", "develop" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build and Test
run: ./gradlew check
- name: Send Slack Notification (on failure)
if: failure()
uses: actions/github-script@v6
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
with:
script: |
const message = `:red_circle: CI Failed for ${{ github.repository }} - ${{ github.event.pull_request.title || github.sha }}nSee workflow logs at ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`;
await github.request('POST', process.env.SLACK_WEBHOOK_URL, {
body: JSON.stringify({ text: message })
});
Best Practices for Compose Multiplatform CI
- Use Gradle Wrapper: Ensures consistent build environment.
- Cache Gradle Dependencies: Speeds up build times.
- Automated Code Analysis: Integrate linting and code quality checks.
- Parallel Testing: Run tests in parallel to reduce feedback time.
- Monitor CI Performance: Regularly review and optimize CI workflow for efficiency.
Conclusion
Implementing a Continuous Integration pipeline for Compose Multiplatform applications is essential for maintaining code quality, detecting issues early, and ensuring smooth releases. By following the steps outlined in this guide, you can set up a robust CI system using GitHub Actions that automates building, testing, and deployment processes, leading to more reliable and maintainable cross-platform applications.