The dependency injection pattern is widely used to apply the principle of control inversion. Indeed, this pattern contributes to greater code modularity, improves maintainability, and facilitates testing. However, implementing dependency injection can be tricky.
There are several dependency injection frameworks available for Kotlin, including Koin, Dagger- Hilt, Kodein, and kotlin-inject. Koin seems to offer many advantages. Let's see why by comparing it to Hilt, the library recommended by Google for Android.
Hilt uses annotations to specify the attributes to be injected and how to construct them. Although Koin also supports annotations, it also allows dependencies to be configured using a domain- specific language (DSL) that is intuitive and easy to master. This configuration is isolated in a file separate from the business code, facilitating the separation of responsibilities.
Koin and Hilt use different approaches to dependency management. Koin performs injection at runtime as a service locator, instantiating all dependencies and providing the necessary references to classes. In contrast, Hilt performs injection directly at compile time, and its annotations have been compatible with Kotlin Symbol Processing (KSP) since November 2024.
Since Koin must resolve dependencies at runtime, this has an impact on performance, but the difference in performance is negligible.
Hilt, on the other hand, detects errors at compile time. However, Koin, which only detects them at runtime, has not yet said its last word, as it is possible to check Koin's configuration in unit tests and thus avoid deploying a faulty configuration. As far as Kotlin Multiplatform (KMP) is concerned, Hilt is only compatible with native Android, while Koin is multi-platform, offering a notable advantage. Since Google I/O, it seems that the compatibility of Google libraries with KMP is only a matter of time. It remains to be seen whether Hilt will be easily adaptable to KMP.
In conclusion, although Koin is not the inversion of control library recommended by Google, its ease of use and multi-platform compatibility make it a preferred choice for our new projects. For an existing project using Hilt, a migration to Koin should only be considered if a switch to KMP is planned soon.
In multi-module Gradle projects, managing dependencies and their versions is a challenge that raises three main issues. Firstly, dependencies need to be updated module by module. Secondly, it's difficult to check whether a dependency is obsolete. Finally, there is no dependency autocompletion. Creating a custom solution is one option. You'll find several, but they're unlikely to match the quality of the Gradle version catalog. This approche creates a single, centralized catalog in a minimal file based on TOML: here you can store all your dependencies and their versions, including plugins. It's easy to update them as needed.
In fact, the version catalog has one advantage: your IDE will alert you to new library versions and can update them for you. We recommend automating these updates with Dependabot or a similar solution. All modules can safely reference dependencies, so your IDE can help you with autocompletion, even if you're still using Gradle files in Groovy instead of the new KotlinScript files.
Migrating to the version catalog is easy and well documented. It's also the perfect opportunity to switch your Gradle files from Groovy to Kotlin (.kts).
Although RefreshVersion is another viable solution, Gradle Version Catalog has been standardized by Gradle and Google as the official dependency management solution. We recommend switching to this solution for any project that is not already using one of the two viable solutions.
Developers often use code generation to reduce repetition and improve code readability. In Android, annotations are essential. Developers can rely on predefined annotations from libraries such as Room, Hilt, or Glide, or create custom annotations to generate specific code. Historically, the Kotlin Annotation Processing Tool (KAPT) has been the standard in the Android community. It allows Java annotations to be used in Kotlin code, ensuring compatibility with Java libraries.
Kotlin Symbol Processing (KSP), developed by JetBrains and Google, aims to overcome the limitations of KAPT by processing Kotlin annotations directly without converting them to Java bytecode. This approach is twice as fast as KAPT because it eliminates compilation steps. What's more, KSP adapts better to the specifics of the Kotlin language, such as default parameters, coroutines, and data classes.
Many libraries that previously depended on KAPT are now migrating to KSP, and most have already done so. This is a godsend because KSP supports Kotlin multiplatform. So, developers who want to migrate their projects from Android to Kotlin Multiplatform should use KSP instead of KAPT.
If you don't use custom annotations, the migration is straightforward. Each library that requires KAPT provides detailed instructions for switching to KSP.
Although KAPT is still a viable option, KSP surpasses it in every way. It provides better support for Kotlin- specific features and is twice as fast. We strongly recommend switching to KSP.
A recurring frustration for developers with raster images such as PNGs is their lack of flexibility: manual resizing, creating multiple versions for each context or color. Vector images, while superior, also require specific manipulation or versions for each use.
Apple's SF icons provide an elegant solution to these problems. They are vector icons that seamlessly integrate with our application's text, greatly improving the development experience and maintainability. Their flexibility is remarkable: you can adjust the thickness of the stroke, change the color, and make them multicolored, which simplifies the work of designers and ensures better integration into the application's design system. What's more, thanks to the rendering engine shared with the fonts, their performance is optimal.
One of the major advantages of SF Symbols is their harmony with SwiftUI, especially when it comes to animations and integration with hierarchical styles. For developers, this means a reduction in development time, and for users, an improvement in the visual quality of applications.
It's possible to create your own SF symbols using the Sketch tool, either directly or from an existing base. However, since the management of edges is not the same as with SVGs, it is preferable to use a specialized converter such as SfSymbolConverter, especially if you already have an existing icon set. Alternatively, the 6,000 icons available by default offer a quick and efficient solution to reduce the lead time of functionalities.
We recommend using this technology for all your native projects. Despite some subtleties in the creation of custom icons, SF Symbols offers undeniable advantages in terms of integration speed, convenience, flexibility and performance.
Despite its advantages, Swift suffers from problems that affect developer productivity. On the one hand, there is a major problem of slow compilation, particularly pronounced in large projects, where typing causes the compiler to fail with the message "The compiler is unable to type-check this expression in a reasonable time". On the other hand, the validity of concurrent code is delegated to the developers who write it.
Swift 6 provides a solution to these problems. In addition to several new features and syntaxes, one of the most significant improvements in this new version is the increased compilation speed. This improvement alone more than justifies a migration, as it can significantly reduce developer frustration and improve productivity.
But Swift 6 also highlights (statically) compiler-tested concurrent code, allowing developers to reduce the number of these sometimes hard-to-detect bugs. However, this feature is not without its drawbacks. Static validation implies major changes that disrupt existing code bases, requiring significant refactoring and customization efforts. This transition can be difficult and resource-intensive, especially for large projects, despite the gradual migration made possible by the feature flags available in Swift 5.
Given these considerations, we recommend migrating to Swift 6. Although major changes require careful evaluation of the investment, the benefits of improved compile speed and verified concurrency are substantial.
SwiftUI provides powerful code primitives for externalizing state but requires a well designed architecture to take full advantage of their potential and not create gigantic monolithic states. In addition, with a developer community split between UIKit, SwiftUI, @Observable, @ObservableObject, and different architectures, it's not easy to find your way around an existing project.
The Composable Architecture (TCA) is a framework designed to solve these problems in iOS applications. Inspired by the one-way Flux architecture popularized by Redux, TCA adapts this approach to Swift, making state management more intuitive and less verbose than in JavaScript. Unlike libraries like ReSwift, TCA is designed for SwiftUI while remaining compatible with UIKit.
TCA structures state around user actions, ensuring a clear, traceable flow of changes, facilitating testing, and enabling modular architecture. Functionality can be developed independently and then integrated into a complete application. This formalization of the code improves the development experience and reduces the learning curve. What's more, the community around TCA is robust, offering extensive documentation and tutorials to support developers.
The addition of navigation management, as well as the backporting of certain features that were otherwise unavailable on earlier versions of iOS, such as state observability, attest to the library's transformation this year and propel it to the forefront of its field.
At Theodo, we've successfully implemented The Composable Architecture in production projects and highly recommend it for state management in SwiftUI applications.
We've featured Tuist in our previous Tech Radar for its ability to improve iOS project management. Using it simplifies build configuration. Better management of build caches speeds up compilation times, especially for projects based on modular architecture. However, because of the major changes introduced, this new version may prove divisive.
The first major change is the end of Carthage support: each project will now have to manage the fetching of Carthage dependencies, which may require significant adjustments to existing workflows.
What's more, Tuist 4 no longer supports application signing, forcing developers who previously used this feature to completely rethink their signing workflow. Another new feature of Tuist 4 is the use of Swift Package Manager (SPM) files instead of a specific format. This transition allows for better integration with other tools in the ecosystem (e.g. Xcode or dependabot).
Perhaps this is really the maturity phase, as Tuist focuses on what it does well to do it better. Finally, the challenges mentioned above are fortunately made easier thanks to the extensive migration documentation and good community support; this makes it possible to take advantage of the significant improvements in build performance brought about by Tuist 4. The syntax is also even more intuitive, which makes the tool easier to use and speeds up development.
We always recommend using Tuist for new projects. We also recommend migrating to Tuist 4, as the long-term performance and project management benefits far outweigh the initial transition challenges.
The growing complexity of mobile applications poses a number of challenges, including the maintainability of a growing code base, increasing build times and difficulties in complying with the test pyramid. To address these issues, companies such as SoundCloud and JustEat have popularized micro-feature architecture, which divides the monolith into smaller, more specialized modules.
This architecture is composed of four types of modules:
The micro-feature architecture offers significant advantages: it facilitates testing of business logic, reduces build times thanks to caching, and enables common code to be shared when teams expand, or new products are developed. This approach also simplifies upgrades and partial overhauls of complex applications.
However, its implementation requires good domain expertise for efficient slicing, a solid understanding of software architecture principles, and good initial design. Tools like Tuist for iOS* can facilitate this slicing, improve caching, and make the transition to this architecture smoother.
At Theodo, micro- features have become a benchmark choice for new native projects. We've been able to test just how much this architecture can simplify the upgrading or partial redesign of complex applications.
With Jetpack Compose, when the parameter of a composable changes, only the affected part is updated, provided the rest is stable. If a parameter is considered unstable, the entire view will be re-rendered even if its value has not changed. This can slow down rendering and increase the load on the UI thread, affecting the performance of complex screens.
A major problem is that any class originating from a different module is considered unstable by the Compose compiler. Model classes are often separated from views to respect the separation of responsibilities, and this degrades the stability of composables. You can detect these stability problems with tools such as the layout inspector or the compose compiler report. To solve this problem, three main strategies were used:
Jetpack Compose recently introduced a stability configuration file, a declarative approach to managing stability without modifying code. This file enables the Compose compiler to treat listed classes as stable.
Using this file is faster, more elegant and less restrictive than other solutions. Developers can set up stable classes in a root file or in separate files for each module.
At Theodo, we see great potential in this solution. Although it is new and may have yet unknown limitations, we recommend a gradual implementation to ensure project compatibility.
Kotlin Multiplatform (KMP) tackles a central problem of modern development: avoiding code duplication and the associated costs, such as the multiplication of bugs and inconsistencies between platforms. There are many cross-platform solutions on the market, such as React Native or Flutter. These solutions are frameworks that offer a single code base for creating complete applications that target multiple platforms.
KMP takes a more flexible approach. It's not a framework, but a technology enabling the Kotlin language to compile on platforms other than the JVM used for native Android development.
This more modular approach makes KMP ideal for sharing code between different platforms (in the form of a library) without imposing anything.
On iOS, for example, Kotlin code is compiled and generates a library that can be used in iOS projects just like any other library native to this platform. This flexibility means that code can be shared according to need: a simple function, a specific layer of the application (network, business, etc.), a specific feature, or even the entire code. You only share what you want to share.
Thanks to KMP, Kotlin benefits from bi-directional interoperability: Kotlin code can be compiled and integrated into native code, while existing native libraries can be used in platform-specific Kotlin code, simplifying the bridges often needed to exploit platform-specific functionalities.
The KMP ecosystem is booming, with Google actively working to make its native Android libraries compatible with KMP. On the other hand, the community is also very active, and the most popular libraries in the Android universe are already compatible with KMP or very close to it, such as Room, Retrofit, Coil, Koin, etc.
KMP is now stable on almost all platforms, including Android, iOS, Desktop (Windows, Mac, Linux), and even the web by transpiling to JavaScript/TypeScript. Compilation to WebAssembly is still in alpha. Compilation for iOS is currently based on Objective-C, although Jetbrains announced the imminent arrival of KotlinTo- Swift at Kotlin Conf 2024 in Copenhagen.
KMP is an ideal solution for sharing code between different platforms. We encourage you to give it a try: adding a KMP module to an Android project is straightforward (just a few configurations in Gradle) and you can start sharing code. KMP is perfect for creating a cross-platform library. However, if you also want to share the user interface, you'll need to use Compose Multiplatform (CMP), which is not yet as stable as KMP.
We encourage you to start your new Android projects directly with KMP to guarantee their scalability. This in no way affects the project itself but will make it easier for you to share code in the future.
In development, it is often difficult to make code testable and maintain a scalable code base due to the strong coupling of dependencies. An effective solution to this problem is dependency injection, which allows components to be decoupled from the code, thus improving its testability and maintainability.
Factory is a modern library that effectively solves these problems for Swift developers. Key benefits include dependency injection checking at compile time, ensuring that no errors occur at runtime due to missing or incorrectly injected dependencies. Factory also allows the use of injection scopes, facilitating the implementation of true inversion of control thanks to the Dependency Injection pattern.
Last year, we recommended keeping Resolver for projects using UIKit; however, due to its depreciation, we now recommend changing dependency injection libraries. For those wishing to retain a similar syntax and more functionality, Factory is an excellent alternative. Because of the similarity of its syntax and operation, migration from Resolver to Factory is straightforward. Factory documentation has also been improved, offering clear, detailed instructions for efficient use in SwiftUI and UIKit projects.
We recommend you give Factory a try in your projects, as it offers significant potential for improving the code quality and performance of iOS applications. Although there are trade-offs, its benefits warrant serious evaluation for future projects.
Data storage in mobile applications is essential to enhance the user experience and avoid waiting times. A caching system makes it possible to operate in offline- first mode, displaying cached data first before refreshing it via a network call.
SQLite is the standard solution for relational databases on mobiles, but it's often preferable to use an ORM (Object-Relational Mapping), a library that interfaces a database with the rest of the code, to simplify interactions and typing.
In the context of Android, Google has created and integrated Room into Android Jetpack, a set of libraries designed to simplify the task of developers. With Room, it's possible to easily declare the entire database structure and various queries in a few lines of Kotlin code, with annotations.
Room is a mature library, in existence since 2018, which benefits from comprehensive documentation and is easy to use. It integrates easily with Kotlin and offers reactive operations. For example, when a table is modified (adding, modifying or deleting rows), Room automatically emits a new value, enabling components to update themselves via reactive methods such as flows (coroutines), observables (Rx) or Livedata.
What's new in 2024 is that, since May, Room has been compatible with Kotlin Multiplatform. Although this version is still in alpha, Google has announced its commitment to KMP, which points to a stable release before the end of the year.
There are serious competitors to Room, such as SQL Delight or Realm, which also offer compatibility with KMP. In any case, Room remains relatively simple to use it's a safe bet for Android and soon for KMP too. You can safely integrate it into your Android application and envisage its use in a future Multiplatform version.
In iOS development, decoupling dependencies is a recurring challenge because there is no official solution. As a result, projects often have highly coupled code, making testing and maintenance difficult.
Swift Dependencies is a dependency injection library designed to address these issues in Swift applications. Initially designed for The Composable Architecture (TCA), it can also be used as a standalone library.
Like all dependency injection mechanisms, it decouples your code from its dependencies and allows mocks to be injected during testing, making it easier to test and maintain. It also provides a method for injecting stubs for previewing. This library uses similar dependency management mechanisms to SwiftUI's @Environment, making it easy to learn. It also includes a macro to quickly implement most dependencies, speeding up their definition and reducing boilerplate code.
Swift Dependencies is lightweight and compatible with incremental adoption, allowing teams to integrate at their own pace. However, this lightness comes at the cost of certain features, such as weak injections and injection scopes, which must be reimplemented on a case-by-case basis.
Swift Dependencies offers a compelling way to manage dependencies, whether in a TCA project or as an incremental migration in a non-injection project, although the lack of important functionality may be a barrier to adoption.
In programming, we distinguish between business errors (a password that's too short) and technical errors (a failed network request). Business errors are normal cases of execution and must be handled in the same way as successes, whereas technical errors are implementation details.
Historically, Java has used mandatory checked exceptions. However, they are difficult to manage and often pollute the business code, making it implementation dependent. What's more, they are regularly abused to handle business errors, which is costly in terms of performance because they generate stacktraces.
Kotlin has responded to this problem with unchecked exceptions, making error handling optional. This reduces the robustness of the code but decouples the business code from the technical implementation.
Arrow, a library for Kotlin, provides a functional- programming-inspired approach to handling business errors: Typed Errors. This distinguishes technical errors, which are handled by unchecked exceptions, from business errors, with two main methods:
These methods avoid trycatch and stacktraces, improving performance and flexibility.
Arrow is a powerful tool for making Kotlin code more robust and readable. Both are already must- haves, but Raise, while promising, remains limited by its reliance on experimental features.
Gradle configuration is a must for any Android or Kotlin multiplatform project, but it can quickly become complex if you think outside the box. This is especially true for multi-module projects, multiple flavors, and third- party plugin integration. This complexity, often a source of frustration, requires a steep learning curve even for experienced developers.
In response to these challenges, JetBrains has developed Amper, a new build system designed to simplify configuration. Intuitive and powerful, Amper integrates seamlessly with your existing ecosystem. It is available as a standalone version for simple projects and as a Gradle plugin for those who need the Gradle ecosystem.
Amper simplifies the configuration of Kotlin multiplatform projects with one file per module, reducing Gradle's boilerplate. As a Gradle plugin, Amper is fully interoperable and provides a Gradle fallback for any unsupported functionality.
Amper supports building and running JVM, Android, iOS, desktop and web applications, as well as building Kotlin multiplatform libraries. It also lets you mix Kotlin, Java and Swift code, while supporting multi- module projects and the use of Compose Multiplatform.
However, Amper is still in preview and not recommended for production projects: it's still young, with an API that's likely to change, and missing features. Nevertheless, this is a good time to experiment and provide feedback to JetBrains, who are known for listening to their users.
Amper is specifically designed to facilitate the development of Kotlin multiplatform projects and could become an interesting solution to explore for developers who want to simplify the management and maintenance of their builds.
Kotlin Multiplatform (KMP) allows you to share code written in Kotlin between different platforms (Android, iOS, desktop, web, etc.), but not to create user interfaces. So even when you're sharing business logic, you have to use native solutions (like Compose on Android or SwiftUI for iOS).
To remedy this, JetBrains has developed Compose Multiplatform (CMP), which allows you to write a common UI for all platforms using Compose's Android-proven API and Flutter's proven Skia rendering engine.
CMP is a very promising technology, but it is still in its infancy and stability levels vary from platform to platform:
CMP is very promising, and we believe in its potential: for projects targeting Android and desktop, it can be adopted without hesitation. For iOS, it's also highly recommended, despite its beta status. For the web, however, it's still too early to use CMP in largescale production. The rapid progress made by JetBrains, and the community is encouraging, so we'll be keeping a close eye on this technology.
The @Observable
annotation, introduced with iOS 17, is a big step forward for the responsiveness of iOS applications. However, this feature is only available in iOS 17 and later. This poses a problem for developers who need to maintain compatibility with earlier versions of iOS (iOS 14 through 16), depriving them of this important feature.
The swift-perception
library aims to solve this problem by backporting @Observable
for iOS versions 14 to 16, allowing it to be used without requiring users to update their OS or create more e-waste.
However, there are a few drawbacks to using swift perception. First, while this solution emulates the behavior of @Observable
, it is not as well integrated as the native implementation. The syntax provided is similar but not identical to @Observable
: you must wrap your views in a special component, which adds noise to the code. This makes the code less pleasant to read, and increases the workload involved in deleting the library when it becomes obsolete.
What's more, like all libraries that use Swift's advanced macros, swift perception adds significantly to compilation time. There are solutions to mitigate this, such as precompiling swift-syntax, but the core of the problem remains.
In conclusion, swift perception is a valuable solution for developers who want to use @Observable
on iOS versions prior to 17. Despite its drawbacks, it allows you to prepare your code for the future without losing users. At Theodo, we're integrating it into our R&D and recommend that you try it for your iOS 14 to 16 compatible projects.
XCTest has been the epitome of iOS testing since it replaced OCUnit, but it's notoriously verbose and inflexible. In fact, the amount of test code grows exponentially and is not easy to read. This has led developers to look for alternatives such as Quick. But while these tools have a more expressive syntax, they don't address the problem of verbosity, and they require a longer learning curve.
Swift Testing is a promising answer to these challenges. This framework has a syntax that leverages the power of macros and annotations to be more intuitive and less verbose. For example, test parameterization allows developers to seamlessly run the same test with different inputs.
In addition, Swift Testing improves test organization with a Swift type-based hierarchy and tag-based categorization. It also takes advantage of Swift 6 to improve the safety of concurrent tests, but most importantly, to launch tests in parallel by default, greatly improving their execution speed.
Finally, integration with existing XCTest-based tests is straightforward, enabling easy migration without overhauling the entire test infrastructure.
At Theodo, we're keeping a close eye on Swift Testing. Its innovative approach and features make it a serious candidate for future adoption. However, due to its newness, it is currently in the evaluation phase. We believe that Swift Testing has the potential to become a key tool in our testing strategy as it proves its reliability and adds new features (such as UI or performance testing).
Hot Reload is the ability to see changes made to a running application without waiting for it to recompile. This feature speeds development by allowing developers to see the effects of their changes in real time and in a context that is closer to the real world than previews.
This capability is generally considered inaccessible to compiled technologies like SwiftUI. However, there is a library that makes this possible by recompiling and then injecting the modified code using very low-level tools: InjectIII. This library also supports UIKit, Vapor, and even Bezel.
However, using InjectIII comes with some serious concessions. The initial installation can be long and frustrating. After that, you'll have to be very patient in the face of technical limitations: you may have to change the path to your application or compromise your Mac's security to enable hot reloading. What's more, some SwiftUI features, such as .onChange
, are not compatible with hot reloading.
The main drawback of this library, however, is that it requires you to modify all components in your codebase to clear their types to AnyView
. This significant customization can interfere with SwiftUI's identification mechanisms, affecting your application's animations and performance during development. A special workflow is also required to remove all traces of the library in production, otherwise the security and performance of the application will be compromised.
In spite of the technical feat, we don't recommend using this library due to its important drawbacks. Nevertheless, it shows that hot reloading is possible, and some Apple engineers might be inspired by it.
Find out what our experts have to say about the techniques, platforms, tools, languages and frameworks associated with the main mobile technologies we use every day at BAM: React Native, Flutter and Native.