Mastering the Mobile Dev Maze: UIKit vs. SwiftUI vs. XML vs. Compose — Part 3
Welcome back, readers! If you’re joining us again for the final installment of this series, it’s great to have you on board. As this article builds directly upon its predecessors, I recommend giving them a read for context and depth — you can find the links just below:
Also, throughout reading the series, you can follow up the repo below, where I published all the apps along with libraries and resources:
In this part, we’ll begin by revisiting the predictions we made in the first part and see how they measure up against the outcomes of our experiments. Following that, I’ll extend my acknowledgments, sharing insights and contributions that have enriched this exploratory journey. So, without further ado, let’s get started!
1. Comparison of Predictions and Outcomes
If you’re coming from the previous installments, you probably have a good idea of what I’m about to say. But to simplify and summarize, our predictions pretty much turned into reality. Let me elaborate:
Android:
- Things mostly went as planned. Since CMP uses the same package as Jetpack Compose, and much of the Jetpack Compose code is available on the common side, we expected similar FPS and memory usage performances between the Base versions and the KMP & CMP versions. The results confirmed this similarity.
- However, adding Compose (both Jetpack Compose and CMP) to any XML project affected both FPS performance and app sizes similarly, with the exception of using CMP alone. This resulted in a few MBs more in size increase, which was explained in the previous parts.
- For memory usage, some tests showed KMP with better results compared to the full-native Base versions. But this could be due to the removal of Android’s Lifecycle ViewModel, and further investigation is needed to confirm this.
iOS:
- We were braced for the unexpected, and it certainly occurred. While the SwiftUI implementations were not the best, adding KMP & CMP significantly reduced FPS performance (for CMP) and greatly increased memory usage in MBs, along with a substantial increase in the ipa sizes.
- As previously explained, adding KMP & CMP introduces headers, and CMP comes with SKIA, which majorly impacted the increase in ipa size.
- The instant freezes during app use were a major drawback in the CMP version I used for these experiments. However, using CMP across the entire screen yielded better FPS results compared to integrating it in parts. But it’s important to note that every CMP element is a UIViewController on the iOS side, and using a UIViewController for each TableViewCell was not an ideal approach from the start.
- Regarding the UIKit versions, adding KMP & CMP significantly increased memory usage. However, assigning the garbage collection solely to Kotlin/Native GC — rather than having both Kotlin/Native and Apple’s GCs working together — improved memory management, though it still worse than the Base UIKit version. And personally, I think the Base SwiftUI version would have been better, if I had used ListView instead of LazyVStack inside a ScrollView, in terms of memory usage.
- To sum up, CMP on iOS is still in its alpha stage and needs further optimizations for enhanced performance. Nevertheless, I’m optimistic that CMP will become a significant player in the near future, and I look forward to JetBrains achieving “native performance with the same code-base” in CMP too.
2. Acknowledgements
This is probably the most waited part of the series, as I have already used a lot “in the Acknowledgements section” throughout the series. This part is basically the area where I will explain all the razzle-dazzle and how I solved them. Let’s get started!
2.1 Copying KMP library to CMP library
In the first part of this series, I mentioned that I had to duplicate the business logic from my KMP library (KMPKtorCoinbase) into my CMP library (CmpCoinbase). The reason stemmed from an issue I encountered when using my CMP library on iOS. Specifically, when using my CoinTile composable, it required passing a model into it. Since the CMP library initially depended on the KMP one, and the required model was sourced from the KMP library, XCode expected the KMP model itself rather than the imported (or already dependent) CMP one.
To address this, my initial solution was to add the KMP dependency to the project as well. However, I soon realized this approach was flawed. The CMP library was already dependent on the KMP, and logically, it should have recognized that I was using the same object. Unfortunately, this wasn’t the case. I couldn’t pass the same model with identical variables to the CMP CoinTile because the packages of the models in the dependencies differed post-compilation. Additionally, having both KMP and CMP dependencies forced me to create a mapper for the same model object, despite them being essentially identical but originating from different packages after compilation.
For clarity, let me break this down step by step:
- My KMP library contained a model object named “A”.
- My CMP library depended on the KMP library and utilized model “A” in my CoinTile.
- When attempting to use CoinTile on the iOS side, the composable required passing model “A”. However, this model “A” was expected to come from “KMP.A” rather than “CMP.A”.
- Consequently, I added the KMP library to my iOS app to access “KMP.A”.
- Since I was already relying on CMP for the iOS side, the business logic provided “CMP.A”, which couldn’t be directly passed to the composable. Thus, I had to create a mapper for “CMP.A” to “KMP.A” to make it work.
- Additionally, since CoinTile was clickable and returns the model itself inside the Lambda in order to access the required parameters, there needed an another reverse mapping, which was another flaw requires to be mentioned.
Eventually, I realized this approach was inefficient. Instead of copying the logic to each iOS project individually, I decided to replicate the entire code from my KMP library into the CMP library.
2.2 Using both XML and ImageVector for the same icon
When I was trying to reason about why the app size is increased when the CMP library is introduced to the Android side, I have mentioned that I have used both XML and a pure Kotlin ImageVector for the same icons, in order to make the resource shareable on both platforms, let me explain.
When I first started to implement my CMP library, I relied on the experimental resource sharing library that the JetBrains was working on. Since it was experimental, I was expecting the unexpected. However, no matter my efforts, I couldn’t directly use the XML vectors on the iOS side. I even downloaded and carefully inspected the sample project from the Kotlin Multiplatform Wizard, which successfully used an XML-based Compose logo in iOS, but to no avail. Despite mirroring their approach, my vector icons wouldn’t display on iOS. Subsequently, I asked for an advice on the Kotlin Slack Channel but didn’t receive any responses for days. As a result, I settled on converting the XMLs into ImageVectors by utilizing convertor tool in composables.com and used them in the CMP side. Using a simple “if” check, I differentiated the platform, employing XML for Android and ImageVector for iOS. Since the code compiles OS-independently on the shared side, I believe that both the XML and ImageVector versions of the icons are included in the library, which results to the increase in app size.
2.3 Ktor’s fault on creating the a Base-Url with “defaultRequest”
Solving this issue took me three days, and I even considered abandoning the entire experiment. On the iOS side, I kept encountering an error resembling “InvalidUrl,” even though everything functioned perfectly on Android side. Let me explain.
While working on the Android apps, before even integrating the libraries, I noticed a method in the DSL named “defaultRequest” for HttpClient during creation. This function allows assigning a base URL directly, eliminating the need to specify it in every request. On Android, this worked seamlessly. However, the first time I tried using the KMP library in the iOS app, requests were immediately canceled (or in fact, not even sent) with an “InvalidUrl” exception. The URL with error was displayed on the screen as my flow in the use case returned the error message whenever an exception occurred. Despite adding numerous print statements everywhere, I couldn’t identify the issue. It appeared the process failed even before any request was sent.
After three frustrating days, I decided to try adding the base URL directly instead of relying on “defaultRequest.” Voilà! Everything started working. Initially, it never crossed my mind that this simple function, which creates the base Url, would fail. Least of all, I didn’t expect JetBrains to overlook such a detail in their library’s release version.
2.4 “.description” and “.description()” in Kotlin’s Data Classes along with “description” as a parameter
This issue turned out to be the funniest challenge I faced while implementing the iOS apps for this experiment. Interestingly, Kotlin’s Data Classes include a default “description()” method, once compiled to the iOS side. They also have a “description” variable that returns the same output as the method. The twist came into play with the models I generated from DTOs, which also had their own “description” variable. This led to the following situation when I tried to display them in the UI:
Here’s the catch: when I attempted to access the “description” variable, which was mapped from the DTO’s “description”, it displayed as the Data Class’s “toString()” method on the screen. Therefore, one should bear in mind that not to use “description” as a variable name while working with KMP. I (forcefully) resolved this issue simply by renaming the variable.
2.6 Sharing Libraries
Not every aspect of the Acknowledgements section is about challenges; in fact, this part is a positive highlight! These experiments greatly enhanced my understanding of how to publish KMP and CMP libraries.
For Android, the process is quite straightforward. You can add the maven-publish
dependency to the app-level build.gradle file, which gives you access to various Gradle tasks. There are different tasks for different publishing strategies, but for this experiment, I used local publishing. Once published, the library is easily accessible via the path to its published location.
iOS, as usual, was a bit trickier than Android. For iOS, you have the choice between using Cocoapods or XCFramework to build the library. After building, you can add the library by navigating to the Frameworks & Libraries section of the Target. There, you use ‘Add Package Dependency’ found under the ‘Add Others’ section when you click the “+” icon. Next, you have to select the built framework (either Cocoapods or XCFramework) by locating it in the build folder of your library.
2.7 Access to Native Side
While putting together a reusable ‘Measurers’ library for my projects, my initial plan was pretty straightforward: rely heavily on native-side code and create simple functions in the common area to handle platform-specific methods. On Android, this approach worked like a charm. But when it came to integrating Swift files for these functionalities, I hit a bit of a rough patch, given my little or less experience with iOS.
I discovered a library that could navigate Swift files and generate the needed Objective-C headers, which sounded perfect at first. However, I ran into a roadblock with some inconsistencies between KMP versions. Addressing these issues across the entirety of 15 apps and 2 libraries I was developing would have been too frustrating for the scope of this experiment. So, I decided to put off with that idea and resorted to the more time-tested, good ol’ method of copying and pasting code across projects for the tests.
For those interested, the specific files aren’t in the current projects anymore, but you’re welcome to check out the “measurers” folder in the repository for a closer look.
Conclusion
First off, I want to express my sincere appreciation to you, the reader, for accompanying me on this journey through this series of articles. Your engagement and interest have been invaluable.
This entire experiment took over two months, sparked by the simple yet intriguing question: “Is CMP good?”. Along this path, I faced various challenges, acquired new insights, and gradually developed an appreciation for the convenience and capabilities of KMP and CMP. On the Android side, the experience was quite positive, with the tools proving to be robust and practical. However, the iOS side still has some growing pains, needing more fine-tuning and possibly innovative solutions, particularly regarding its reliance on UIViewControllers. CMP on iOS, being in its Alpha phase, has room for growth, and I’m optimistic about its progress. The results, interestingly, suggest that fully embracing CMP for iOS development might be a worthwhile venture, and I encourage you to experiment with it.
While this series might be concluding, my exploration with these technologies is ongoing. I intend to continue updating the versions and revisiting the tests to observe any future developments. If you’re interested in following these updates, I recommend clicking that “Follow” button. Let’s continue this journey together, exploring the dynamic world of KMP and CMP with a blend of enthusiasm and critical analysis.
Finally, feel free to share and utilize any part of this article series and project, as long as proper credits are attributed. I value your feedback and welcome any questions you may have. You can reach out to me at:
- My LinkedIn: Ali Taha Dinçer
- My E-Mail: alitahasubfly@gmail.com
Keep learning, continue improving!