Blog Infos
Author
Published
Topics
,
Published
Topics
,

Compose Multiplatform stands as a declarative framework dedicated to facilitating the seamless sharing of UIs across diverse platforms. Based on Kotlin and Jetpack Compose, JetBrains introduces its distinctive perspective on a cross-platform¹ framework.

Within the scope of this story, we shall delve into two distinct strategies for managing UI State. In essence, we explore techniques to update your Compose-driven UI, particularly when employing it to share layouts with iOS. Prior to embarking on our exploration, it is assumed that the reader possesses understanding of Kotlin MultiplatformJetpack Compose, and SwiftUI.

While not obligatory, familiarity with my story on the subject of Sharing UI State management with Kotlin Multiplatform Mobile could prove beneficial.

Intro

When you use Compose Multiplatform on iOS, the Kotlin code for your UI is compiled to native code using Kotlin/Native. This native code is then used to create a UIKit-based UI that runs on the iOS platform. Compose Multiplatform for iOS provides the Kotlin APIs for building Compose UIs on iOS. This library bridges the gap between the Kotlin/Native code and the UIKit framework.

Kotlin code
   |
   v
Kotlin/Native compiler
   |
   v
Native code for iOS
   |
   v
UIKit framework
   |
   v
Compose UI

The alpha² release of Compose Multiplatform introduces a prototype for bidirectional interaction at the UI level. With the help of UIKitView, you can seamlessly integrate intricate platform-specific widgets — such as maps, web views, media players, and camera feeds — into your shared user interface. Conversely, employing ComposeUIViewController allows you to nest Compose Multiplatform screens within SwiftUI applications, facilitating a gradual integration of Compose Multiplatform into your iOS apps.

Our emphasis will be on the latter.

No state management on iOS

By this, I’m referring to a screen implementation in which state changes are managed by the multiplatform (shared-ui) module, in Kotlin. This implies that on the iOS side, our task involves just creating a container to display it.

Let me simplify this with an example. We’ll create a Composable containing two buttons for navigating between screens. On iOS, we’ll include this Composable and set up our navigation.

In the shared-ui module, we create a function that gives us a UIViewController. This function wraps our Composable implementation within ComposeUIViewController:

fun selector(onClickA: () -> Unit, onClickB () -> Unit): UIViewController {
    return ComposeUIViewController { /*compose implementation...*/ }
}

In iOS we create a UIViewControllerRepresentable that will import the ComposeUIViewController:

private struct SelectorUIViewController: UIViewControllerRepresentable {
    
    let showScreenA: () -> Void
    let showScreenB: () -> Void
    
    func makeUIViewController(context: Context) -> UIViewController {
        return SharedViewControllersKt.selector(onClickA: showScreenA, onClickB: showScreenB)
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

We finish it by setting up our navigation:

private enum Destination: Hashable {
    case screenA
    case screenB
}

struct HomeScreen: View {
    
    @State var navigation = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $navigation) {
            SelectorUIViewController(
                showScreenA: { navigation.append(Destination.screenA) },
                showScreenB: { navigation.append(Destination.screenB) }
            )
            .navigationDestination(for: Destination.self) { destination in
                switch destination {
                case .screenA:
                    ScreenA()
                case .screenB:
                    ScreenB()
                }
            }
            .ignoresSafeArea()
        }
    }
}

And it’s done. We have a Composable inside a SwiftUI View.

Mixed state management

By this, I’m referring to a screen implementation in which state changes are not managed by the shared-ui module. This implies that on the iOS side, we must forward state changes to the shared-ui module.

In this example, we have a SwiftUI View where an observable ViewModel handles the state, passing changes to State and Binding properties:

struct StatusScreen: View {
    
    @StateObject private var viewModel = StatusViewModel()
    @State private var status: String = ""
    
    var body: some View {
        StatusUIViewController(
            status: $status,
            action: { viewModel.changeStatus() }
        )
        .onReceive(viewModel.$state) { new in            
            status = new.status
        }
        .ignoresSafeArea()
    }
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Managing a state might be a challenge. Managing the state with hundreds of updates and constant recomposition of floating emojis is a challenge indeed.
Watch Video

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Piotr Prus
Android developer

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Piotr Prus
Android developer

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Piotr Prus
Android developer

Jobs

Our UIViewControllerRepresentable:

struct StatusUIViewController: UIViewControllerRepresentable {
    
    @Binding var status: String
    let action: () -> Void
    
    func makeUIViewController(context: Context) -> UIViewController {
        return SharedViewControllers().statusComposable(status: status, click: action)
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        //how to update the composable state when binding values changes...?
    }
}

Despite the invocation of updateUIViewController, the Multiplatform Compose API lacks³ a mechanism to communicate these changes to the Composable.

Thankfully, there’s a workaround we can employ. Within our ComposeUIViewController, we gather state changes using a MutableStateFlow from kotlinx.coroutines.flow or mutableStateOf from compose.runtime. Subsequently, we expose a function for iOS to invoke, facilitating the emission of these changes:

object SharedViewControllers {

    private data class ViewState(val status: String = "")
    private val state = MutableStateFlow(ViewState())

    fun statusComposable(click: () -> Unit): UIViewController {
        return ComposeUIViewController {
            with(state.collectAsState().value) {
                 StatusComposable(status, click)
            }
        }
    }

    fun updateStatusComposable(status: String) {
        state.update { it.copy(status = status) }
    }
}

And then our updateUIViewController becomes:

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
    SharedViewControllers().updateStatusComposable(status: status)
}

While not the most elegant approach, it’s essential to keep in mind that we’re working with an alpha version. With time, we can anticipate significant enhancements and refinements.

Conclusions

Just like Kotlin Multiplatform, Compose Multiplatform is tailored for mobile developers. Consequently, I see it as a prime candidate for becoming the go-to cross-platform solution for this community. It’s not so much a question of if, but rather a matter of when.

In its current alpha stage on iOS, Compose Multiplatform offers impressive flexibility for gradual adoption and experimentation. However, it’s important to note that this won’t be a universal solution; rather, it will be another valuable tool in our toolkit. I envision specific use cases where Compose Multiplatform can provide substantial benefits.

Let’s witness its evolution in the near future.

As always, I hope you find this article useful, thanks for reading. You can explore these strategies in a playground available here:

[1]: In the mobile realm, cross-platform frameworks enable using a single technology stack for full implementation (layouts + business logic), e.g., Flutter, React Native. Conversely, multi-platform frameworks permit sharing specific system elements (business logic), while each platform determines its own layout implementation and stack.

[2]: As of May 18, 2023, Compose Multiplatform for iOS Is in Alpha.

[3]: As of Aug 12, 2023, this capability is not yet available — Ability to define a UIViewController type #3478

I want to thank Dima Avdeev from JetBrains for taking the time to discuss and share ideas on the topic.

🎉 Featured in Android Weekly #583

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
In this part of the series, we will plan our first screen in Jetpack…
READ MORE
blog
We’ll be selecting a time whenever a user presses a number key. Following points…
READ MORE
blog
Compose is a relatively young technology for writing declarative UI. Many developers don’t even…
READ MORE
blog
When it comes to the contentDescription-attribute, I’ve noticed a couple of things Android devs…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu