This document outlines the tasks required to refactor the Wabbitemu Android application into a Kotlin Compose Multiplatform app targeting iOS and Desktop. The C code will be kept as-is and compiled natively for each platform.
The plan is structured as a series of sequential phases (the outer lists). Within each phase, the inner lists represent parallelable tasks, where each task is scoped to be an appropriate size for a single Pull Request (PR).
These tasks lay the foundation for Kotlin Multiplatform (KMP) by updating the build system and separating the existing Android app into its own module.
- 1.1. Migrate Build Scripts to Kotlin DSL:
- Migrate the root
build.gradletobuild.gradle.kts. - Migrate
app/build.gradletoapp/build.gradle.kts. - Migrate
settings.gradletosettings.gradle.kts.
- Migrate the root
- 1.2. Implement Gradle Version Catalog:
- Create
gradle/libs.versions.toml. - Move all existing dependencies and plugin versions from build scripts to the version catalog.
- Update all
build.gradle.ktsfiles to use the version catalog.
- Create
- 1.3. Extract Android Application Module:
- Create a new module directory (e.g.,
androidApp). - Move the existing
app/srcdirectory,AndroidManifest.xml, and Android-specific resources intoandroidApp/src/main. - Update
settings.gradle.ktsto include:androidAppinstead of:app.
- Create a new module directory (e.g.,
- 1.4. Initialize Shared KMP Module:
- Create a new module directory
shared. - Configure
shared/build.gradle.ktswith the Kotlin Multiplatform plugin. - Define targets for
android,iosX64,iosArm64,iosSimulatorArm64, andjvm(Desktop). - Set up the basic source sets (
commonMain,androidMain,iosMain,desktopMain). - Update
settings.gradle.ktsto include:sharedand add thesharedmodule as a dependency toandroidApp.
- Create a new module directory
The core emulator logic written in C/C++ must be decoupled from the Android JNI bindings (wabbitemujni.c) so it can be compiled and linked on all target platforms.
- 2.1. Isolate Core C Logic:
- Create a new directory structure (e.g.,
shared/src/nativeInterop/cinterop/wabbitemu). - Move the core emulator files (
core/,hardware/,utilities/) into this directory. - Leave
wabbitemujni.candwabbitemujni.hin theandroidAppmodule (or an Android-specific native folder) since they contain JNI specifics.
- Create a new directory structure (e.g.,
- 2.2. Configure iOS Static Library Build:
- Create an Xcode project or a build script (e.g., using CMake or raw Clang invocations) to compile the core C files into an iOS static library (
.a). - Ensure the build script handles all required iOS architectures (arm64, x86_64).
- Verify the library compiles successfully on macOS.
- Create an Xcode project or a build script (e.g., using CMake or raw Clang invocations) to compile the core C files into an iOS static library (
- 2.3. Configure Desktop Dynamic Library Build:
- Create a
CMakeLists.txtin the native core directory specifically for Desktop builds. - Configure CMake to compile the core C files into a dynamic library (
.dylibfor macOS,.dllfor Windows,.sofor Linux). - Verify the library compiles successfully on a desktop environment.
- Create a
- 2.4. Generate Kotlin Native Bindings (cinterop):
- Create a
.deffile (e.g.,wabbitemu.def) inshared/src/nativeInterop/cinterop/. - Configure the
.deffile to point to the core C headers (e.g.,calc.h,state.h). - Update
shared/build.gradle.ktsto configure thecinteroptask for all iOS targets, ensuring Kotlin Native can generate bindings for the C code.
- Create a
This phase bridges the gap between the Kotlin code and the underlying C code on each platform using Kotlin's expect/actual mechanism.
- 3.1. Define Shared Interface:
- In
shared/src/commonMain/kotlin/io/github/angelsl/wabbitemu/calc/, createCalcInterface.kt. - Define
expect class CalcInterfacemirroring the static methods currently in the JavaCalcInterface.
- In
- 3.2. Implement Android JNI Bridge:
- In
shared/src/androidMain/kotlin/io/github/angelsl/wabbitemu/calc/, implement theactual class CalcInterface. - Map the
actualmethods to the existing JNI calls defined inwabbitemujni.c. - Ensure the
Wabbitemunative library is loaded correctly in the Android target.
- In
- 3.3. Implement iOS cinterop Bridge:
- In
shared/src/iosMain/kotlin/io/github/angelsl/wabbitemu/calc/, implement theactual class CalcInterface. - Map the
actualmethods to the Kotlin Native bindings generated bycinterop(from Phase 2). - Handle C-pointers and memory management manually where necessary using
kotlinx.cinterop.
- In
- 3.4. Implement Desktop JNI/JNA Bridge:
- In
shared/src/desktopMain/kotlin/io/github/angelsl/wabbitemu/calc/, implement theactual class CalcInterface. - Choose between JNI (writing new C++ wrappers for the Desktop dynamic library) or JNA (Java Native Access) to interface with the dynamic library built in Phase 2.
- Map the
actualmethods to the chosen bridge.
- In
The core application logic (CalculatorManager, CalcThread, etc.) needs to be moved to the shared module, which requires abstracting away Android-specific APIs.
- 4.1. Abstract Logging:
- In
shared/src/commonMain/, create anexpect class Logger. - Implement
actual class Loggerusingandroid.util.LoginandroidMain. - Implement
actual class LoggerusingNSLogoros_loginiosMain. - Implement
actual class Loggerusing standard output (println) or a JVM logging framework indesktopMain. - Replace all Android
Log.*calls in the core logic with the newLogger.
- In
- 4.2. Abstract SharedPreferences:
- Add a multiplatform settings library (e.g.,
com.russhwolf:multiplatform-settings) to the version catalog andsharedbuild script. - Refactor the code to use this library instead of Android's
SharedPreferences.
- Add a multiplatform settings library (e.g.,
- 4.3. Abstract File System Access:
- Create an
expect class FileSystemincommonMainfor reading/writing files and handling paths (e.g., for ROMs and savestates). - Implement the
actualclass inandroidMainusing Android's Context/FilesDir. - Implement the
actualclass iniosMainusingNSFileManager. - Implement the
actualclass indesktopMainusingjava.io.Fileorjava.nio.file. - Refactor the native C code (or the Kotlin bridge) to accept raw file bytes or standardized paths instead of Android Uris where applicable.
- Create an
- 4.4. Migrate Core Logic to Shared Module:
- Move
CalculatorManager.java(converting to Kotlin),CalcThread.java,MainThread.java, and related models toshared/src/commonMain. - Refactor
CalcThreadandMainThreadto use Kotlin Coroutines (kotlinx.coroutines) instead of JavaThreads, ensuring thread safety and platform compatibility.
- Move
This phase focuses on rebuilding the UI using Compose Multiplatform, replacing the Android XML layouts and Views.
- 5.1. Setup Compose Multiplatform:
- Add Compose Multiplatform plugins and dependencies to the version catalog and the
sharedbuild script. - Define a shared
Theme.kt(Typography, Colors, Shapes) inshared/src/commonMain.
- Add Compose Multiplatform plugins and dependencies to the version catalog and the
- 5.2. Migrate Navigation and Main Layout:
- Implement a shared navigation system (e.g., using Voyager or a custom state-based router) in
commonMain. - Create the main App scaffolding (Drawer, TopAppBar) in Compose.
- Implement a shared navigation system (e.g., using Voyager or a custom state-based router) in
- 5.3. Migrate the Setup Wizard UI:
- Port the
LandingPageView,ChooseOsPageView,ModelPageView, andOsDownloadPageViewfrom Android XML to Compose UI incommonMain. - Connect these new Compose screens to the abstracted
CalculatorManagerand file system.
- Port the
- 5.4. Migrate Settings and About Screens:
- Port the
SettingsActivityandAboutActivityto Compose UI incommonMain.
- Port the
- 5.5. Abstract File Picker UI:
- Create an
expect fun openFilePicker()incommonMain. - Implement the
actualfunction inandroidMainusing Android Intents (ACTION_GET_CONTENT). - Implement the
actualfunction iniosMainusingUIDocumentPickerViewController. - Implement the
actualfunction indesktopMainusing a SwingJFileChooseror JavaFXFileChooser.
- Create an
The most complex UI component is the emulator screen and the interactive calculator skin.
- 6.1. Port the Emulator LCD Renderer:
- Replace
WabbitLCD.java(which uses AndroidSurfaceView) with a ComposeCanvasincommonMain. - Implement logic to receive the raw ARGB pixel buffer from the
CalcInterfaceand draw it onto the ComposeCanvasefficiently (e.g., usingImageBitmap).
- Replace
- 6.2. Port the Calculator Skin Renderer:
- Refactor
SkinBitmapLoader.javaandCalcSkin.javainto Compose UI. - Load the skin images as Compose
ImageBitmaps. - Implement dynamic layout logic to position the skin and the LCD canvas correctly based on the device screen size and constraints.
- Refactor
- 6.3. Implement Cross-Platform Touch Input:
- Replace Android's
onTouchEventhandling with Compose'sModifier.pointerInput. - Map the pointer events (down, move, up) to the corresponding key press/release logic in
CalcInterface. - Ensure the hitboxes for the calculator buttons scale correctly with the Compose layout.
- Replace Android's
The final phase involves wiring up the entry points for each platform to launch the shared Compose UI.
- 7.1. Android Entry Point Integration:
- Update
WabbitemuActivity.ktin theandroidAppmodule to launch the shared Compose UI usingsetContent { App() }. - Ensure all necessary Android permissions (e.g., storage) are requested correctly.
- Update
- 7.2. Desktop Entry Point Integration:
- Create a new module or source set for the desktop application.
- Implement the main
main()function using Compose for Desktop'sWindow { App() }. - Ensure the dynamic libraries built in Phase 2 are bundled and loaded correctly at runtime.
- 7.3. iOS Entry Point Integration:
- Create a standard Xcode project for the iOS app.
- Configure the Xcode project to link against the shared Kotlin framework generated by KMP.
- Write
App.swift(orViewController.swift) to useComposeUIViewControllerto host the shared KMP UI. - Ensure the static C library built in Phase 2 is linked correctly in the Xcode build phases.