Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Droidcon London: Beyond the UI

Droidcon London: Beyond the UI

In this talk, we’ll explore how Compose Multiplatform can reshape not just the user interface, but the entire architecture of your app.

You’ll learn techniques for structuring state, managing business logic, and creating modular, testable, and maintainable systems across platforms.

Whether you’re targeting mobile, desktop, or beyond, this session will give you the tools and perspective to design applications that exploit the efficacy of Compose.

Avatar for Ash Davies

Ash Davies

October 30, 2025
Tweet

More Decks by Ash Davies

Other Decks in Programming

Transcript

  1. Beyond the UI Compose as a Foundation for Multiplatform Apps

    Droidcon London - October '25 ! Ash Davies | ashdavies.dev Android GDE Berlin 1
  2. @Composable fun JetpackCompose() { Card { var expanded by remember

    { mutableStateOf(false) } Column(Modifier.clickable { expanded = !expanded }) { Image(painterResource(R.drawable.jetpack_compose)) AnimatedVisibility(expanded) { Text( text = "Jetpack Compose", style = MaterialTheme.typography.bodyLarge, ) } } } } ashdavies.dev 3
  3. Android Layouts <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"

    android:orientation="vertical"> <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello, I am a TextView" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello, I am a Button" /> </LinearLayout> ashdavies.dev 5
  4. Intelligent Recomposition ! Compose UI @Composable fun ClickCounter(clicks: Int, onClick:

    () -> Unit) { Button(onClick = onClick) { Text("I've been clicked $clicks times") } } ashdavies.dev 13
  5. Kotlin Language Features ! Compose UI — Default parameters —

    Higher order functions — Trailing lambdas — Scopes / Receivers — Delegated properties — ... ashdavies.dev 14
  6. Compose is, at its core, a general-purpose tool for managing

    a tree of nodes of any type ... a “tree of nodes” describes just about anything, and as a result Compose can target just about anything. — Jake Wharton jakewharton.com/a-jetpack-compose-by-any-other-name 17
  7. “I suppose it is tempting, if the only tool you

    have is a hammer, to treat everything as if it were a nail.” — Abraham Maslow ashdavies.dev 28
  8. // Compiled Compose code fun Counter($composer: Composer) { $composer.startRestartGroup(-1913267612) /*

    ... */ $composer.endRestartGroup() } // Compiled Coroutines code fun counter($completion: Continuation) { /* ... */ } ashdavies.dev 29
  9. @Suppress("DEPRECATION") class CallbackLoginPresenter( private val service: SessionService, private val goTo:

    (Screen) -> Unit, ) { /* ... */ inner class LoginAsyncTask : AsyncTask<Submit,Void,LoginResult>() { private var username: String = "" override fun doInBackground(vararg events: Submit?): LoginResult { val event = events[0]!! username = event.username return runBlocking { service.login(event.username, event.password) } } override fun onPostExecute(result: LoginResult?) { when (result) { is Success -> goTo(LoggedInScreen(username)) is Failure -> goTo(ErrorScreen(result.throwable?.message ?: "")) else -> {} } } } } speakerdeck.com/ashdavies/droidcon-nyc-demystifying-molecule?slide=27 31
  10. Observable.just("Hey") .subscribeOn(Schedulers.io()) .map(String::length) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { doAction() } .flatMap

    { doAction() Observable.timer(1, TimeUnit.SECONDS) .subscribeOn(Schedulers.single()) .doOnSubscribe { doAction() } } .subscribe { doAction() } proandroiddev.com/how-rxjava-chain-actually-works-2800692f7e13 32
  11. KotlinX Coroutines — Lightweight memory usage — Structured concurrency —

    Cancellation propagation — Lifecycle aware ashdavies.dev 33
  12. Reactive Architecture — Push (not pull) — Unidirectional Data Flow

    — Declarative — Idempotent ashdavies.dev 35
  13. downloadManager .downloadFile("https://.../") .flatMap { result -> fileManager.saveFile("storage/file", result) } .observe

    { success -> if (success) { println("Downloaded file successfully") } } ashdavies.dev 37
  14. val file = downloadFile("https://.../") val success = fileManager.saveFile("storage/file", file) if

    (success) { println("Downloaded file successfully") } ashdavies.dev 38
  15. - downloadManager - .downloadFile("https://.../") - .flatMap { result -> -

    fileManager.saveFile("storage/file", result) - } - .observe { success -> - if (success) { - println("Downloaded file successfully") - } - } + downloadManager. + downloadFile("https://.../") + .flatMapLatest { state -> + when (state) { + is State.Loaded -> + stateFileManager + .saveFile("storage/file", state.value) + + else -> state + } + } + .collect { state -> + when (state) { + is State.Saved -> + println("Downloaded file successfully") + + is State.Loading -> + /* ... */ + } + } ashdavies.dev 39
  16. val downloadState = downloadManager .downloadFile("https://.../") .collectAsState(State.Downloading) val fileState = when(downloadState)

    { is State.Loaded -> stateFileManager .saveFile("storage/file", downloadState.value) else -> downloadState } when (fileState) { is State.Loading -> /* ... */ is State.Saved -> LaunchedEffect(fileState) { println("Downloaded file successfully") } } ashdavies.dev 41
  17. Molecule fun CoroutineScope.launchCounter(): StateFlow<Int> { return launchMolecule(mode = ContextClock) {

    var count by remember { mutableStateOf(0) } LaunchedEffect(Unit) { while (true) { delay(1_000) count++ } } count } } ashdavies.dev 43
  18. Testing @Test fun counter() = runTest { moleculeFlow(RecompositionMode.Immediate) { Counter()

    }.test { assertEquals(0, awaitItem()) assertEquals(1, awaitItem()) assertEquals(2, awaitItem()) cancel() } } ashdavies.dev 44
  19. import com.arkivanov.decompose.ComponentContext class DefaultRootComponent( componentContext: ComponentContext, ) : RootComponent, ComponentContext

    by componentContext { init { lifecycle... // Access the Lifecycle stateKeeper... // Access the StateKeeper instanceKeeper... // Access the InstanceKeeper backHandler... // Access the BackHandler } } ashdavies.dev 53
  20. Voyager class PostListScreen : Screen { @Composable override fun Content()

    { // ... } @Composable private fun PostCard(post: Post) { val navigator = LocalNavigator.currentOrThrow Card( modifier = Modifier.clickable { navigator.push(PostDetailsScreen(post.id)) } ) { // ... } } } ashdavies.dev 55
  21. Circuit State @Parcelize data object HomeScreen : Screen { data

    class State( val title: String, ): CircuitUiState } ashdavies.dev 60
  22. Circuit Presenter class HomePresenter : Presenter<HomeScreen.State> { @Composable override fun

    present(): HomeScreen.State { return HomeScreen.State("Hello World") } } ashdavies.dev 61
  23. Circuit UI @Composable fun HomeScreen( state: HomeScreen.State, modifier: Modifier =

    Modifier, ) { Text( text = state.title, modifier = modifier, ) } ashdavies.dev 62
  24. Circuit val circuit = Circuit.Builder() .addPresenter<HomeScreen, HomeScreen.State>(HomePresenter()) .addUi<LauncherScreen, LauncherScreen.State> {

    _, _ -> HomeScreen(state, modifier) } .build() CircuitCompositionLocals(circuit) { val backStack = rememberSaveableBackStack(HomeScreen) NavigableCircuitContent( navigator = rememberCircuitNavigator(backStack), backStack = backStack, ) } ashdavies.dev 63
  25. Circuit Navigation @Parcelize data object HomeScreen : Screen { data

    class State( val title: String, val eventSink: (Event) -> Unit ): CircuitUiState sealed interface Event { data class DetailClicked( val id: String, ): Event } ashdavies.dev 64
  26. Circuit Navigation class HomePresenter(private val navigator: Navigator) : Presenter<HomeScreen.State> {

    @Composable override fun present(): HomeScreen.State { return HomeScreen.State("Hello World") { event -> when (event) { is HomeScreen.Event.DetailClicked -> navigator.goTo(DetailScreen(event.id)) } } } } ashdavies.dev 65
  27. Remember ! var path by remember { mutableStateOf("https://.../") } val

    file = remember(path) { downloadManager.downloadFile(path) } ashdavies.dev 76
  28. Remember ! var path by remember { mutableStateOf("https://.../") } val

    file = rememberSaveable(path) { // Must be Parcelable on Android! downloadManager.downloadFile(path) } ashdavies.dev 77
  29. Compose Multiplatform Material Theming MaterialTheme( colorScheme = /* ... */,

    typography = /* ... */, shapes = /* ... */, ) { // M3 app content } ashdavies.dev 81
  30. @Composable fun AppTheme( theme: Theme, content: @Composable () -> Unit

    ) { AdaptiveTheme( material = { // Tweak this for your Material design MaterialTheme(content = it) }, cupertino = { // Tweak this for your iOS design CupertinoTheme(content = it) }, target = theme, content = content ) } ashdavies.dev 83
  31. Beyond the UI Wrap-Up ✅ Compose is more than a

    UI toolkit ✅ Enables scalable, shared architecture ✅ Designed for Kotlin-first developers ✅ Multiplatform not just business logic ashdavies.dev 85