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

mDevCamp Prague: Beyond the UI

mDevCamp Prague: Beyond the UI

Much in the same way that Coroutines revolutionized reactive architecture, Compose challenges us to rethink how we design applications. While traditionally seen as a UI toolkit, Compose — and specifically Compose Multiplatform — can play a much larger role in our applications. By leveraging its declarative and state-driven nature, we can build, model, and manage application state in a way that is consistent, scalable, and platform-agnostic.

Avatar for Ash Davies

Ash Davies

June 03, 2025
Tweet

More Decks by Ash Davies

Other Decks in Programming

Transcript

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

    mDevCamp - June '25 ! Ash Davies | ashdavies.dev Android GDE Berlin
  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. 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
  4. Intelligent Recomposition ! Compose UI @Composable fun ClickCounter(clicks: Int, onClick:

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

    Higher order functions — Trailing lambdas — Scopes / Receivers — Delegated properties — ... ashdavies.dev
  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
  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
  8. // Compiled Compose code fun Counter($composer: Composer) { $composer.startRestartGroup(-1913267612) /*

    ... */ $composer.endRestartGroup() } // Compiled Coroutines code fun counter($completion: Continuation) { /* ... */ } ashdavies.dev
  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
  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
  11. KotlinX Coroutines — Lightweight memory usage — Structured concurrency —

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

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

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

    (success) { println("Downloaded file successfully") } ashdavies.dev
  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
  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
  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
  18. Testing @Test fun counter() = runTest { moleculeFlow(RecompositionMode.Immediate) { Counter()

    }.test { assertEquals(0, awaitItem()) assertEquals(1, awaitItem()) assertEquals(2, awaitItem()) cancel() } } ashdavies.dev
  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
  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
  21. Circuit State @Parcelize data object HomeScreen : Screen { data

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

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

    Modifier, ) { Text( text = state.title, modifier = modifier, ) } ashdavies.dev
  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
  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
  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
  27. Remember ! var path by remember { mutableStateOf("https://.../") } val

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

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

    typography = /* ... */, shapes = /* ... */, ) { // M3 app content } ashdavies.dev
  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
  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