Kotlin Multiplatform Mobile in QIWI Wallet: Our Story
Since 2021 we have developed every new feature for QIWI Wallet with Kotlin Multiplatform Mobile. The decision to switch to Cross-Platform development was not easy, just like the challenges we faced along the way. But similar engineering approaches and a high level of collaboration inside our teams provided us with a great foundation to embrace the evolution of mobile development. In this article, I want to share with you our story, with all ups and downs.
QIWI Wallet app was launched in 2007, reaching over 15 million monthly active users. Our customers use it as an electronic wallet to pay for goods and services or transfer money across virtual or physical environments. We have 15 mobile engineers mainly distributed across cross-functional teams.
In QIWI, we often experiment with our architectures and development processes. During the past five years, we have built different apps using a variety of approaches. For example, when we created a stock trading app called QIWI Investor, it was the first time when we tried to share a domain logic layer between an Android and iOS app. Back in the day, we used an open-source java to objective-c converter. This solution wasn’t reliable enough. This experiment has finished as the project itself was closed. But the idea of the multiplatform approach was still present in our heads.
In 2018 we started another experiment with our development process in QIWI Wallet. We have switched from separated platform teams to cross-functional teams. It was the most significant update for us since the introduction of Scrum. As a result, we redefined our values. Сollective code ownership has become one of the most remarkable ones. It encouraged us to look closer at each other’s code, share opinions and practices, and solve everyday problems together.
Indeed every platform has its unique patterns, architectures, and best practices. But it appeared to us that our mobile platforms were evolving almost symmetrically in recent years. Notably, Android and iOS architectures have come to a very similar implementation of MVVM-like architecture. Our approaches were so similar that it felt like we duplicated code in multiple layers: data, domain, and presentation. Strictly speaking, mobile engineers were separately solving a set of very similar problems during the sprint. It may not seem like a big deal for simple features with a very straightforward user flow. But things might get interesting when a new component requires complex business logic and navigation between a large set of states.
Since mobile engineers work separately, their final product implementation can vary. It might affect only some corner cases in UX when loading and error states are handled differently in various scenarios, raising some eyebrows in a design department. It can result in more significant bugs since there is no guarantee that business logic works the same way and produces the same states on both platforms. If you track key user actions for funnel analysis, there is also a possibility that sources of these actions may differ from platform to platform. It might affect the quality of data that you analyze. So, designers, QA engineers, and analysts may have to double-check your features on Android and iOS before production since there is a big room for misinterpretation or miscommunication. In the end, these small things can affect the user experience and increase time-to-market.
In this case, it seems that using a multiplatform solution is a self-evident answer. Single data, domain, and presentation layers will result in a more consistent app behavior. If only it were that simple. For us, it all started with a prototype.
Building a proof-of-concept
By the end of 2020, all cross-functional teams worked closely with dedicated platform teams. The primary responsibilities for the platform teams were to support the technological growth of the platform, provide the best instruments and solutions to simplify and speed up the production cycle. When Kotlin Multiplatform Mobile (KMM) was proposed to address all the things that I mentioned above, it was due to the platform teams to prepare a proof-of-concept.
We decided that the most comprehensive proof-of-concept should cover the most popular cases in our application and a few unusual ones, such as using a database to cache user events for our metrics. Most importantly, these cases should correspond to the engineering practices that we have on iOS and Android. With this in mind, we have picked the following components for our proof-of-concept:
- Multiplatform HTTP Client based on Ktor
- Blueprint of MVI-based architecture for new features
- Logging and metrics system with a database as a cache
It took about three months for platform teams to deliver everything from this plan. New HTTP client also defined for us a move to Kotlin Coroutines for asynchronous communication. The team provided an architecture blueprint and also checked its reliability in a production scenario. They refactored an already-existing screen with complex states and made sure that everything was working fine. Finally, logging and tracking systems within a new shared module have brought an excellent conclusion for research. Of course, a successful proof-of-concept doesn’t mean that all next steps of integration will be smooth, but feature teams had a solid ground to start.
Moving to new technology is always an investment. We talked through all potential gains and losses with our product owners. We concluded that we should handle with care the first few sprints when we use a new KMM module. At least we should add some additional time to our estimates since it will take a while to get used to new instrumentation and polish all significant flaws.
So there was not a surprise when we encountered some well-known problems. For example, our first crashes were caused by concurrent mutability when native code on iOS tried to modify immutable objects. Bugs like this were tricky to find because our logs initially did not provide enough information for analysis. Since the shared module is handled like an external library, debugging on iOS is still more time-consuming. But it encouraged us to write more unit and integration tests. It is the only way to ensure that logic in the shared module is working as expected before shipping it to platforms. For Android developers, it was easy to forget that there was no access to the Java standard library. At that time, there was no solution to deal with DateTime or BigDecimal right out of the box. After the first release, we were not entirely happy with our crash reports from iOS devices, but we quickly found an amicable open-source solution that helped us a lot.
Despite the specific problems with KMM, we also had very typical ones. Every move to a new architecture is always accompanied by some issues that reveal themselves only when you start using it. So we made some tweaks along the way with our first features. It really surprised us that an adoption of KMM was not only a challenge for your team’s hard skills but also for your teamwork and collaboration.
Diversity of opinions
When we switched to KMM, our Android and iOS engineers started to write shared code together on a daily basis. It may seem that mobile engineers in every company have a lot in common: creating the same product, similar applications, and solving similar sets of problems. But eventually, we noticed that we had different views on the concepts that we both considered well-known and production-proven. For example, we had a lot of discussions about our new architecture. Our previous iOS and Android approaches had minor differences that led to major discussions. We discussed matters like what we should include in viewstate on every update, whether dialogs are part of viewstate, how to transfer data between viewmodels, how to check the validity of the user input events, where to store resources, etc.
These particular problems were not connected directly with the KMM, but they highlight the valuable thing that KMM brought to us: diversity of opinions. Every discussion was empowered by a different team member with a slightly different perspective. Some solutions were right on the surface, but the others took time and several tries to work out. What definitely pleased us is that every step on this way of KMM adoption had led us to a better code and a better product.
Future is bright
During the last year, we shipped every new feature using KMM. It fitted well in our engineering practices and cross-functional development values. We even shared our experience with more technical details on our Android meetup. We still have some challenges in our roadmap. But most of them are already acknowledged by the JetBrains team, or you can find some great solutions in open source.
One cannot simply underestimate the power of the community that is making KMM better every day. It gives us the confidence to move forward. We are grateful to be in the same boat with companies from all over the world adopting this technology, which seems to be a solid foundation for the future of mobile development.