Recently, our team of native Android developers embarked on an exploration of Compose Multiplatform, a promising cross-platform technology, in an internal project. This attempt aimed to develop a mobile application compatible with both Android and iOS platforms, featuring a singular functionality: invoking a Firebase function after authentication via Google Sign-In.
We designed our app with a focus on simplicity, incorporating a single feature and screen to assess the technology’s capabilities. Given that this article presumes the reader has a basic understanding of Kotlin Multiplatform (KMP), we’ll highlight what distinguishes Compose Multiplatform in the realm of cross-platform development. For those new to the subject or seeking to broaden their knowledge, we recommend visiting JetBrains’ Compose Multiplatform overview and the official Kotlin documentation on multiplatform libraries.
What sets Compose Multiplatform apart?
Compose Multiplatform stands out in the crowded space of cross-platform development frameworks by leveraging the robustness of Kotlin Multiplatform (KMP) and extending it with a declarative UI toolkit designed for simplicity. Compose Multiplatform offers a unified development experience, allowing developers to write UI code once and deploy it on both Android and iOS platforms with native performance!
Its integration with KMP enables the sharing not only of business logic but also UI components across platforms, significantly reducing development time and maintaining the consistency of in-app behavior and appearance. And that’s not all! Compose Multiplatform is backed by JetBrains, the creators of Kotlin, ensuring continuous updates, improvements, and a growing ecosystem.
This synergy between a declarative UI framework and a shared codebase philosophy fundamentally changes the game, providing developers with a powerful toolset to build truly native applications with less code and greater efficiency.
Core advantages of Compose Multiplatform
Following the completion of our internal project, the team conducted a review to talk about what makes Compose Multiplatform so good. A great highlight was the framework’s exceptional ease in onboarding Android developers.
The ability to implement UI for both platforms using Composable functions and Kotlin streamlined the development process, enabling our team to create visually consistent features with ease and efficiency. This makes it so much easier and faster for Android developers to work with Compose Multiplatform.
Our project’s performance metrics were equally promising. We observed that Compose Multiplatform delivered satisfying performance levels across both Android and iOS apps without any noticeable issues. That makes us think that there is potential for future, bigger projects utilizing Compose Multiplatform.
The efficiency of UI implementation with Compose Multiplatform cannot be overstated. The use of Composable functions simplifies the development process, effectively reducing the time and effort required for UI design. This approach not only accelerates development but also ensures a consistent and high-quality user experience across platforms.
Compose Multiplatform is also constantly getting better and adding new features, like the ability to make web apps using the same code. This is a huge advantage because it means we can do a lot more with it as compared to other tools, like Flutter. Plus, there are a lot of libraries we can use to add features to our app easily.
Overcoming Challenges
However, working with Compose Multiplatform isn’t always easy. One of the biggest problems we faced was that there weren’t many resources or help from other developers available online. This meant we often had to figure things out on our own, which took extra time. But as more people start using Compose Multiplatform, we hope this will improve.
Another significant challenge was the steep learning curve faced by our iOS developers. Despite the framework’s advantages, this transition required a considerable adjustment, particularly for developers who only have experience with iOS native development. The project ended up being mostly led by our Android developers, with the iOS developers helping out with specific iOS-related tasks. This approach worked out, but it showed us that having a good mix of skills on the team is important.
We also encountered iOS-specific challenges, particularly related to compatibility issues with Xcode and project configurations. Fixing these problems required us to learn more about how iOS development works, which was a challenge for our team members who were more familiar with Android.
Even with these challenges, working on this project taught us a lot about what Compose Multiplatform can do and what to watch out for. These experiences have made us better prepared for our next projects and not only enriched our understanding but also prepared us for future projects leveraging this promising technology.
Practical Insights from our project
Let’s talk about what we learned from the project we worked on using Compose Multiplatform. Our project was simple: it had just a single button. When you press this button, it calls for a Firebase function (what this function does is not important for this article). But, there’s a catch – you have to sign in with Google first. If the sign-in works, you can press the button, and you’ll see a success animation. If something goes wrong, you’ll see an error animation instead.
Dependency Injection with Koin
In our journey with Compose Multiplatform, implementing Dependency Injection was a cornerstone for organizing our project architecture. We used the Koin library, for its simplicity and Kotlin-oriented design, to manage our dependencies. This choice allowed us to maintain a modular architecture, ensuring components, such as use cases and repositories, could be easily managed and replaced as needed. The use of Koin not only streamlined our development process but also enhanced the maintainability of our codebase by providing a clear structure for dependency management.
Resource Management with MOKO Resources
Another cornerstone of our project was efficient resource management across both Android and iOS platforms. To tackle this, we utilized MOKO Resources, a library that enabled us to centralize our app’s resources, such as strings, colors, and images. This meant we could effortlessly ensure consistency in our app’s look and feel on both platforms, streamline the localization process for different languages, and significantly reduce the overhead associated with maintaining duplicate resource files.
UI Implementation Across Platforms
Our project was organized into three main packages: shared, androidApp, and iosApp, each playing a key role in our application’s cross-platform development. Integrating Compose Multiplatform into our project allowed us to leverage Kotlin’s powerful features to create a unified UI layer that works seamlessly across Android and iOS. This approach significantly streamlined the UI development process, making it more efficient and consistent across platforms. Here’s a closer look at how we achieved this:
For the Android platform, our approach was straightforward and utilized the Compose framework within MainActivity (in the androidApp package):
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainView()
}
}
}
In addition, we defined a composable function to handle the UI logic, demonstrating the power and flexibility of Compose:
@Composable
fun MainView(googleAuthProvider: GoogleAuthProvider = get()) = MainScreen(
authProvider = googleAuthProvider
)
This composable function, MainScreen (in the shared package), encapsulates the UI implementation, maintaining a consistent development similar to native app development with Compose. Check it out below:
@Composable
fun MainScreen(authProvider: GoogleAuthProvider) {
MaterialTheme {
val scaffoldState = rememberScaffoldState()
val snackbarHostState = scaffoldState.snackbarHostState
val coroutineScope = rememberCoroutineScope()
val loggedOutMessage = stringResource(MR.strings.logged_out)
Scaffold(
scaffoldState = scaffoldState,
topBar = {
SesameTopBar(
onShowLogoutInfo = {
coroutineScope.launch {
snackbarHostState.showSnackbar(loggedOutMessage)
}
}
)
},
modifier = Modifier
.fillMaxSize()
.background(color = colorResource(MR.colors.black)),
backgroundColor = colorResource(MR.colors.black),
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly,
) {
LineOfShapes(largerSpaceAfterIndex = 1)
Spacer(modifier = Modifier.height(16.dp))
OpenGate(googleAuthProvider = authProvider)
Spacer(modifier = Modifier.height(16.dp))
LineOfShapes(largerSpaceAfterIndex = 0)
}
}
}
}
On the iOS side, we bridged the Compose UI with SwiftUI through a ComposeView in ContentView.swift file (in the iosApp package), enabling us to inject our Compose UI into the iOS application structure:
import UIKit
import SwiftUI
import shared
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
Main_iosKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
ComposeView().ignoresSafeArea(.all, edges: .bottom)
}
}
The MainViewController function serves as the entry point for invoking the same MainScreen composable function used in the Android implementation, ensuring UI consistency:
fun MainViewController() = ComposeUIViewController {
MainScreen(
authProvider = koinInject()
)
}
By leveraging Compose Multiplatform, we were able to consistently inject dependencies, such as the authentication provider, across both platforms. This unified approach to UI development not only simplified our workflow but also ensured that our app provided a coherent user experience, regardless of the platform.
The rest of the project
Additionally, the core of our application, including architecture and animations, was implemented in the commonMain package using a Clean Architecture MVVM approach with use cases to ensure a clean separation of concerns. All animations were crafted in Compose, showcasing its robust capability for creating engaging user interfaces. You can check out an example of our animation implementation below:
@Composable
private fun LoadingUI() {
GateUIWrapper(
borderColor = colorResource(MR.colors.transparent),
content = {
CircularProgressIndicator(
modifier = Modifier.size(250.dp),
color = colorResource(MR.colors.primaryGreen)
)
}
)
}
To improve screen UI we have added dynamic shape generation the same way we do it on Android native development. You can check out an example below:
enum class ShapeType {
CIRCLE, SQUARE, DIAMOND
}
@Composable
fun SingleShape(shapeType: ShapeType, size: Dp, color: Color) {
Canvas(modifier = Modifier.size(size)) {
when (shapeType) {
ShapeType.CIRCLE -> drawCircle(color = color, radius = size.toPx() / 2)
ShapeType.SQUARE -> drawRect(color = color, size = Size(size.toPx(), size.toPx()))
ShapeType.DIAMOND -> drawDiamondPath(color, size)
}
}
}
private fun DrawScope.drawDiamondPath(color: Color, size: Dp) {
val path = Path().apply {
moveTo(size.toPx() / 2, 0f)
lineTo(0f, size.toPx() / 2)
lineTo(size.toPx() / 2, size.toPx())
lineTo(size.toPx(), size.toPx() / 2)
close()
}
drawPath(path, color)
}
@Composable
fun LineOfShapes(largerSpaceAfterIndex: Int, spacing: Dp = 56.dp) {
val shapeSize = 40.dp
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.Center,
) {
repeat(3) { index ->
val color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat(), 1f)
val shapeType = ShapeType.values().random()
SingleShape(shapeType, shapeSize, color)
if (index != 2) {
val spacerModifier =
if (index == largerSpaceAfterIndex) Modifier.width(spacing) else Modifier.weight(
1f
)
Spacer(modifier = spacerModifier)
}
}
}
}
For networking, we chose the Ktor library, favoring its Kotlin-native approach for seamless integration. We defined an interface for the repository responsible for calling the Firebase function. This repository was then utilized within a use case, which was subsequently invoked by a ViewModel, illustrating a clear flow of data and actions throughout the application’s architecture. This setup not only streamlined development across platforms but also ensured that our code remained manageable and scalable. To summarize this part of the project, we used the same implementation as we would for native Android applications!
Is Compose Multiplatform ready for commercial projects?
After diving into Compose Multiplatform for our internal project, it’s safe to say we’re pretty impressed with what it could do for commercial projects. We’ve seen firsthand how it makes life easier for Android developers, letting us share a lot of the heavy lifting across Android and iOS without doubling our workload. Sure, we ran into a few bumps along the way, like the learning curve for our iOS buddies and XCode setup issues, but that’s all part of the adventure in tech, right?
All the developers involved in our internal project agreed that having support from at least one iOS developer would be crucial to quickly tackle the obstacles related to iOS-specific elements, such as XCode, ensuring smoother progress and more efficient problem-solving.
The real highlight is how Compose Multiplatform enables us to maintain a consistent UI across platforms without compromising performance. Through collaborative effort and smart coding strategies, we successfully integrated Google Sign-In, utilized the Firebase function, and achieved fluid animations, showcasing the framework’s robust capabilities.
So, what’s our take on whether Compose Multiplatform is ready for larger-scale commercial projects? Well, it shows a lot of promise, and there’s definitely potential for simple apps. We’ve faced similar challenges to those encountered with Kotlin Multiplatform (KMP), such as setup and configuration issues. While there’s hope and a lot of enthusiasm for what it might offer in the future, we can’t definitively say it’s ready for big projects just yet, simply because we haven’t tested it in that context. With JetBrains’ ongoing support and the technology continually improving, we’re keen to see how Compose Multiplatform evolves and eventually readies for more complex commercial endeavors.