GSoC 2026 Proposal: Compose Multiplatform Migration — Anuj
Applicant: Anuj (IRC: anuj_)
Email: aanuj8619@gmail.com
GitHub: anuj990
LinkedIn: anuj990
About Me
I am a sophomore at the Indian Institute of Information Technology Jabalpur
(IIITDM Jabalpur) pursuing B.Tech in Electronics and Communication Engineering.
I started my coding journey with C++ for competitive programming, and through
my college’s open source programme BSOC (BitByte Summer of Code) I discovered
Android development. Since then my primary stack has been Kotlin, Jetpack
Compose, and MVVM and over the past few months I have been working directly
with Kotlin Multiplatform and Compose Multiplatform.
I am particularly drawn to this project because it is not about adding new
features it is about making the existing codebase structurally so that
a completely new group of users (iOS users) can access ListenBrainz for the
first time. That kind of foundational work is what excites me most.
I have been contributing to ListenBrainz Android since December 2025 across
both the main and cmp branches. I have read the code deeply, received
review feedback from jasje, acted on it, and gotten PRs merged.
Project Summary
ListenBrainz Android is currently available only on Android which means iOS
users cannot access this service at all. This project proposes
completing the migration of the existing codebase to Kotlin Multiplatform (KMP)
and Compose Multiplatform (CMP), so that both Android and iOS users can run
ListenBrainz from a single shared codebase.
The migration is already actively in progress on the cmp branch, tracked
under Epic Issue #614
some migrations is already done. My GSoC work focuses on completing the remaining migration in a structured phase-by-phase approach, resulting in a fully working Android and iOS app by the end of the summer.
The final architecture will look like this:
commonMain
├── Models, Repositories, ViewModels
├── Network (Ktor), DataStore KMP, Room KMP
├── Compose UI (JetBrains CMP), Navigation (Voyager)
│
├── androidMain iosMain
│ ├── ExoPlayer ├── AVPlayer
│ ├── WorkManager ├── BGTaskScheduler
│ ├── Permissions ├── Native Permissions
│ └── SplashScreen └── LaunchScreen
│
├── androidApp iosApp
Current State of Migration
Already Done
- KMP project structure + shared module
- Gradle KMP plugin configuration
- Source sets (commonMain, androidMain, iosMain)
- Dagger/Hilt → Koin
- Gson → Kotlin Serialization
- Retrofit → Ktor
- coil/glide → coil3
- androidsvg → coil SVG decoder
- JUnit → kotlin-test
- turbine (already KMP compatible)
- androidx.datastore → DataStore KMP
AppPreferencesImplin commonMainPreferenceKeysin commonMainDataStorePreference<T>interface in commonMainPlatformContextexpect/actual in placecreateDataStoreexpect/actual in place
- compose ratingbar → custom CMP component
- threetenabp → kotlinx-datetime
Partial
- socket.io → ktor-websockets — PR in review
- Room → Room KMP — infrastructure done but DAOs, entities, and database classes still
in androidMain - androidx.preference → DataStore KMP shared module done, minor
cleanup remaining in app module - androidx.lifecycle → KMP ViewModel PR #732 attempted SocialViewModel
migration but used plainCoroutineScopeinstead of proper KMPViewModel()
with JetBrainslifecycle-kmp="2.8.0"i will it update soon
Remaining for GSoC
- Logger-Android → Kermit
- jsoup → Ksoup
- ktor-client-okhttp → platform engines (darwin/okhttp)
- Remove chucker
- Androidx.paging → paging-common
- Koin platform-specific setup:
Move koin-core to commonMain- koin-androidx-compose → koin-compose
- Remove koin-androidx-workmanager (WorkManager is Android-only)
- lottie/lottie-compose → Compottie
- androidx.lifecycle → KMP ViewModel (full migration)
- androidx.compose.* → JetBrains CMP
- androidx.navigation → Voyager
- compose-shimmer → KMP shimmer
- ExoPlayer → interface (AVPlayer for iOS)
- spotify-app-remote → stub for iOS
- WorkManager → BGTaskScheduler (expect/actual)
- accompanist-permissions → platform-specific
- accompanist-systemuicontroller → platform-specific
- share-android → platform-specific
- androidx.browser → platform-specific
- androidx.core.splashscreen → platform-specific
- app-update → Android-only
- androidx.palette → Custom implementation
Migration Plan
Phase 1 — Final Build Infrastructure
Update libs.versions.toml to the final target state adding all KMP/CMP
plugins (jetbrains-compose, lifecycle-kmp, voyager, kermit,
compottie, ksoup, shimmer-kmp) and dependency versions. Set up the
final shared/build.gradle.kts with proper commonMain, androidMain,
and iosMain source sets so that approximately 80% of all code lives in
commonMain by the end of GSoC.
Phase 2 — Domain Models
Move all model classes from androidMain to commonMain. Replace Gson’s
@SerializedName with kotlinx.serialization’s @SerialName, annotate any
missing @Serializable, and replace all Java date types (java.util.Date,
Calendar, SimpleDateFormat) with kotlinx-datetime equivalents
(Clock.System.now(), Instant, LocalDateTime).
Phase 3 — Network Layer
This is one of the most impactful phases. Migrate createBaseHttpClient
to commonMain via expect/actual platform engines (OkHttp for Android,
Darwin for iOS). Remove ChuckerInterceptor (Android-only debug tool) and
replace it with Ktor’s built-in Logging plugin using Kermit as the
underlying logger. Replace Logger-Android with Kermit throughout.
Replace socket.io-client with ktor-client-websockets, handling the
Engine.IO handshake manually (0 → 40 → subscribe, ping/pong 2 → 3) with
automatic reconnect logic. Replace jsoup with Ksoup for HTML parsing.
Migrate all Ktorfit service interfaces and Koin networkModule to shared.
Phase 4 — Room KMP
Move all entities (SongEntity, AlbumEntity, ArtistEntity,
PlaylistEntity, ListenSubmitBody.Payload), all DAOs, and both database
classes (BrainzPlayerDatabase, ListensSubmissionDatabase) from
androidMain to commonMain. Add expect/actual database builders to
handle platform-specific file paths — Android uses Context.getDatabasePath()
while iOS uses NSFileManager + NSDocumentDirectory.
Phase 5 — Repository Layer + Paging
Migrate all repository interfaces and implementations to commonMain.
Replace all java.util.Calendar and SimpleDateFormat usages with
kotlinx-datetime. Migrate all paging sources from androidx.paging
to paging-common so they work on both platforms. Keep paging-runtime
and paging-compose in androidMain only. Migrate Koin repositoryModule
to commonMain.
Phase 6 — ViewModel Migration
Add JetBrains lifecycle-kmp="2.8.0" to commonMain — this is critical.
ViewModels must extend KMP ViewModel() and use viewModelScope which
auto-cancels on both platforms. Plain CoroutineScope is not lifecycle-aware
and must not be used (PR #732 needs updating for this reason). Keep thin
Android wrappers in androidMain only for Android-specific dependencies
like RemotePlaybackHandler and android.net.Uri. Migrate Koin
viewModelModule to commonMain.
Phase 7 — Compose UI Migration
Replace androidx.compose.* with JetBrains CMP. Migrate app theme, colors,
and typography to commonMain. Move all resources from Android res/ to
shared composeResources/ — updating all painterResource(R.drawable.x)
calls to painterResource(Res.drawable.x). Replace lottie-compose with
Compottie for KMP animations. Replace compose-shimmer with KMP shimmer
(same API, different artifact). Replace both androidx.navigation.compose
and androidx.navigation3 with Voyager — converting string-based routes to
type-safe Screen data classes. Wire platform entry points
(MainActivity.setContent { App() } on Android,
ComposeUIViewController { App() } on iOS).
Phase 8 — Platform-Specific Boundaries
Implement expect/actual for the four major platform boundaries:
ScrobblerManager — NotificationListenerService on Android, permanent
no-op stub on iOS (iOS fundamentally cannot read other apps’ notifications);
AudioPlayer — ExoPlayer on Android wrapped with MediaBrowserServiceCompat
for lock screen + Bluetooth integration, AVPlayer on iOS;
scheduleListenSubmission — WorkManager on Android (existing
ListenSubmissionWorker), BGTaskScheduler on iOS;
PermissionChecker — Accompanist permissions on Android (notification
listener + battery optimization), native iOS permission APIs.
Timeline
| Period | Milestone |
|---|---|
| May 1–24 | Community bonding — study cmp branch deeply, confirm all decisions with mentor |
| May 25–Jun 7 | Phase 1 + Phase 2 — final build setup + all domain models to commonMain |
| Jun 8–Jun 21 | Phase 3 — complete network layer migration (both parts) |
| Jun 22–Jun 28 | Phase 4 — Room KMP complete |
| Jun 29–Jul 5 | Phase 5 — all repositories + paging to commonMain |
| Jul 6–10 | Midterm Evaluation — models, network, Room, repositories all in commonMain |
| Jul 11–Jul 17 | Tests + midterm bug fixes, move tests to commonTest |
| Jul 18–Jul 24 | Phase 6 — full ViewModel migration with proper KMP ViewModel |
| Jul 25–Jul 31 | Phase 8 — all platform boundaries via expect/actual |
| Aug 1–Aug 7 | Phase 7 Part 1 — CMP core, theme, resources, Compottie |
| Aug 8–Aug 14 | Phase 7 Part 2 — Voyager navigation, shimmer, platform entry points |
| Aug 15–Aug 24 | Full Android + iOS testing, polish, final submission |
My Contributions
| PR | Branch | Description |
|---|---|---|
| #639 | main | Fixed search state mismatch between user and BrainzPlayer search |
| #648 | main | Fixed broken overflow menu on BrainzPlayer Albums screen |
| #727 | main | Implemented Delete Listen feature |
| #732 | cmp | Migrating SocialViewModel to shared/commonMain for KMP |
All PRs: github.com/metabrainz/listenbrainz-android/pulls/anuj990
Full Proposal
Due to the community post character limit, the complete proposal including
all technical details, actual ListenBrainz code examples showing exact
before/after migration for every phase, full dependency configurations,
expect/actual implementations, and the complete week-by-week timeline
is available here:
Looking forward to feedback from the community and mentors!