Mobile App Development with KMP × Unity UaaL ── Multi-Repo Setup and Automation

Hi! We’re the AnotherBall Mobile Engineering Team.

Our app “Avvy” has a somewhat complex architecture: we use KMP (Kotlin Multiplatform) to share business logic across iOS and Android, and Unity as a Library (UaaL) to embed Unity’s 2D avatar rendering into our native apps.

In this article, we’ll share how we coordinate five repositories and how much we’ve automated with GitHub Actions.

Repository Structure

To avoid build complexity and inter-team dependencies, we split our codebase by function into separate repositories.

Repository Role Artifacts
shared-kmm Business logic AAR / XCFramework (KMP library)
unity-module 2D avatar rendering UaaL libraries for Android / iOS
android-app Android app APK / AAB
ios-app iOS app IPA
unity-spm SPM distribution for Unity XCFramework Swift Package

Each repository communicates through pre-built libraries—AAR for Android and XCFramework for iOS. This allows each team to work independently.

What the Unity Module Does

The Unity module handles avatar display and real-time control.

  • Avatar display: Renders avatars with 2D animation
  • Face tracking: Detects facial movements via camera and reflects them on the avatar
  • Customization: Outfit and accessory changes

Communication with native apps requires special handling. Face tracking sends data 60 times per second, so standard bridges would cause latency. On iOS, we use direct pointer access; on Android, we use memory-mapped files for fast data exchange.

How We Automated It

We use GitHub Actions to automate most of the cross-repository coordination. About 1,200 PRs are processed automatically each month (December 2025 figures).

Server API Changes

When the server’s API definition file (OpenAPI) is updated, an update PR is automatically created in the KMP repository.

KMP Library Updates

From KMP library release to update PR creation in each app repository—everything is automated.

The flow is simple: Publish → Trigger → Update.

  1. When a release is triggered in shared-kmm, it publishes to GitHub Packages
  2. On success, gh workflow run triggers the update workflow in each app repository
  3. Each app gets an auto-generated PR with the version update
1
2
3
4
5
6
7
8
9
# On KMP release (excerpt)
- name: Publish to GitHub Packages
run: ./gradlew publish

- name: Trigger Android update
run: |
gh workflow run update-kmm-version.yml \
--repo AnotherBall/android-app \
--field version=${{ env.VERSION }}
1
2
3
4
5
6
7
8
9
10
11
# Android app update workflow (excerpt)
- name: Update version in libs.versions.toml
run: |
sed -i "s/kmm = \".*\"/kmm = \"${{ inputs.version }}\"/" \
gradle/libs.versions.toml

- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
title: "Update KMP to ${{ inputs.version }}"
branch: "auto/kmm-${{ inputs.version }}"

Unity Library Distribution

To use Unity modules in the iOS app, we distribute them via SPM (Swift Package Manager).

  1. Build the Unity XCFramework and upload it to GitHub Releases
  2. Calculate a hash value for the file
  3. Auto-generate Package.swift (embedding the download URL and hash)
  4. Create a PR in the distribution repository (unity-spm)

While we could distribute directly from the unity-module repository, Xcode downloads the entire repository when resolving SPM packages. By creating a separate unity-spm repository that contains only Package.swift, we significantly speed up the download process.

The hash value lets the iOS app verify the file wasn’t corrupted during download.

Auto-Generated Release Branch PRs

When changes are pushed to a release/* branch, multiple merge PRs are automatically created:

  • release/2.10.0main (for production release)
  • release/2.10.0release/2.11.0 (to propagate bug fixes to the next version)

Version numbers are compared to determine the appropriate merge targets, preventing missed merges.

Package.resolved Conflict Resolution (iOS)

When multiple KMM/UaaL update PRs exist simultaneously, Package.resolved file conflicts occur. In the iOS repository, we have a workflow that automatically resolves these conflicts.

Trigger: Push to release/* branch

How it works:

  1. Fetch open PRs targeting the release branch via GitHub API
  2. Filter PRs with titles starting with chore: update KMM or chore: update UaaL
  3. Check if each PR can be merged, and identify those with conflicts
  4. Resolve conflicts for each PR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Attempt to merge base branch
if git merge "origin/$BASE_REF" --no-edit; then
echo "Merge succeeded"
else
# On conflict: temporarily adopt PR's Package.resolved
git checkout --ours Package.resolved
git add Package.resolved
git merge --continue
fi

# Re-resolve SPM dependencies (incorporates base branch changes)
make resolve-package-dependencies

git commit -m "chore: resolve Package.resolved conflict"
git push

The key is make resolve-package-dependencies (which runs xcodebuild -resolvePackageDependencies internally), re-resolving dependencies including target branch changes. When multiple PRs have conflicts, they’re processed in parallel.

Remaining Challenges

  • CI/CD execution time: Gradle/Xcodebuild can take 40+ minutes; we’re looking into better caching strategies
  • Workflow duplication: Similar logic exists in multiple workflow files; we want to extract it into reusable components
  • Auto-merging update PRs: Currently we only auto-create PRs; we’d like to auto-merge when tests pass

Conclusion

Even with a complex setup combining KMP and Unity UaaL, separating repositories and communicating through artifacts lets each team work independently. We’ve learned that automation isn’t a one-time setup—it requires ongoing improvement.

We’re Hiring

AnotherBall is looking for mobile engineers interested in app development using Kotlin/Swift/Unity/AI!
We’re seeking teammates to grow our product together while adopting new technologies like KMP. If you’re interested, we’d love to hear from you!