Multiplatform¶
Supported platforms¶
Lifecycle¶
Multiplatform interface:
package com.bumble.appyx.navigation.lifecycle
interface Lifecycle {
val currentState: State
val coroutineScope: CoroutineScope
fun addObserver(observer: PlatformLifecycleObserver)
fun removeObserver(observer: PlatformLifecycleObserver)
fun asFlow(): Flow<State>
enum class State {
INITIALIZED,
CREATED,
STARTED,
RESUMED,
DESTROYED,
}
enum class Event {
ON_CREATE,
ON_START,
ON_RESUME,
ON_PAUSE,
ON_STOP,
ON_DESTROY,
ON_ANY,
}
}
- On Android, it is implemented by
AndroidLifecycle
, which is backed byandroidx.lifecycle
. - On other platforms, it is implemented by the corresponding
PlatformLifecycleRegistry
Parcelable, Parcelize, RawValue¶
Multiplatform typealiases that are backed by their proper platform-specific ones.
E.g. on Android:
package com.bumble.appyx.utils.multiplatform
actual typealias Parcelize = kotlinx.parcelize.Parcelize
actual typealias Parcelable = android.os.Parcelable
actual typealias RawValue = kotlinx.parcelize.RawValue
Node hosts¶
The root node of an Appyx navigation tree needs to be connected to the platform. This ensures that system events (lifecycle, back press, etc.) reach your components in the tree.
You only need to do this for the root of the tree.
Android¶
// Please note we are extending NodeActivity
class MainActivity : NodeActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
YourAppTheme {
NodeHost(
lifecycle = AndroidLifecycle(LocalLifecycleOwner.current.lifecycle),
integrationPoint = appyxIntegrationPoint
) {
RootNode(nodeContext = it)
}
}
}
}
}
Desktop¶
fun main() = application {
val events: Channel<Events> = Channel()
val windowState = rememberWindowState(size = DpSize(480.dp, 640.dp))
val eventScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
Window(
state = windowState,
onCloseRequest = ::exitApplication,
onKeyEvent = {
// See back handling section in the docs below!
onKeyEvent(it, events, eventScope)
},
) {
YourAppTheme {
Surface(color = MaterialTheme.colorScheme.background) {
DesktopNodeHost(
windowState = windowState,
onBackPressedEvents = events.receiveAsFlow().mapNotNull {
if (it is Events.OnBackPressed) Unit else null
}
) {
RootNode(nodeContext = it)
}
}
}
}
}
Web¶
// Add this only when targeting Kotlin/Wasm as this method isn't exposed anywhere
external fun onWasmReady(onReady: () -> Unit)
fun main() {
val events: Channel<Unit> = Channel()
onWasmReady {
CanvasBasedWindow("Your app") {
val requester = remember { FocusRequester() }
var hasFocus by remember { mutableStateOf(false) }
var screenSize by remember { mutableStateOf(ScreenSize(0.dp, 0.dp)) }
val eventScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
YourAppTheme {
Surface(
color = MaterialTheme.colorScheme.background,
modifier = Modifier
.fillMaxSize()
.onSizeChanged { screenSize = ScreenSize(it.width.dp, it.height.dp) }
.onKeyEvent {
// See back handling section in the docs below!
onKeyEvent(it, events, eventScope)
}
.focusRequester(requester)
.focusable()
.onFocusChanged { hasFocus = it.hasFocus }
) {
WebNodeHost(
screenSize = screenSize,
onBackPressedEvents = events.receiveAsFlow(),
) {
RootNode(nodeContext = it)
}
}
if (!hasFocus) {
LaunchedEffect(Unit) {
requester.requestFocus()
}
}
}
}
}
}
iOS¶
val backEvents: Channel<Unit> = Channel()
fun MainViewController() = ComposeUIViewController {
YourAppTheme {
IosNodeHost(
modifier = Modifier,
// See back handling section in the docs below!
onBackPressedEvents = backEvents.receiveAsFlow()
) {
RootNode(
nodeContext = it
)
}
}
}
@Composable
private fun BackButton(coroutineScope: CoroutineScope) {
IconButton(
onClick = {
coroutineScope.launch {
backEvents.send(Unit)
}
},
modifier = Modifier.zIndex(99f)
) {
Icon(
imageVector = Icons.Default.ArrowBack,
tint = Color.White,
contentDescription = "Go Back"
)
}
}
Back handling¶
Android¶
On Android back events are handled automatically.
Desktop & Web¶
In the above desktop and web examples there is a reference to an onKeyEvent
method.
You can configure any KeyEvent
to trigger a back event via the events Channel
. In this example the OnBackPressed
event is launched when the backspace key is pressed down:
private fun onKeyEvent(
keyEvent: KeyEvent,
events: Channel<Events>,
coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main),
): Boolean =
when {
// You can also register e.g. Key.Escape instead of BackSpace:
keyEvent.type == KeyEventType.KeyDown && keyEvent.key == Key.Backspace -> {
coroutineScope.launch { events.send(Events.OnBackPressed) }
true
}
else -> false
}
iOS¶
On iOS, you can design a user interface element to enable back navigation, similar to how it's done on other platforms.
In the example mentioned earlier, we create a Composable component BackButton
that includes an ArrowBack
icon.
When this button is clicked, it triggers the back event through the backEvents
Channel
.
Setting up the environment for execution¶
Setting up the environment for execution
Warning
In order to launch the iOS target, you need a Mac with macOS to write and run iOS-specific code on simulated or real devices.
This is an Apple requirement.
The instructions here are tweaked and inspired from the Compose-Multiplatform-iOS template.
To work with this project, you need the following:
- A machine running a recent version of macOS
- Xcode
- Android Studio
- Kotlin Multiplatform Mobile plugin
- CocoaPods dependency manager
Check your environment¶
Before you start, use the KDoctor tool to ensure that your development environment is configured correctly:
-
Install KDoctor with Homebrew:
brew install kdoctor
-
Run KDoctor in your terminal:
kdoctor
If everything is set up correctly, you'll see valid output:
Environment diagnose (to see all details, use -v option):
[✓] Operation System
[✓] Java
[✓] Android Studio
[✓] Xcode
[✓] Cocoapods
Conclusion:
✓ Your system is ready for Kotlin Multiplatform Mobile development!
Otherwise, KDoctor will highlight which parts of your setup still need to be configured and will suggest a way to fix them.
The project structure¶
Open the project in Android Studio and switch the view from Android to Project to see all the files and targets belonging to the project. The :demos module contains the sample target appyx-navigation.
This module follows the latest compose multiplatform project structure, with a mainApp
module targeting all available platforms (Android, Desktop, iOS and Web).
There is also a companion module named iosApp
with the needed glue code for iOS. There are the targets and purposes:
commonMain¶
This Kotlin module contains the logic common for Android, Desktop, iOS and web applications, that is, the code you share between platforms.
androidMain¶
This is a Kotlin module that builds into an Android application.
desktopMain¶
This module builds into a Desktop application.
iosMain¶
This is an Xcode project that builds into an iOS application.
wasmJsMain¶
This module builds into a Web app.
Run your application¶
On Android¶
To run your application on an Android emulator:
- Ensure you have an Android virtual device available. Otherwise, create one.
- In the list of run configurations, select
demos.appyx-navigation.android
. - Choose your virtual/physical device and click Run.
On iOS¶
Running on a simulator¶
To run your application on an iOS simulator in Android Studio, modify the iOS
run configuration:
- In the list of run configurations, select Edit Configurations:
- Navigate to iOS Application | iosApp.
- Select the desired
.xcworkspace
file underXCode project file
which can be found in/demos/appyx-navigation/iosApp/iosApp.xcworkspace
. - Ensure
Xcode project scheme
is set toiosApp
. - In the Execution target list, select your target device. Click OK.
- The
iosApp
run configuration is now available. Click Run next to your virtual device.
Running on a real device¶
To run the Compose Multiplatform application on a real iOS device. You'll need the following:
- The
TEAM_ID
associated with your Apple ID - The iOS device registered in Xcode
Finding your Team ID¶
In the terminal, run kdoctor --team-ids
to find your Team ID.
KDoctor will list all Team IDs currently configured on your system.
To run the application, set the TEAM_ID
:
- In the project, navigate to the
iosApp/Configuration/Config.xcconfig
file. - Set your
TEAM_ID
. - Re-open the project in Android Studio. It should show the registered iOS device in the
iosApp
run configuration.
On Desktop¶
To run the application as a JVM target on desktop:
- In the list of run configurations, select Edit Configurations.
- Click Add new configuration and select Gradle.
- Set
run
configuration underRun
. - Select the desired target under
Gradle project
to be executed (for example:appyx:demos:appyx-navigation:desktop
). - The desktop configuration for the desired target is now available. Click Run to execute.
On Web¶
To run the application on web:
- In the list of run configurations, select Edit Configurations.
- Click Add new configuration and select Gradle.
- Decide which target you prefer and choose the appropriate task under run:
jsBrowserDevelopmentRun
for targeting Kotlin/JS.wasmJsBrowserDevelopmentRun
for targeting Kotlin/Wasm.
- Select the desired target under
Gradle project
to be executed (for example:appyx:demos:appyx-navigation:web
). - The web configuration for the desired target is now available. Click Run to execute.